diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0dafacd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +node_modules +.next +.git +.env +.env.local +logs +sicherungen +coverage + +# Nur für die Entwicklung. Halten den Kontext klein und sorgen dafür, dass ein +# defektes Werkzeugskript nicht den Bau des Abbilds blockiert – geprüft werden +# sie ohnehin von "npm run pruefen". +e2e +screenshots +test +test-results +playwright-report +playwright.config.ts +playwright.screenshots.config.ts +vitest.config.ts +docs +devlog +*.sh +scripts/pruefen.mjs +scripts/integrationstest.ts +scripts/migrationstest.ts +scripts/screenshots.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1014ba7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cf43fca --- /dev/null +++ b/.env.example @@ -0,0 +1,68 @@ +# --------------------------------------------------------------------------- +# Planer – Beispielkonfiguration +# Kopieren nach .env und ausfuellen. .env gehoert NIE ins Git. +# --------------------------------------------------------------------------- + +# Welche Instanz laeuft hier? Steuert Provider, Integrationen und Routen. +# Erlaubt: firma | daheim +INSTANZ=daheim + +# Oeffentliche Adresse der Anwendung, ohne abschliessenden Schraegstrich. +APP_URL=http://localhost:3000 + +# --- Datenbank ------------------------------------------------------------- +# Muss zu den POSTGRES_*-Werten unten passen, wenn Docker Compose benutzt wird. +DATABASE_URL=postgresql://planer:bitte-aendern@localhost:5432/planer?schema=public + +# Nur fuer docker compose: Zugangsdaten des Postgres-Containers. +POSTGRES_USER=planer +POSTGRES_PASSWORD=bitte-aendern +POSTGRES_DB=planer + +# Nach aussen veroeffentlichte Ports. Aendern, wenn 5432 oder 3000 auf dem +# Rechner schon belegt sind; DATABASE_URL muss dann denselben Port nennen. +POSTGRES_PORT=5432 +APP_PORT=3000 + +# --- Anmeldung ------------------------------------------------------------- +# Signiert Sitzungen und Tokens. Erzeugen mit: +# openssl rand -base64 32 +AUTH_SECRET= + +# Hinter einem Reverse Proxy auf true setzen, damit Auth.js dem Host vertraut. +AUTH_TRUST_HOST=true + +# Anmeldung per E-Mail und Passwort erlauben. In der Firma ueblicherweise +# false, damit nur ueber OIDC angemeldet wird. +ANMELDUNG_PASSWORT=true + +# Erstes Verwaltungskonto, wird vom Seed angelegt. Ohne ADMIN_PASSWORT +# entsteht ein Konto ohne Passwort, das sich nur per OIDC oder Passkey +# anmelden kann. Nach der Einrichtung dürfen beide Werte wieder raus. +ADMIN_EMAIL= +ADMIN_PASSWORT= + +# --- OIDC ------------------------------------------------------------------ +# firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab. +# Der Rueckleit-URI beim Anbieter lautet: +# /api/auth/callback/oidc +OIDC_NAME= +OIDC_ISSUER= +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= + +# --- Passkeys (WebAuthn) --------------------------------------------------- +# RP_ID ist die nackte Domain ohne Schema und Port, z. B. planer.example.de. +# Lokal ist "localhost" richtig. +WEBAUTHN_RP_ID=localhost +WEBAUTHN_RP_NAME=Planer +# Muss exakt der Herkunft im Browser entsprechen, inklusive Port. +WEBAUTHN_ORIGIN=http://localhost:3000 + +# --- Sonstiges ------------------------------------------------------------- +# Zeitzone fuer Anzeigen und Tagesgrenzen. +TZ=Europe/Berlin + +# Nur fuer die Screenshot-Instanz. Schaltet den Demo-Kalenderprovider frei +# und darf in echten Umgebungen niemals true sein. +DEMO_MODUS=false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..168ae29 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Im Git immer LF, damit die Shellskripte auch unter Linux laufen. +* text=auto eol=lf +*.sh text eol=lf +*.png binary +*.jpg binary +*.ico binary +*.woff2 binary +package-lock.json -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..253c276 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Abhängigkeiten +node_modules/ +.pnp +.pnp.js + +# Next.js +.next/ +out/ +next-env.d.ts + +# Erzeugter Prisma-Client +src/generated/ + +# Tests +coverage/ +test-results/ +playwright-report/ +blob-report/ +.playwright/ + +# Geheimnisse und lokale Umgebung +.env +.env.local +.env.*.local +!.env.example + +# Betrieb +logs/ +sicherungen/ +*.dump + +# Zeiterfassung: laufender Eintrag ist lokal +devlog/.zeit-offen + +# Werkzeuge +*.tsbuildinfo +.DS_Store +Thumbs.db +.vscode/* +!.vscode/extensions.json +.idea/ +screenshot-lauf.log diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ebb889a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +.next +node_modules +package-lock.json +docs/bilder +prisma/migrations +logs +test-results +playwright-report +coverage +devlog/zeiten.csv diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..df47e06 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 90, + "tabWidth": 2, + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..61f3fd4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Änderungen + +Alle nennenswerten Änderungen an diesem Projekt stehen hier. +Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), +die Versionierung [Semantic Versioning](https://semver.org/lang/de/). + +## [Unveröffentlicht] + +## [0.1.0] – 2026-09-10 + +### Hinzugefügt + +- Projektgerüst mit Next 16 (App Router, `output: "standalone"`), React 19, + TypeScript strict, Tailwind 4 und shadcn-Primitiven. +- `npm run pruefen` bündelt Prettier, ESLint, `tsc --noEmit`, Vitest und + ShellCheck. ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt. +- Datenmodell für Benutzer mit Rolle, OIDC-Konten, Passkeys, Bereiche und + Audit-Einträge; PostgreSQL 16 über Prisma 7 mit pg-Treiberadapter. +- Anmeldung über einen generischen OIDC-Anbieter, über Passkeys + (SimpleWebAuthn) und wahlweise über E-Mail und Passwort. +- Rollen `BENUTZER` und `ADMIN`. Jede schreibende Aktion prüft die Rolle + serverseitig in der Seite **und** noch einmal in der Server Action. +- Audit-Protokoll für jede schreibende Aktion, mit Protokollseite für die + Verwaltung. Details werden vor dem Speichern gesäubert. +- Instanzsteuerung über `INSTANZ=firma|daheim`. Was eine Instanz nicht darf, + liefert dort 404 und lädt seine Konfiguration gar nicht erst. +- Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag. +- Zeitrechnung durchgehend in Europe/Berlin, mit Tagesgrenzen, Wochenbeginn + am Montag und deutschen Anzeigeformaten. +- `/api/health` mit Status, Version, Instanz und Erreichbarkeit der Datenbank. +- Hell und Dunkel, Navigation mit Rollenfilter, Oberfläche auf Deutsch. +- Dockerfile in drei Stufen und Compose-Stapel mit eigenem Migrationsdienst. +- `backup.sh` (pg_dump im Custom-Format, `.env` und Uploads), + `update.sh` (mit Rückrollpfad), `scripts/release.sh` und `scripts/zeit.sh`. +- Tests: Unit mit Vitest, Integrationstest und Migrationstest gegen eine + echte Datenbank, E2E-Rauchtest mit Playwright. +- `npm run screenshots` nimmt die Handbuchbilder in einer Wegwerf-Instanz mit + ausschließlich erfundenen Daten auf. +- README, Schnellstart, Handbuch, ROADMAP und DEVLOG. + +### Sicherheit + +- Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, + damit sich nicht abfragen lässt, wer ein Konto hat. +- Sitzungen als JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage + frisch aus der Datenbank gelesen — eine Sperre wirkt sofort. +- Die WebAuthn-Herausforderung wird nach einem Versuch verworfen, gleich ob + er geglückt ist. +- Der Demo-Seed verweigert den Dienst ohne `DEMO_MODUS=true`. +- `allowDangerousEmailAccountLinking` ist bewusst aktiv: je Instanz gibt es + genau einen vertrauenswürdigen OIDC-Anbieter, der die Adresse selbst prüft. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6250b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# Planer – Abbild für den Betrieb. +# +# Die Stufe "bauen" dient zugleich als Abbild für den Migrationsdienst: die +# Prisma-CLI bringt eine ganze Reihe Module mit, die im schlanken +# Laufzeitabbild nichts zu suchen haben. Siehe docker-compose.yml. +# --------------------------------------------------------------------------- + +FROM node:24-alpine AS abhaengigkeiten +WORKDIR /app +RUN apk add --no-cache libc6-compat +COPY package.json package-lock.json ./ +# Die Bauwerkzeuge werden gebraucht und bleiben in dieser Stufe. +RUN npm ci --ignore-scripts && npm rebuild prisma @prisma/engines esbuild unrs-resolver + + +FROM node:24-alpine AS bauen +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=abhaengigkeiten /app/node_modules ./node_modules +COPY . . +# Der Prisma-Client wird erzeugt, bevor Next baut. +RUN npx prisma generate && npm run build + + +FROM node:24-alpine AS betrieb +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + TZ=Europe/Berlin + +RUN apk add --no-cache tzdata curl \ + && addgroup --system --gid 1001 planer \ + && adduser --system --uid 1001 --ingroup planer planer + +# Die standalone-Ausgabe bringt nur die wirklich benutzten Module mit. +COPY --from=bauen --chown=planer:planer /app/.next/standalone ./ +COPY --from=bauen --chown=planer:planer /app/.next/static ./.next/static +COPY --from=bauen --chown=planer:planer /app/public ./public + +USER planer +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=5 \ + CMD curl -fsS http://127.0.0.1:3000/api/health || exit 1 + +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..71244c4 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# Planer + +Aufgaben, Ideen, Projekte und Zeitblöcke an einem Ort. + +Die Kernidee: Kalender aus Microsoft 365 (Firma, Gewerbe) und Google (privat) +lesen und Aufgaben per Ziehen und Ablegen als Zeitblöcke direkt in diese +Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben +wird. + +> **Stand:** Meilenstein 1 von 8. Die Grundlage steht: Anmeldung, Rollen, +> Protokoll, Bereiche und der Betrieb. Aufgaben kommen mit M2, das Lesen der +> Kalender mit M3, die Wochenplanung mit M4. Der [Fahrplan](ROADMAP.md) sagt, +> was wann folgt. + +- **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone` + zur ersten Anmeldung +- **[Handbuch](docs/handbuch/)** – Anleitung für den Alltag +- **[Fahrplan](ROADMAP.md)** – was geplant ist und was bewusst nicht +- **[Änderungen](CHANGELOG.md)** – was sich je Version getan hat + +## Funktionsumfang + +### Heute + +- Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und + Passwort — je Instanz einzeln zuschaltbar +- Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird + serverseitig geprüft und hinterlässt einen Protokolleintrag +- Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag +- Hell und Dunkel, Oberfläche durchgehend auf Deutsch +- Betrieb über Docker Compose, mit Sicherung, Aktualisierung und Healthcheck + +### Geplant + +Aufgaben und Projekte, Kalender lesen und schreiben, Wochenplanung per Ziehen +und Ablegen, Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und +ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). + +## Bildergalerie + +| | Hell | Dunkel | +| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) | +| Start | ![Startseite, hell](docs/bilder/start-hell-schreibtisch.png) | ![Startseite, dunkel](docs/bilder/start-dunkel-schreibtisch.png) | +| Bereiche | ![Bereiche, hell](docs/bilder/bereiche-hell-schreibtisch.png) | ![Bereiche, dunkel](docs/bilder/bereiche-dunkel-schreibtisch.png) | +| Einstellungen | ![Einstellungen, hell](docs/bilder/einstellungen-hell-schreibtisch.png) | ![Einstellungen, dunkel](docs/bilder/einstellungen-dunkel-schreibtisch.png) | +| Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) | + +Alle Bilder entstehen mit `npm run screenshots` in einer Wegwerf-Instanz mit +ausschließlich erfundenen Daten — niemals aus einer Instanz mit echten Daten. + +## Die zwei Instanzen + +Ein Abbild, zwei Deployments, gesteuert über `INSTANZ`. Die Umschaltung wird +**serverseitig erzwungen**: Was eine Instanz nicht darf, liefert dort 404 und +lädt seine Konfiguration gar nicht erst. + +| | `firma` | `daheim` | +| -------------- | -------------------------------------- | ------------------------------ | +| Anmeldung | OIDC Entra ID | OIDC authentik | +| Kalenderkonten | Graph (Firmen-Tenant) | Graph (Gewerbe-Tenant), Google | +| Mail | Graph Firmenpostfach | Graph Gewerbepostfach, Gmail | +| Integrationen | Zammad, Wartungsplaner, Spiegel senden | Telegram, Spiegel empfangen | + +Ein falsch geschriebener Wert bricht den Start ab, statt still auf einen +Standard zu fallen — in der Firma dürfen keine Provider anlaufen, die dort +nichts zu suchen haben. + +## Umgebungsvariablen + +Alle Werte stehen in `.env`. Vorlage: [`.env.example`](.env.example). +`.env` gehört niemals ins Git. + +### Pflicht + +| Variable | Beispiel | Bedeutung | +| ----------------- | ---------------------------------------------------- | ---------------------------------------------- | +| `INSTANZ` | `daheim` | `firma` oder `daheim` | +| `APP_URL` | `https://planer.example.de` | Öffentliche Adresse, ohne Schrägstrich am Ende | +| `DATABASE_URL` | `postgresql://planer:…@db:5432/planer?schema=public` | Verbindung zur Datenbank | +| `AUTH_SECRET` | _(32 Byte Base64)_ | Signiert Sitzungen. `openssl rand -base64 32` | +| `WEBAUTHN_RP_ID` | `planer.example.de` | Nackte Domain, ohne Schema und Port | +| `WEBAUTHN_ORIGIN` | `https://planer.example.de` | Exakt die Herkunft im Browser | + +### Datenbank im Compose-Stapel + +| Variable | Standard | Bedeutung | +| ------------------- | ---------- | ---------------------------------------------- | +| `POSTGRES_USER` | `planer` | Benutzer der Datenbank | +| `POSTGRES_PASSWORD` | _(keiner)_ | Pflicht. Muss zu `DATABASE_URL` passen | +| `POSTGRES_DB` | `planer` | Name der Datenbank | +| `POSTGRES_PORT` | `5432` | Nach außen veröffentlichter Port | +| `APP_PORT` | `3000` | Nach außen veröffentlichter Port der Anwendung | + +### Anmeldung + +| Variable | Standard | Bedeutung | +| -------------------- | -------- | ------------------------------------------ | +| `AUTH_TRUST_HOST` | `false` | Hinter einem Reverse Proxy auf `true` | +| `ANMELDUNG_PASSWORT` | `false` | Anmeldung mit E-Mail und Passwort erlauben | +| `OIDC_NAME` | _(leer)_ | Beschriftung des Anmeldeknopfs | +| `OIDC_ISSUER` | _(leer)_ | Issuer-URL des Anbieters | +| `OIDC_CLIENT_ID` | _(leer)_ | Client-ID | +| `OIDC_CLIENT_SECRET` | _(leer)_ | Client-Secret | +| `WEBAUTHN_RP_NAME` | `Planer` | Name, den das Gerät beim Passkey anzeigt | + +OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der +Rückleit-URI beim Anbieter lautet `/api/auth/callback/oidc`. + +### Einrichtung und Sonstiges + +| Variable | Standard | Bedeutung | +| ---------------- | --------------- | ---------------------------------------------------------------------------------------- | +| `ADMIN_EMAIL` | _(leer)_ | Erstes Verwaltungskonto, wird vom Seed angelegt | +| `ADMIN_PASSWORT` | _(leer)_ | Mindestens 12 Zeichen. Ohne den Wert entsteht ein Konto, das nur OIDC oder Passkey kennt | +| `TZ` | `Europe/Berlin` | Zeitzone für Anzeigen und Tagesgrenzen | +| `DEMO_MODUS` | `false` | Nur für die Screenshot-Instanz. Im Betrieb niemals `true` | + +## Betrieb + +### Starten und stoppen + +```bash +docker compose up -d # starten +docker compose logs -f app # Protokoll mitlesen +docker compose down # stoppen, Daten bleiben +``` + +Der Stapel besteht aus drei Diensten: `db`, ein Einmaldienst `migration`, der +die Migrationen einspielt und sich beendet, und `app`. + +### Healthcheck + +```bash +curl https://planer.example.de/api/health +``` + +```json +{ + "status": "ok", + "version": "0.1.0", + "instanz": "daheim", + "datenbank": "ok", + "zeit": "2026-03-17T08:20:00.000Z" +} +``` + +Die Auskunft ist bewusst ohne Anmeldung erreichbar und enthält deshalb keine +Zählstände, Namen oder Adressen. Bei `"status": "fehler"` antwortet der +Endpunkt mit HTTP 503. + +### Aktualisieren + +```bash +./update.sh # auf den neuesten Tag +./update.sh --nur-pruefen # nur zeigen, was passieren würde +./update.sh --version v0.2.0 # auf eine bestimmte Version +./update.sh --ja # ohne Rückfrage +``` + +Das Skript prüft die Voraussetzungen, zeigt die Änderungen aus dem CHANGELOG, +legt eine Sicherung an, wechselt auf den Ziel-Tag, baut, startet und wartet +auf einen gesunden `/api/health` mit der erwarteten Version. Klappt das nicht, +rollt es auf die vorherige Version zurück und nennt den Pfad der Sicherung. +Jeder Lauf wird nach `logs/update-.log` protokolliert. + +Die **Datenbank wird beim Zurückrollen nicht automatisch zurückgespielt** — +das wäre bei einem Teilfehler gefährlicher als hilfreich. Das Skript nennt den +Befehl dafür. + +### Sicherung und Wiederherstellung + +```bash +./backup.sh # Datenbank, .env und Uploads +./backup.sh --tage 90 # längere Aufbewahrung +./backup.sh --nur-datenbank # nur der Dump +``` + +Je Lauf entstehen zwei Dateien in `sicherungen/`: +ein `pg_dump` im Custom-Format und ein tar.gz mit `.env` und `uploads`. + +Wiederherstellen: + +```bash +# Datenbank +docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists \ + < sicherungen/planer-db-2026-03-17_02-00-00.dump + +# Dateien +tar -xzf sicherungen/planer-dateien-2026-03-17_02-00-00.tar.gz +``` + +Die Sicherung enthält `.env` und damit alle Geheimnisse. Sie gehört so +geschützt wie die Datenbank selbst. + +Nächtlich per cron: + +```cron +0 2 * * * cd /opt/planer && ./backup.sh --tage 30 >> logs/backup.log 2>&1 +``` + +## Entwicklung + +```bash +npm install +docker compose up -d db # nur die Datenbank +npm run db:migrieren # Migrationen einspielen +npm run db:seed # Bereiche und Verwaltungskonto +npm run dev # Entwicklungsserver +``` + +### Prüfungen + +```bash +npm run pruefen # Prettier, ESLint, tsc, Vitest, ShellCheck +npm run test:integration # gegen eine echte Datenbank +npm run test:migration # Migrationen gegen eine Wegwerf-Datenbank +npm run test:e2e # Playwright gegen die gebaute Anwendung +npm run screenshots # Handbuchbilder neu aufnehmen +``` + +`npm run pruefen` muss vor jedem Commit grün sein. Fehlt `shellcheck` lokal, +wird das Docker-Abbild `koalaman/shellcheck` benutzt. + +### Aufbau + +``` +prisma/ Schema, Migrationen, Seed, Demo-Seed +scripts/ Prüf-, Test- und Betriebsskripte +src/app/ Seiten und API-Routen; (app) = angemeldeter Bereich +src/components/ Bausteine; ui/ enthält die Primitiven +src/lib/ Reine Logik ohne DB und ohne Systemzeit, je Datei ein Test +src/server/ DB-nahe Dienste, Server Actions, Provider +e2e/ Playwright-Rauchtests +screenshots/ Aufnahme der Handbuchbilder +docs/ Schnellstart, Handbuch, Bilder +devlog/ Entwicklungstagebuch und Zeiterfassung +``` + +Reine Funktionen bekommen die aktuelle Zeit als Parameter, damit sie ohne +Tricks prüfbar bleiben. Die Tests laufen bewusst in UTC — so fällt sofort auf, +wenn irgendwo die Ortszeit des Servers durchschlägt statt Europe/Berlin. + +### Bekannte Meldungen + +`npm audit` meldet vier Funde in transitiven Abhängigkeiten der +**`prisma`-CLI** (`deepmerge-ts` über `@prisma/config`, `mysql2`). Beide sind +Entwicklungsabhängigkeiten und erreichen das Laufzeitabbild nicht; MySQL wird +ohnehin nie geladen. Der angebotene Fix wäre ein Rückschritt auf Prisma 6. + +## Sicherheit + +- Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, damit + sich nicht abfragen lässt, wer hier ein Konto hat. +- Sitzungen sind JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage + frisch aus der Datenbank gelesen — eine Sperre wirkt sofort. +- Jede schreibende Aktion prüft die Rolle in der Seite **und** noch einmal in + der Server Action. Die Oberfläche blendet Knöpfe nur zusätzlich aus. +- Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name nach + Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt. +- Keine Geheimnisse, Tokens, Mailinhalte oder Termin-Betreffs in Protokollen. + +Gefundene Schwachstellen bitte nicht als Issue anlegen, sondern direkt melden. + +## Lizenz + +Privates Projekt, keine Lizenz zur Weiterverwendung. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..4a3bbd7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,124 @@ +# Fahrplan + +Acht Meilensteine. Jeder endet mit einer lauffähigen Version auf `main`, +grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG. + +| Meilenstein | Ziel | Status | +| --------------------------------------- | ---------------------------- | --------- | +| [M1](#m1--fundament) | Fundament | in Arbeit | +| [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | geplant | +| [M3](#m3--kalender-lesen) | Kalender lesen | geplant | +| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | +| [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | +| [M6](#m6--integrationen) | Integrationen | geplant | +| [M7](#m7--schnellerfassung-und-api) | Schnellerfassung und API | geplant | +| [M8](#m8--mcp) | MCP | geplant | + +--- + +## M1 – Fundament + +**Ziel:** Ein angemeldeter, betreibbarer Rumpf. Noch keine Aufgaben, kein +Kalender — aber alles, worauf die späteren Meilensteine aufsetzen. + +- [x] Projektgerüst: Next 16, React 19, TypeScript strict, Tailwind 4 +- [x] `npm run pruefen`: Prettier, ESLint, tsc, Vitest, ShellCheck +- [x] Datenmodell für Anmeldung, Passkeys, Bereiche und Audit +- [x] Anmeldung über OIDC, Passkey und wahlweise Passwort +- [x] Rollen `BENUTZER` und `ADMIN`, serverseitig in Seite **und** Server Action +- [x] Audit-Eintrag je schreibender Aktion, Details werden gesäubert +- [x] Instanzsteuerung `INSTANZ=firma|daheim` mit 404 statt Hinweis +- [x] Bereiche verwalten: Farbe, Arbeitsfenster, Tageskapazität +- [x] Protokollseite für die Verwaltung +- [x] `/api/health` mit Status, Version und Instanz +- [x] Dockerfile, Compose-Stapel mit eigenem Migrationsdienst +- [x] `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh` +- [x] Unit-, Integrations-, Migrations- und E2E-Tests +- [x] Screenshot-Kette mit eigener Wegwerf-Instanz +- [x] README, Schnellstart, Handbuch, CHANGELOG, DEVLOG + +**Bewusst verschoben:** `Aufgabe` und `Zeitblock` kommen mit M2 und M4, +`Kalenderkonto` mit M3 — jeweils mit den Feldern, die der Provider dann +wirklich braucht, statt heute geraten. + +## M2 – Aufgaben und Projekte + +**Ziel:** Alles erfassen und wiederfinden, noch ohne Kalender. + +- [ ] `Aufgabe` mit Typ, Status, Priorität, Aufwand, Fälligkeit, Quelle +- [ ] Eindeutigkeit `[quelle, quelleRef]`, Integrationen upserten +- [ ] Inbox: schnell erfassen, später einsortieren +- [ ] Ideen getrennt von Aufgaben +- [ ] Projekte mit Arbeitspaketen und Meilensteinen +- [ ] Listen mit Filtern nach Bereich, Status und Fälligkeit +- [ ] Handbuchseiten und Bilder + +## M3 – Kalender lesen + +**Ziel:** Termine aus Microsoft 365 und Google sehen. + +- [ ] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis +- [ ] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim +- [ ] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft +- [ ] Ganztägige Termine zählen nicht zur Belegung +- [ ] Anonymisierte Beispielantworten in `test/fixtures/` +- [ ] Hintergrundjob mit node-cron + +## M4 – Wochenplanung und Zeitblöcke + +**Ziel:** Aufgaben per Ziehen und Ablegen in den Kalender schreiben — und +Verschiebungen von dort zurückholen. + +- [ ] `Zeitblock` an genau einer Aufgabe und einem Kalenderkonto +- [ ] Wochenansicht mit `@dnd-kit/core` +- [ ] Schreiben in den Kalender, Rückweg beim Verschieben +- [ ] Schleifenschutz über `hash = sha256(startISO|endeISO)` +- [ ] Konflikte: der Kalender gewinnt (HTTP 412 auf If-Match) +- [ ] Belegung gegen die Tageskapazität des Bereichs + +## M5 – Benachrichtigungen + +- [ ] Web-Push mit `web-push` +- [ ] Tagesüberblick per E-Mail über nodemailer +- [ ] Erinnerungen an Fälligkeiten, Jobs über node-cron +- [ ] Je Benutzer einstellbar, mit Ruhezeiten + +## M6 – Integrationen + +- [ ] Zammad und Wartungsplaner-API (nur `firma`) +- [ ] Telegram (nur `daheim`) +- [ ] Spiegel: `firma` sendet, `daheim` empfängt +- [ ] Jede Integration upsertet über `[quelle, quelleRef]` + +## M7 – Schnellerfassung und API + +- [ ] Erfassung vom Handy, auch offline +- [ ] Öffentliche API mit Token je Anwendung +- [ ] Rechte je Token, jede Nutzung im Protokoll + +## M8 – MCP + +- [ ] MCP-Server über `@modelcontextprotocol/sdk` +- [ ] Lesen und Anlegen von Aufgaben und Zeitblöcken +- [ ] Rechte wie bei der API, jede Nutzung im Protokoll + +--- + +## Ideen für später + +- Auswertung: wie viel Zeit ging tatsächlich in welchen Bereich +- Wiederkehrende Aufgaben mit eigenem Rhythmus +- Vorlagen für wiederkehrende Projekte +- Verknüpfung von Aufgaben untereinander (blockiert, gehört zu) +- Export der eigenen Daten als JSON + +## Bewusst nicht geplant + +- **Wiederkehrende Wartungsaufgaben mit Nachweispflicht.** Die bleiben im + Wartungsplaner. Der Planer liest nur Fälligkeiten über dessen API. +- **Mehrbenutzerbetrieb mit geteilten Aufgaben.** Der Planer ist für eine + Person gedacht; Kollegen melden sich in der Firmeninstanz nur an ihren + eigenen Daten an. +- **Eigene Kalenderdarstellung als Ersatz für Outlook.** Der Planer schreibt + in die vorhandenen Kalender, er ersetzt sie nicht. +- **Zeiterfassung zur Abrechnung.** Dafür gibt es andere Werkzeuge. diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..ac20caf --- /dev/null +++ b/backup.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Sichert Datenbank, .env und Uploads des Planers. +# Wiederherstellung: siehe README.md, Abschnitt "Sicherung und Wiederherstellung". +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$WURZEL" + +ZIEL="${WURZEL}/sicherungen" +AUFBEWAHRUNG_TAGE=30 +NUR_DATENBANK=0 + +hilfe() { + cat <<'ENDE' +Verwendung: backup.sh [Optionen] + + --ziel Ablageort der Sicherung (Standard: ./sicherungen) + --tage Aufbewahrung in Tagen (Standard: 30, 0 = unbegrenzt) + --nur-datenbank Nur den Datenbank-Dump erzeugen + --hilfe Diese Übersicht + +Erzeugt je Lauf zwei Dateien: + planer-db-.dump Datenbank im Custom-Format von pg_dump + planer-dateien-.tar.gz .env und der Ordner uploads +ENDE +} + +meldung() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"; } +fehler() { + printf '%s Fehler: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --ziel) + ZIEL="${2:-}" + [ -n "$ZIEL" ] || fehler '--ziel braucht einen Pfad.' + shift 2 + ;; + --tage) + AUFBEWAHRUNG_TAGE="${2:-}" + case "$AUFBEWAHRUNG_TAGE" in + ''|*[!0-9]*) fehler '--tage braucht eine ganze Zahl.' ;; + esac + shift 2 + ;; + --nur-datenbank) + NUR_DATENBANK=1 + shift + ;; + --hilfe|-h) + hilfe + exit 0 + ;; + *) fehler "Unbekannte Option '$1'. 'backup.sh --hilfe' zeigt die Übersicht." ;; + esac +done + +command -v docker >/dev/null 2>&1 || fehler 'docker fehlt.' +[ -f "$WURZEL/.env" ] || fehler '.env fehlt – im Projektverzeichnis aufrufen.' + +# Nur die benötigten Werte lesen, nicht die ganze Datei ausführen. +lies_env() { + local schluessel="$1" standard="${2:-}" + local wert + wert="$(grep -E "^${schluessel}=" "$WURZEL/.env" | tail -1 | cut -d= -f2- || true)" + wert="${wert%\"}" + wert="${wert#\"}" + printf '%s' "${wert:-$standard}" +} + +PG_BENUTZER="$(lies_env POSTGRES_USER planer)" +PG_DATENBANK="$(lies_env POSTGRES_DB planer)" + +DB_CONTAINER="$(docker compose ps -q db)" +[ -n "$DB_CONTAINER" ] || fehler 'Der Datenbank-Container läuft nicht. Erst "docker compose up -d db".' + +ZEITSTEMPEL="$(date '+%Y-%m-%d_%H-%M-%S')" +mkdir -p "$ZIEL" + +DUMP="$ZIEL/planer-db-${ZEITSTEMPEL}.dump" +meldung "Datenbank sichern nach $DUMP" +# -Fc ist das Custom-Format: komprimiert und mit pg_restore selektiv einspielbar. +docker exec "$DB_CONTAINER" pg_dump -U "$PG_BENUTZER" -d "$PG_DATENBANK" -Fc > "$DUMP" + +if [ ! -s "$DUMP" ]; then + rm -f "$DUMP" + fehler 'Der Dump ist leer geblieben.' +fi +meldung "Datenbank gesichert ($(du -h "$DUMP" | cut -f1))." + +if [ "$NUR_DATENBANK" -eq 0 ]; then + DATEIEN="$ZIEL/planer-dateien-${ZEITSTEMPEL}.tar.gz" + meldung "Dateien sichern nach $DATEIEN" + ZU_SICHERN=(.env) + [ -d "$WURZEL/uploads" ] && ZU_SICHERN+=(uploads) + tar -czf "$DATEIEN" -C "$WURZEL" "${ZU_SICHERN[@]}" + meldung "Dateien gesichert ($(du -h "$DATEIEN" | cut -f1))." +fi + +if [ "$AUFBEWAHRUNG_TAGE" -gt 0 ]; then + meldung "Sicherungen älter als $AUFBEWAHRUNG_TAGE Tage entfernen." + find "$ZIEL" -maxdepth 1 -type f -name 'planer-*' -mtime "+$AUFBEWAHRUNG_TAGE" -print -delete +fi + +meldung 'Sicherung abgeschlossen.' diff --git a/devlog/DEVLOG.md b/devlog/DEVLOG.md new file mode 100644 index 0000000..c47687a --- /dev/null +++ b/devlog/DEVLOG.md @@ -0,0 +1,137 @@ +# Entwicklungstagebuch + +Neuester Eintrag oben. Die Dauer je Sitzung stammt aus `devlog/zeiten.csv`, +die Übersicht aus `scripts/zeit.sh summe`. + +## Zeit je Meilenstein + +| Meilenstein | Art | Minuten | Stunden | +| ----------- | ------ | ------- | -------- | +| M1 | claude | 161 | 2,68 | +| **Gesamt** | | **161** | **2,68** | + +--- + +## 2026-09-10 – M1: Fundament + +**Dauer:** 161 Minuten (12:06–14:47), Art `claude`. + +**Ziel der Sitzung:** Das Projekt von Grund auf aufsetzen: Gerüst, +Datenbank, Anmeldung, Rollen, Audit, Instanzsteuerung, Betrieb und +Dokumentation. + +### Erledigt + +- Projektgerüst mit Next 16, React 19, TypeScript strict, Tailwind 4, + alle Versionen exakt gepinnt +- `npm run pruefen` als Sammelprüfung: Prettier, ESLint, `tsc`, Vitest, + ShellCheck +- Datenmodell und erste Migration: Benutzer mit Rolle, OIDC-Konten, + Passkeys, WebAuthn-Herausforderungen, Bereiche, Audit +- Anmeldung über OIDC, Passkey und Passwort, je Instanz zuschaltbar +- Rollenprüfung in Seite **und** Server Action, Audit je Schreibaktion +- Instanzsteuerung mit 404 statt Hinweis +- Bereiche verwalten, Protokollseite, Startseite, Einstellungen +- `/api/health`, Dockerfile, Compose-Stapel mit Migrationsdienst +- `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh` +- Unit-, Integrations-, Migrations- und E2E-Tests +- Screenshot-Kette mit eigener Wegwerf-Instanz +- README, Schnellstart, Handbuch, ROADMAP, CHANGELOG + +### Entscheidungen mit Begründung + +**Prisma 7.10.0 statt der notierten Fassung 6.** Fassung 6 ist überholt, +Fassung 8 liegt nur als RC auf `latest`. Prisma 7 nimmt die Verbindung +nicht mehr im Schema entgegen — sie steht in `prisma.config.ts`, der +Client bekommt den pg-Treiberadapter. + +**TypeScript 6.0.3 statt 7.0.2.** TS 7 ist zwar `latest`, aber +`typescript-eslint@8.70` verlangt `typescript >=4.8.4 <6.1.0`. Mit TS 7 +wäre die Lint-Kette kaputt gewesen. + +**ESLint 9.39.5 statt 10.** Das in `eslint-config-next@16` gebündelte +`eslint-plugin-react` ruft noch `context.getFilename()`, das ESLint 10 +entfernt hat. Der Lint-Lauf brach reproduzierbar ab. + +**Eigener Auth.js-Adapter.** Der mitgelieferte `@auth/prisma-adapter` +greift fest auf `prisma.user` und `prisma.account` zu; unsere Modelle +heißen `Benutzer` und `Konto`. Der Adapter ist schmal und ersparte es, +die Fachbegriffe der Konvention zu opfern. + +**Passkeys direkt über SimpleWebAuthn 14.** Der WebAuthn-Provider von +Auth.js ist als experimentell markiert und auf SimpleWebAuthn 9 +festgelegt. Direkt angebunden bleibt die Fassung aktuell und der Ablauf +nachvollziehbar. Für die npm-Auflösung brauchte es `overrides`, weil +next-auth die älteren Fassungen als optionale Peers deklariert. + +**Sitzungen als JWT, Rolle trotzdem aus der Datenbank.** Credentials- +Provider verlangen JWT. Damit eine Sperre nicht bis zum Ablauf des Tokens +wirkungslos bleibt, liest der `session`-Callback Rolle und Status bei +jeder Anfrage frisch. Die Tabellen `sitzung` und `verifizierungs_token` +wären damit tot und wurden wieder entfernt, solange es nur eine einzige +Migration gab. + +**Migrationen als eigener Compose-Dienst.** Der Versuch, nur die +Prisma-CLI ins schlanke Laufzeitabbild zu kopieren, scheiterte an ihren +transitiven Modulen (`effect` über `@prisma/config`). Alle mitzunehmen +hätte das Abbild aufgebläht. Ein Einmaldienst aus der Bau-Stufe läuft vor +der App und muss sauber durchlaufen. + +**Aufgabe, Zeitblock und Kalenderkonto bewusst nicht modelliert.** Sie +kommen mit M2, M4 und M3 — dann mit den Feldern, die der jeweilige +Provider wirklich braucht, statt heute geraten. + +### Probleme und Lösungen + +| Problem | Lösung | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `prisma validate` lehnte `url` im Schema ab | Prisma 7 erwartet die Verbindung in `prisma.config.ts` und einen Treiberadapter im Client. | +| Integrationstest brach nach der ersten erwarteten Verletzung ab | In PostgreSQL bricht ein fehlgeschlagener Befehl die ganze Transaktion ab. Der Test läuft jetzt ohne Sammeltransaktion; alle Testdaten tragen eine Kennung und werden am Ende entfernt. | +| `prisma migrate diff` kannte `--from-url` nicht | In Prisma 7 heißen die Flaggen anders. Der Test vergleicht jetzt die eingespielte Wegwerf-Datenbank über `--from-config-datasource` gegen das Schema — und braucht damit keine Schattendatenbank. | +| Nach der Anmeldung ging es nicht zum ursprünglichen Ziel zurück | Das Layout leitet um, bevor die Seite drankommt, und kennt seinen Pfad nicht. Ein schlanker Proxy reicht ihn als Kopfzeile durch — ohne Anmeldelogik, weil er im Edge-Umfeld läuft. | +| Der Docker-Bau brach beim Sammeln der Seitendaten ab | Prisma-Client und Auth.js-Konfiguration wurden beim Laden des Moduls ausgewertet und brauchten Umgebungsvariablen, die es zur Bauzeit nicht gibt. Beide entstehen jetzt faul. | +| Ein Typfehler in `scripts/screenshots.ts` blockierte den Abbild-Bau | Entwicklungsskripte gehören nicht in den Docker-Kontext. `.dockerignore` schließt sie aus; geprüft werden sie von `npm run pruefen`. | +| ESLint beanstandete `setState` im Effekt | Der Hydratations-Trick im Farbschema-Knopf war unnötig: `next-themes` setzt die `dark`-Klasse vor der Hydratation, das erledigt CSS ohne Zustand. | + +### Eigene Fehler, die ich korrigiert habe + +- Die ersten vier Commits lagen direkt auf `main` statt auf einem + Meilenstein-Zweig und ohne Attributionszeile. Historie umgebaut: + `main` trägt nur noch einen leeren Wurzel-Commit, die Arbeit liegt auf + `m1-fundament`. +- Ein `git add -A` zog die Proxy-Umbenennung in den Doku-Commit. Wieder + getrennt. +- `scripts/screenshots.ts` wurde geschrieben, ohne danach `npm run +pruefen` laufen zu lassen; der Typfehler fiel erst im Docker-Bau auf. + +### Offene Punkte + +- Die `origin`-URL fehlt noch. Push und Merge stehen ohnehin erst nach + der Freigabe an. +- Der Schnellstart endet beim Passkey statt beim ersten Kalenderkonto und + der ersten Aufgabe — beides gibt es erst mit M3 und M2. +- Der Demo-Seed enthält noch keine erfundenen Termine; einen + Demo-Kalenderprovider gibt es erst mit M3. +- Lokal weichen die Ports ab (Datenbank 5435, App 3001), weil 5432 und + 3000 auf diesem Rechner belegt sind. In `.env.example` stehen die + üblichen Werte. + +### Commits + +- de1f923 chore: Projektgerüst und Werkzeugkette einrichten +- 748d032 feat(datenbank): Datenmodell für Anmeldung, Bereiche und Audit +- e2b00f4 feat(instanz): Instanzsteuerung mit serverseitigem Riegel +- 19ca082 feat(zeit): Zeitrechnung und Anzeigeformate in Europe/Berlin +- 4ff67c1 feat(oberflaeche): Grundlayout, Navigation und Farbschema +- cc013fd feat(anmeldung): OIDC, Passkeys und Passwort mit Auth.js v5 +- e9a2b4b feat(bereiche): Bereiche verwalten mit Rollenprüfung und Audit +- 1876ae4 feat(betrieb): Health-Endpunkt und Seed für eine frische Datenbank +- 24a799d chore: tsconfig-Anpassungen von Next 16 übernehmen +- 126ad93 test: Integrations-, Migrations- und E2E-Tests ergänzen +- f91d015 feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte +- 56b5356 docs: README, Schnellstart, Handbuch, Fahrplan und CHANGELOG +- e3390c8 refactor: middleware in proxy umbenennen +- a882731 chore: Screenshot-Kette und Demo-Seed +- cf28ec1 test: Rollentrennung end-to-end absichern +- 6d04bc2 fix(oberflaeche): Kopfleiste einzeilig halten, Demo-Uhr feststellen +- 688d9f5 docs: Handbuchbilder aufnehmen diff --git a/devlog/zeiten.csv b/devlog/zeiten.csv new file mode 100644 index 0000000..36787b9 --- /dev/null +++ b/devlog/zeiten.csv @@ -0,0 +1,2 @@ +datum;start;ende;minuten;meilenstein;art;notiz +2026-09-10;12:06;14:47;161;M1;claude;M1 Fundament fertiggestellt diff --git a/docker-compose.screenshots.yml b/docker-compose.screenshots.yml new file mode 100644 index 0000000..b92667c --- /dev/null +++ b/docker-compose.screenshots.yml @@ -0,0 +1,61 @@ +# Wegwerf-Instanz nur für "npm run screenshots". +# +# Eigenes Compose-Projekt (planer-screenshots), eigenes Volume, eigene Ports. +# Sie kommt der Entwicklungs- oder Betriebsinstanz damit nicht in die Quere und +# wird nach dem Lauf samt Volume wieder entfernt. +services: + db: + image: postgres:16-alpine + restart: 'no' + environment: + POSTGRES_USER: demo + POSTGRES_PASSWORD: demo + POSTGRES_DB: demo + TZ: Europe/Berlin + ports: + - '${SCREENSHOT_DB_PORT:-5455}:5432' + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U demo -d demo'] + interval: 3s + timeout: 3s + retries: 20 + start_period: 10s + + migration: + build: + context: . + dockerfile: Dockerfile + target: bauen + restart: 'no' + depends_on: + db: + condition: service_healthy + environment: + TZ: Europe/Berlin + DATABASE_URL: postgresql://demo:demo@db:5432/demo?schema=public + command: node node_modules/prisma/build/index.js migrate deploy + + app: + build: + context: . + dockerfile: Dockerfile + target: betrieb + restart: 'no' + depends_on: + migration: + condition: service_completed_successfully + environment: + NODE_ENV: production + TZ: Europe/Berlin + INSTANZ: daheim + APP_URL: http://127.0.0.1:${SCREENSHOT_APP_PORT:-3200} + DATABASE_URL: postgresql://demo:demo@db:5432/demo?schema=public + AUTH_SECRET: nur-fuer-die-screenshot-instanz-nicht-geheim + AUTH_TRUST_HOST: 'true' + ANMELDUNG_PASSWORT: 'true' + WEBAUTHN_RP_ID: 127.0.0.1 + WEBAUTHN_RP_NAME: Planer + WEBAUTHN_ORIGIN: http://127.0.0.1:${SCREENSHOT_APP_PORT:-3200} + DEMO_MODUS: 'true' + ports: + - '${SCREENSHOT_APP_PORT:-3200}:3000' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..765b507 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,75 @@ +services: + db: + image: postgres:16-alpine + container_name: planer-db + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-planer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env} + POSTGRES_DB: ${POSTGRES_DB:-planer} + TZ: ${TZ:-Europe/Berlin} + volumes: + - db-daten:/var/lib/postgresql/data + ports: + - '${POSTGRES_PORT:-5432}:5432' + healthcheck: + test: + ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-planer} -d ${POSTGRES_DB:-planer}'] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + + # Einmaldienst: spielt die Migrationen ein und beendet sich. Läuft aus der + # Bau-Stufe, weil die Prisma-CLI eine Reihe Module braucht, die im schlanken + # Laufzeitabbild nichts zu suchen haben. + migration: + build: + context: . + dockerfile: Dockerfile + target: bauen + container_name: planer-migration + restart: 'no' + depends_on: + db: + condition: service_healthy + environment: + TZ: ${TZ:-Europe/Berlin} + DATABASE_URL: postgresql://${POSTGRES_USER:-planer}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-planer}?schema=public + command: node node_modules/prisma/build/index.js migrate deploy + + app: + build: + context: . + dockerfile: Dockerfile + target: betrieb + container_name: planer-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + migration: + condition: service_completed_successfully + environment: + NODE_ENV: production + TZ: ${TZ:-Europe/Berlin} + INSTANZ: ${INSTANZ:?INSTANZ fehlt in .env} + APP_URL: ${APP_URL:?APP_URL fehlt in .env} + DATABASE_URL: postgresql://${POSTGRES_USER:-planer}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-planer}?schema=public + AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in .env} + AUTH_TRUST_HOST: ${AUTH_TRUST_HOST:-true} + ANMELDUNG_PASSWORT: ${ANMELDUNG_PASSWORT:-false} + OIDC_NAME: ${OIDC_NAME:-} + OIDC_ISSUER: ${OIDC_ISSUER:-} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} + WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:?WEBAUTHN_RP_ID fehlt in .env} + WEBAUTHN_RP_NAME: ${WEBAUTHN_RP_NAME:-Planer} + WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:?WEBAUTHN_ORIGIN fehlt in .env} + # Der Demo-Modus ist im Betrieb nie erlaubt. + DEMO_MODUS: 'false' + ports: + - '${APP_PORT:-3000}:3000' + +volumes: + db-daten: diff --git a/docs/SCHNELLSTART.md b/docs/SCHNELLSTART.md new file mode 100644 index 0000000..4651e79 --- /dev/null +++ b/docs/SCHNELLSTART.md @@ -0,0 +1,155 @@ +# Schnellstart + +Von `git clone` bis zur ersten Anmeldung in höchstens zehn Minuten. +Jeder Befehl ist zum Kopieren, jeder Schritt nennt das erwartete Ergebnis. + +## Voraussetzungen + +| Werkzeug | Fassung | Prüfen mit | +| ------------------ | ---------------- | ------------------------ | +| Docker mit Compose | 25 oder neuer | `docker compose version` | +| Git | 2.40 oder neuer | `git --version` | +| Node.js | 22.12 oder neuer | `node -v` | + +Node wird nur für die Entwicklung gebraucht. Für den reinen Betrieb genügen +Docker und Git. + +## 1. Holen und Verzeichnis wechseln + +```bash +git clone planer +cd planer +``` + +**Erwartet:** Das Verzeichnis `planer` enthält unter anderem `package.json`, +`docker-compose.yml` und `.env.example`. + +## 2. Konfiguration anlegen + +```bash +cp .env.example .env +``` + +Jetzt die Pflichtwerte setzen. Die Geheimnisse werden erzeugt, nicht erfunden: + +```bash +# Signiert die Sitzungen. Ohne diesen Wert startet die Anwendung nicht. +openssl rand -base64 32 + +# Ein Passwort für die Datenbank. +openssl rand -hex 16 +``` + +Unter Windows ohne OpenSSL geht auch: + +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" +``` + +Diese Werte in `.env` eintragen: + +| Variable | Beispiel | Bedeutung | +| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | +| `INSTANZ` | `daheim` | `firma` oder `daheim` – bestimmt, was es überhaupt gibt | +| `APP_URL` | `http://localhost:3000` | Adresse ohne Schrägstrich am Ende | +| `AUTH_SECRET` | _(erzeugt)_ | Signatur der Sitzungen | +| `POSTGRES_PASSWORD` | _(erzeugt)_ | Passwort der Datenbank | +| `DATABASE_URL` | `postgresql://planer:@localhost:5432/planer?schema=public` | muss dasselbe Passwort enthalten | +| `WEBAUTHN_RP_ID` | `localhost` | nackte Domain, ohne Schema und Port | +| `WEBAUTHN_ORIGIN` | `http://localhost:3000` | exakt wie im Browser, mit Port | +| `ADMIN_EMAIL` | `du@example.de` | erstes Verwaltungskonto | +| `ADMIN_PASSWORT` | _(mindestens 12 Zeichen)_ | nur nötig, wenn du dich ohne OIDC anmelden willst | + +> **Sind die Ports 5432 oder 3000 auf deinem Rechner belegt?** +> Dann `POSTGRES_PORT` beziehungsweise `APP_PORT` in `.env` auf einen freien +> Wert setzen. `DATABASE_URL`, `APP_URL` und `WEBAUTHN_ORIGIN` müssen denselben +> Port nennen. + +**Erwartet:** `.env` liegt im Projektverzeichnis. Sie gehört niemals ins Git — +`.gitignore` sorgt dafür. + +## 3. Starten + +```bash +docker compose up -d --build +``` + +Beim ersten Mal dauert der Bau einige Minuten. Der Ablauf ist: Datenbank +starten, Migrationen einspielen, Anwendung starten. + +**Erwartet:** + +```bash +docker compose ps +``` + +zeigt `db` als `healthy` und `app` als `Up`. Der Dienst `migration` taucht +nicht mehr auf — er hat seine Arbeit getan und sich beendet. + +## 4. Prüfen, ob alles läuft + +```bash +curl http://localhost:3000/api/health +``` + +**Erwartet:** + +```json +{ "status": "ok", "version": "0.1.0", "instanz": "daheim", "datenbank": "ok" } +``` + +Steht dort `"datenbank": "fehler"`, stimmt `DATABASE_URL` nicht mit +`POSTGRES_PASSWORD` überein. + +## 5. Erste Bereiche und das Verwaltungskonto anlegen + +```bash +docker compose run --rm migration node node_modules/prisma/build/index.js db seed +``` + +**Erwartet:** Die Ausgabe nennt die vier angelegten Bereiche (Firma, Gewerbe, +Privat, Homelab) und bestätigt das Verwaltungskonto. + +## 6. Anmelden + +Im Browser öffnen. + +**Erwartet:** Die Anmeldeseite. Mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` +anmelden — danach steht oben „Guten Tag" und darunter die vier Bereiche. + +> Ist OIDC eingerichtet, erscheint zusätzlich ein Knopf mit dem Namen aus +> `OIDC_NAME`. Der Rückleit-URI, den du beim Anbieter hinterlegen musst, +> lautet `/api/auth/callback/oidc`. + +## 7. Einen Passkey hinterlegen + +Auf **Einstellungen** gehen, unter _Passkeys_ einen Namen für das Gerät +eintragen (etwa „Arbeitsrechner") und auf **Anlegen** klicken. Das Gerät +fragt nach Fingerabdruck, Gesicht oder PIN. + +**Erwartet:** Der Passkey steht in der Liste. Ab jetzt genügt zum Anmelden die +E-Mail-Adresse und **Mit Passkey anmelden**. + +> Hinterlege am besten zwei Passkeys auf verschiedenen Geräten. Sonst sperrst +> du dich aus, wenn ein Gerät verlorengeht. + +## 8. Aufräumen + +Wenn `ADMIN_PASSWORT` nur zur Einrichtung gedient hat, kannst du es jetzt +zusammen mit `ADMIN_EMAIL` aus `.env` entfernen. Das Konto bleibt bestehen. + +## Wie es weitergeht + +- [Handbuch](handbuch/) – Anleitung für den Alltag +- [README](../README.md) – alle Umgebungsvariablen, Betrieb, Sicherung +- [ROADMAP](../ROADMAP.md) – was in den nächsten Meilensteinen kommt + +## Wenn etwas klemmt + +| Symptom | Ursache und Abhilfe | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `INSTANZ fehlt in .env` beim Start | `INSTANZ` ist leer. Erlaubt sind nur `firma` und `daheim`. | +| `Die Konfiguration ist unvollständig` | Die Meldung nennt die fehlende Variable. Sie nennt nie den Wert. | +| `port is already allocated` | Ein anderer Dienst hält den Port. `APP_PORT` oder `POSTGRES_PORT` ändern. | +| Passkey lässt sich nicht anlegen | `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. `WEBAUTHN_RP_ID` ist die nackte Domain, ohne `https://`. | +| Anmeldung schlägt immer fehl | Bewusst gibt es nur eine Meldung für alle Fälle. Prüfe im Protokoll der Anwendung: `docker compose logs app`. | diff --git a/docs/bilder/anmelden-dunkel-handy.png b/docs/bilder/anmelden-dunkel-handy.png new file mode 100644 index 0000000..1876ce5 Binary files /dev/null and b/docs/bilder/anmelden-dunkel-handy.png differ diff --git a/docs/bilder/anmelden-dunkel-schreibtisch.png b/docs/bilder/anmelden-dunkel-schreibtisch.png new file mode 100644 index 0000000..0e9f143 Binary files /dev/null and b/docs/bilder/anmelden-dunkel-schreibtisch.png differ diff --git a/docs/bilder/anmelden-hell-handy.png b/docs/bilder/anmelden-hell-handy.png new file mode 100644 index 0000000..4c427ea Binary files /dev/null and b/docs/bilder/anmelden-hell-handy.png differ diff --git a/docs/bilder/anmelden-hell-schreibtisch.png b/docs/bilder/anmelden-hell-schreibtisch.png new file mode 100644 index 0000000..72ca80d Binary files /dev/null and b/docs/bilder/anmelden-hell-schreibtisch.png differ diff --git a/docs/bilder/bereiche-dunkel-handy.png b/docs/bilder/bereiche-dunkel-handy.png new file mode 100644 index 0000000..5421b41 Binary files /dev/null and b/docs/bilder/bereiche-dunkel-handy.png differ diff --git a/docs/bilder/bereiche-dunkel-schreibtisch.png b/docs/bilder/bereiche-dunkel-schreibtisch.png new file mode 100644 index 0000000..85cd9f9 Binary files /dev/null and b/docs/bilder/bereiche-dunkel-schreibtisch.png differ diff --git a/docs/bilder/bereiche-hell-handy.png b/docs/bilder/bereiche-hell-handy.png new file mode 100644 index 0000000..5cb6c66 Binary files /dev/null and b/docs/bilder/bereiche-hell-handy.png differ diff --git a/docs/bilder/bereiche-hell-schreibtisch.png b/docs/bilder/bereiche-hell-schreibtisch.png new file mode 100644 index 0000000..56ab9be Binary files /dev/null and b/docs/bilder/bereiche-hell-schreibtisch.png differ diff --git a/docs/bilder/einstellungen-dunkel-handy.png b/docs/bilder/einstellungen-dunkel-handy.png new file mode 100644 index 0000000..46bdac1 Binary files /dev/null and b/docs/bilder/einstellungen-dunkel-handy.png differ diff --git a/docs/bilder/einstellungen-dunkel-schreibtisch.png b/docs/bilder/einstellungen-dunkel-schreibtisch.png new file mode 100644 index 0000000..c1491a5 Binary files /dev/null and b/docs/bilder/einstellungen-dunkel-schreibtisch.png differ diff --git a/docs/bilder/einstellungen-hell-handy.png b/docs/bilder/einstellungen-hell-handy.png new file mode 100644 index 0000000..e3792e8 Binary files /dev/null and b/docs/bilder/einstellungen-hell-handy.png differ diff --git a/docs/bilder/einstellungen-hell-schreibtisch.png b/docs/bilder/einstellungen-hell-schreibtisch.png new file mode 100644 index 0000000..8418a1b Binary files /dev/null and b/docs/bilder/einstellungen-hell-schreibtisch.png differ diff --git a/docs/bilder/protokoll-dunkel-handy.png b/docs/bilder/protokoll-dunkel-handy.png new file mode 100644 index 0000000..79420c3 Binary files /dev/null and b/docs/bilder/protokoll-dunkel-handy.png differ diff --git a/docs/bilder/protokoll-dunkel-schreibtisch.png b/docs/bilder/protokoll-dunkel-schreibtisch.png new file mode 100644 index 0000000..172e8ad Binary files /dev/null and b/docs/bilder/protokoll-dunkel-schreibtisch.png differ diff --git a/docs/bilder/protokoll-hell-handy.png b/docs/bilder/protokoll-hell-handy.png new file mode 100644 index 0000000..e7239bc Binary files /dev/null and b/docs/bilder/protokoll-hell-handy.png differ diff --git a/docs/bilder/protokoll-hell-schreibtisch.png b/docs/bilder/protokoll-hell-schreibtisch.png new file mode 100644 index 0000000..398878d Binary files /dev/null and b/docs/bilder/protokoll-hell-schreibtisch.png differ diff --git a/docs/bilder/start-dunkel-handy.png b/docs/bilder/start-dunkel-handy.png new file mode 100644 index 0000000..6574fb9 Binary files /dev/null and b/docs/bilder/start-dunkel-handy.png differ diff --git a/docs/bilder/start-dunkel-schreibtisch.png b/docs/bilder/start-dunkel-schreibtisch.png new file mode 100644 index 0000000..5f15998 Binary files /dev/null and b/docs/bilder/start-dunkel-schreibtisch.png differ diff --git a/docs/bilder/start-hell-handy.png b/docs/bilder/start-hell-handy.png new file mode 100644 index 0000000..1dfc082 Binary files /dev/null and b/docs/bilder/start-hell-handy.png differ diff --git a/docs/bilder/start-hell-schreibtisch.png b/docs/bilder/start-hell-schreibtisch.png new file mode 100644 index 0000000..6e767ea Binary files /dev/null and b/docs/bilder/start-hell-schreibtisch.png differ diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md new file mode 100644 index 0000000..cdbc4f8 --- /dev/null +++ b/docs/handbuch/README.md @@ -0,0 +1,36 @@ +# Handbuch + +Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. + +## Erste Schritte + +1. **[Anmelden](anmelden.md)** – die drei Wege hinein, Passkeys einrichten, + was tun, wenn nichts mehr geht +2. **[Bereiche einrichten](bereiche.md)** – Farbe, Arbeitsfenster und + Tageskapazität festlegen + +## Verwaltung + +3. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat + +## Was noch kommt + +Diese Seiten entstehen mit den nächsten Meilensteinen: + +| Thema | Meilenstein | +| ------------------------------- | ----------- | +| Aufgaben erfassen, Inbox, Ideen | M2 | +| Projekte und Arbeitspakete | M2 | +| Kalenderkonten verbinden | M3 | +| Wochenplanung und Zeitblöcke | M4 | +| Benachrichtigungen | M5 | +| Integrationen | M6 | +| Schnellerfassung vom Handy, API | M7 | +| MCP | M8 | + +Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt. + +--- + +Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md). +Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). diff --git a/docs/handbuch/anmelden.md b/docs/handbuch/anmelden.md new file mode 100644 index 0000000..acef851 --- /dev/null +++ b/docs/handbuch/anmelden.md @@ -0,0 +1,83 @@ +# Anmelden + +Es gibt drei Wege in den Planer. Welche davon zu sehen sind, hängt davon ab, +was für deine Instanz eingerichtet wurde. + +![Die Anmeldeseite](../bilder/anmelden-hell-schreibtisch.png) + +| Weg | Wann sinnvoll | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | +| **Unternehmensanmeldung** (OIDC) | In der Firma der Normalfall. Ein Klick, die Anmeldung übernimmt Entra ID beziehungsweise authentik. | +| **Passkey** | Der bequemste Weg am eigenen Gerät: Fingerabdruck, Gesicht oder Geräte-PIN, kein Passwort. | +| **E-Mail und Passwort** | Der Rückfallweg. Oft nur daheim freigeschaltet. | + +## Mit Passkey anmelden + +1. E-Mail-Adresse eintragen. +2. Auf **Mit Passkey anmelden** klicken. +3. Das Gerät fragt nach Fingerabdruck, Gesicht oder PIN. + +Fertig — du landest auf der Startseite. + +> **Warum erst die E-Mail-Adresse?** Damit der Planer weiß, welche Passkeys er +> anbieten soll. Er antwortet dabei für unbekannte Adressen genauso wie für +> bekannte, damit sich von außen nicht abfragen lässt, wer hier ein Konto hat. + +## Einen Passkey einrichten + +Das geht erst, wenn du angemeldet bist — also beim ersten Mal über OIDC oder +über E-Mail und Passwort. + +1. Oben rechts auf **Einstellungen**. +2. Im Abschnitt _Passkeys_ einen Namen für das Gerät eintragen, etwa + „Arbeitsrechner" oder „Diensthandy". +3. Auf **Anlegen** klicken und die Abfrage des Geräts bestätigen. + +![Passkeys in den Einstellungen](../bilder/einstellungen-hell-schreibtisch.png) + +Der Passkey steht danach in der Liste, mit Anlegedatum und letzter Benutzung. + +> **Lege zwei Passkeys an, auf zwei verschiedenen Geräten.** Sonst sperrst du +> dich aus, sobald ein Gerät verlorengeht oder zurückgesetzt wird. + +Unter jedem Eintrag steht, ob der Passkey **geräteübergreifend** ist — dann +liegt er im Schlüsselbund von Apple, Google oder deinem Passwortmanager und ist +auf deinen anderen Geräten ebenfalls verfügbar. Steht dort **nur dieses +Gerät**, ist er an genau dieses Gerät gebunden. + +## Einen Passkey entfernen + +In den Einstellungen auf das Papierkorb-Symbol neben dem Eintrag. Der Passkey +ist sofort ungültig; auf dem Gerät selbst bleibt ein Rest zurück, den du dort +im Schlüsselbund löschen kannst. + +## Abmelden + +Oben rechts auf das Symbol mit dem Pfeil aus der Tür. + +## Wenn es klemmt + +| Was du siehst | Was dahintersteckt | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| „Anmeldung nicht möglich. Bitte Angaben prüfen." | Diese eine Meldung gilt für alle Fälle: falsches Passwort, unbekannte Adresse, gesperrtes Konto. Das ist Absicht — sonst ließe sich abfragen, wer hier ein Konto hat. | +| Es passiert nichts nach dem Klick auf **Mit Passkey anmelden** | Du hast den Systemdialog abgebrochen. Einfach noch einmal klicken. | +| „Dieses Gerät ist hier schon hinterlegt." | Für dieses Gerät gibt es bereits einen Passkey. Ein zweiter bringt nichts. | +| Der Passkey-Knopf tut gar nichts | Der Browser unterstützt WebAuthn nicht, oder die Seite läuft nicht über HTTPS. Passkeys brauchen eine sichere Verbindung — `localhost` ausgenommen. | +| Passkey lässt sich nicht anlegen | Meist stimmt die Konfiguration nicht: `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. Siehe [README](../../README.md). | + +## Ausgesperrt? + +Wenn kein Weg mehr hineinführt, hilft nur jemand mit Zugriff auf den Server. +Dort lässt sich mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` in der `.env` und einem +erneuten Seed-Lauf wieder ein Passwort setzen — siehe +[Schnellstart, Schritt 5](../SCHNELLSTART.md). + +## Rollen + +Unten in der Fußzeile und in den Einstellungen steht, welche Rolle du hast. + +- **Benutzer** – sieht und plant die eigenen Sachen. +- **Verwaltung** – darf zusätzlich Bereiche ändern und das + [Protokoll](protokoll.md) einsehen. + +Die Rolle vergibt die Verwaltung; sie lässt sich nicht selbst hochstufen. diff --git a/docs/handbuch/bereiche.md b/docs/handbuch/bereiche.md new file mode 100644 index 0000000..136af55 --- /dev/null +++ b/docs/handbuch/bereiche.md @@ -0,0 +1,80 @@ +# Bereiche einrichten + +Ein **Bereich** ist ein Lebensbereich: Firma, Gewerbe, Privat, Homelab. Jede +Aufgabe und jeder Zeitblock gehört später zu genau einem davon. + +Der Bereich bestimmt drei Dinge: + +- **die Farbe**, an der du im Kalender sofort erkennst, worum es geht, +- **das Arbeitsfenster** — die Tageszeit, in der für diesen Bereich überhaupt + geplant wird, +- **die Tageskapazität** — wie viele Minuten je Wochentag höchstens + hineingehen. + +![Die Bereichsseite](../bilder/bereiche-hell-schreibtisch.png) + +> Bereiche ändern darf nur die **Verwaltung**. Als Benutzer siehst du sie, +> aber ohne die Formulare. + +## Einen Bereich anlegen + +1. Im Menü auf **Bereiche**. +2. Ganz unten unter _Neuen Bereich anlegen_ die Felder ausfüllen. +3. Auf **Bereich anlegen** klicken. + +| Feld | Bedeutung | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | +| **Name** | Zwei bis 40 Zeichen, muss eindeutig sein | +| **Farbe** | Wird zum Farbpunkt in Listen und später im Kalender | +| **Arbeitsfenster von / bis** | Zwischen diesen Uhrzeiten wird geplant. Muss nach dem Beginn enden | +| **Tageskapazität** | Minuten je Wochentag. 0 heißt: an diesem Tag gar nicht | +| **In Benutzung** | Abgehakt lassen. Ohne Haken bleibt der Bereich erhalten, taucht aber nicht mehr zur Auswahl auf | +| **Reihenfolge** | Kleinere Zahlen stehen weiter oben | + +## Was eine sinnvolle Kapazität ist + +Nicht die Zeit, die der Tag hergibt — die Zeit, die **wirklich für geplante +Arbeit** übrig bleibt, nach Besprechungen, Störungen und Pausen. + +Ein brauchbarer Anfang: + +| Bereich | Montag bis Donnerstag | Freitag | Wochenende | +| ------- | --------------------- | ------- | --------------- | +| Firma | 360 min (6 h) | 240 min | 0 | +| Gewerbe | 90 min | 60 min | Samstag 180 min | +| Privat | 60 min | 120 min | je 240 min | +| Homelab | 0 bis 60 min | 0 | Samstag 120 min | + +Wenn am Ende der Woche regelmäßig etwas liegenbleibt, ist die Kapazität zu +hoch angesetzt, nicht der Tag zu kurz. + +> Die Tageskapazität darf nie länger sein als das Arbeitsfenster. Ein Fenster +> von 08:00 bis 17:00 fasst höchstens 540 Minuten; trägst du mehr ein, weist +> das Formular es ab. + +## Einen Bereich ändern + +Auf **Bearbeiten** unter dem Bereich klicken, ändern, **Speichern**. Mit +**Schließen** verwirfst du, was du noch nicht gespeichert hast. + +## Einen Bereich loswerden + +Zwei Wege, und meistens ist der erste der richtige: + +1. **Stilllegen** – den Haken bei _In Benutzung_ entfernen und speichern. Alles + Bisherige bleibt erhalten, der Bereich taucht nur nicht mehr zur Auswahl auf. +2. **Löschen** – der rote Knopf. Endgültig. + +> Ab M2 hängen Aufgaben an Bereichen. Dann ist Stilllegen fast immer die +> richtige Wahl, weil beim Löschen auch die Zuordnung verlorenginge. + +Jede Änderung landet im [Protokoll](protokoll.md). + +## Typische Fehler + +| Meldung | Was zu tun ist | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| „Diesen Namen gibt es schon." | Namen sind eindeutig. Vielleicht existiert der Bereich stillgelegt schon. | +| „Das Arbeitsfenster muss nach seinem Beginn enden." | Von-Zeit liegt hinter der Bis-Zeit. Über Mitternacht hinweg geht nicht — dafür lieber zwei Bereiche. | +| „Keine Tageskapazität darf länger sein als das Arbeitsfenster." | Entweder die Kapazität senken oder das Fenster weiten. | +| „Dazu fehlt dir die Berechtigung." | Du bist als Benutzer angemeldet, nicht als Verwaltung. | diff --git a/docs/handbuch/protokoll.md b/docs/handbuch/protokoll.md new file mode 100644 index 0000000..65be422 --- /dev/null +++ b/docs/handbuch/protokoll.md @@ -0,0 +1,57 @@ +# Protokoll + +Jede schreibende Aktion im Planer hinterlässt einen Eintrag: wer, wann, was, +und woran. Einträge lassen sich weder ändern noch löschen. + +Die Seite steht nur der **Verwaltung** offen. Als Benutzer siehst du den +Menüpunkt gar nicht erst; wer die Adresse direkt aufruft, landet auf einem +Hinweis. + +![Die Protokollseite](../bilder/protokoll-hell-schreibtisch.png) + +## Was dort steht + +| Spalte | Bedeutung | +| ------------- | --------------------------------------------------------------------- | +| **Zeitpunkt** | Datum und Uhrzeit in Europe/Berlin | +| **Wer** | Die E-Mail-Adresse zum Zeitpunkt der Aktion | +| **Was** | Die Aktion, in der Form `objekt.verb` | +| **Objekt** | Die Art des betroffenen Objekts, dahinter die fachlichen Einzelheiten | + +Angezeigt werden die letzten 100 Einträge, die neuesten zuerst. + +## Die Aktionen + +| Aktion | Bedeutung | +| ------------------- | --------------------------------------------------- | +| `bereich.angelegt` | Ein neuer Bereich ist entstanden | +| `bereich.geaendert` | Name, Farbe, Fenster oder Kapazität wurden geändert | +| `bereich.geloescht` | Ein Bereich wurde entfernt | +| `passkey.angelegt` | Jemand hat ein Gerät hinterlegt | +| `passkey.entfernt` | Ein Gerät wurde entfernt | + +Mit jedem Meilenstein kommen Aktionen hinzu. + +## Was dort bewusst **nicht** steht + +- Keine Passwörter, Hashes oder Tokens. +- Keine Termin-Betreffs und keine Mailinhalte. +- Keine Zugangsdaten der Kalenderkonten. + +Das ist doppelt abgesichert: Solche Felder werden gar nicht erst übergeben, +und vor dem Speichern ersetzt der Planer zusätzlich jedes Feld, dessen Name +nach einem Geheimnis aussieht, durch `[entfernt]`. Wenn du dort `[entfernt]` +liest, hat dieser zweite Riegel gegriffen. + +## Warum die E-Mail-Adresse und nicht nur die Kennung + +Wird ein Benutzer gelöscht, verschwinden seine Passkeys und verknüpften +Konten — der Protokolleintrag bleibt. Die Verknüpfung zum Konto wird gelöst, +die Adresse bleibt als Nachweis stehen. Sonst wäre das Protokoll nach einer +Löschung wertlos. + +## Aufbewahrung + +Einträge werden nicht automatisch entfernt. Die Tabelle wächst langsam — ein +Eintrag je Änderung, nicht je Seitenaufruf. Sollte sie einmal stören, gehört +das Aufräumen in die Sicherungsroutine und nicht in die Anwendung. diff --git a/e2e/anmeldung.spec.ts b/e2e/anmeldung.spec.ts new file mode 100644 index 0000000..7003e8f --- /dev/null +++ b/e2e/anmeldung.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test' + +/** + * Rauchtest der Anmeldung. + * + * Prüft den Weg, der in M1 wirklich trägt: Wer nicht angemeldet ist, kommt + * nirgends hin; mit richtigem Passwort schon; und die Verwaltungsseite bleibt + * verschlossen, wenn die Rolle nicht reicht. + * + * Voraussetzung: "npm run db:seed" ist gelaufen und ADMIN_EMAIL sowie + * ADMIN_PASSWORT sind gesetzt. + */ + +const EMAIL = process.env.ADMIN_EMAIL ?? '' +const PASSWORT = process.env.ADMIN_PASSWORT ?? '' + +test.describe('Anmeldung', () => { + test.skip( + !EMAIL || !PASSWORT, + 'ADMIN_EMAIL und ADMIN_PASSWORT sind nicht gesetzt – ohne sie gibt es kein Testkonto.', + ) + + test('leitet nicht Angemeldete zur Anmeldeseite', async ({ page }) => { + await page.goto('/') + await expect(page).toHaveURL(/\/anmelden/) + await expect(page.getByRole('heading', { name: 'Planer' })).toBeVisible() + }) + + test('merkt sich das Ziel und kehrt nach der Anmeldung dorthin zurück', async ({ + page, + }) => { + await page.goto('/bereiche') + await expect(page).toHaveURL(/\/anmelden\?weiter=%2Fbereiche/) + }) + + test('weist falsche Zugangsdaten ab, ohne zu verraten warum', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill('ganz-sicher-falsch-123') + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + + await expect(page.getByText('Anmeldung nicht möglich')).toBeVisible() + await expect(page).toHaveURL(/\/anmelden/) + }) + + test('lässt mit richtigem Passwort hinein und wieder hinaus', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill(PASSWORT) + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + + await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + await expect(page.getByRole('link', { name: 'Bereiche' })).toBeVisible() + + await page.getByTitle('Abmelden').click() + await expect(page).toHaveURL(/\/anmelden/) + }) + + test('zeigt der Verwaltung das Protokoll', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill(PASSWORT) + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + + await page.getByRole('link', { name: 'Protokoll' }).click() + await expect(page.getByRole('heading', { name: 'Protokoll' })).toBeVisible() + }) +}) + +test('der Health-Endpunkt antwortet ohne Anmeldung', async ({ request }) => { + const antwort = await request.get('/api/health') + expect(antwort.ok()).toBe(true) + + const inhalt = (await antwort.json()) as Record + expect(inhalt.status).toBe('ok') + expect(inhalt.datenbank).toBe('ok') + expect(typeof inhalt.version).toBe('string') +}) diff --git a/e2e/rollen.spec.ts b/e2e/rollen.spec.ts new file mode 100644 index 0000000..db89306 --- /dev/null +++ b/e2e/rollen.spec.ts @@ -0,0 +1,74 @@ +import { PrismaPg } from '@prisma/adapter-pg' +import { expect, test, type Page } from '@playwright/test' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +/** + * Prüft, dass die Rollentrennung serverseitig hält. + * + * Die Oberfläche blendet Knöpfe aus — darauf allein darf man sich nicht + * verlassen. Deshalb ruft dieser Test die Verwaltungsseite direkt auf, statt + * nach einem Menüpunkt zu suchen. + */ + +const EMAIL = 'zzz-e2e-benutzer@example.invalid' +const PASSWORT = 'nur-fuer-den-test-1234' + +const verbindung = process.env.DATABASE_URL ?? '' +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +test.describe('Rollentrennung', () => { + test.skip(!verbindung, 'DATABASE_URL ist nicht gesetzt.') + + test.beforeAll(async () => { + await db.benutzer.deleteMany({ where: { email: EMAIL } }) + await db.benutzer.create({ + data: { + email: EMAIL, + name: 'Testbenutzer ohne Rechte', + rolle: 'BENUTZER', + passwortHash: await bcrypt.hash(PASSWORT, 10), + }, + }) + }) + + test.afterAll(async () => { + await db.auditEintrag.deleteMany({ where: { benutzerEmail: EMAIL } }) + await db.benutzer.deleteMany({ where: { email: EMAIL } }) + await db.$disconnect() + }) + + async function anmelden(seite: Page): Promise { + await seite.goto('/anmelden') + await seite.getByLabel('E-Mail-Adresse').fill(EMAIL) + await seite.getByLabel('Passwort').fill(PASSWORT) + await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await expect(seite.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + } + + test('zeigt einem Benutzer den Menüpunkt Protokoll gar nicht', async ({ page }) => { + await anmelden(page) + await expect(page.getByRole('link', { name: 'Protokoll' })).toHaveCount(0) + }) + + test('lässt einen Benutzer auch beim direkten Aufruf nicht ins Protokoll', async ({ + page, + }) => { + await anmelden(page) + await page.goto('/verwaltung/protokoll') + + await expect(page).toHaveURL(/\/keine-berechtigung/) + await expect( + page.getByRole('heading', { name: /Dazu fehlt dir die Berechtigung/ }), + ).toBeVisible() + }) + + test('zeigt einem Benutzer die Bereiche, aber ohne Formulare', async ({ page }) => { + await anmelden(page) + await page.goto('/bereiche') + + await expect(page.getByRole('heading', { name: 'Bereiche' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Bearbeiten' })).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Bereich anlegen' })).toHaveCount(0) + }) +}) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..09e0de2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,57 @@ +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import naechstesWebVitals from 'eslint-config-next/core-web-vitals' +import tseslint from 'typescript-eslint' + +const wurzel = dirname(fileURLToPath(import.meta.url)) + +export default tseslint.config( + { + ignores: [ + '.next/**', + 'node_modules/**', + 'coverage/**', + 'playwright-report/**', + 'test-results/**', + // Vom Generator erzeugt, nicht von Hand gepflegt. + 'src/generated/**', + 'next-env.d.ts', + ], + }, + ...naechstesWebVitals, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: wurzel, + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/require-await': 'error', + // Keine Geheimnisse in Logs: console nur fuer Warnungen und Fehler. + 'no-console': ['error', { allow: ['warn', 'error'] }], + eqeqeq: ['error', 'always', { null: 'ignore' }], + }, + }, + // Skripte, Seed und Tests duerfen auf die Konsole schreiben. + { + files: ['scripts/**', 'prisma/**', '**/*.test.ts', '**/*.test.tsx', 'e2e/**'], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['**/*.js', '**/*.mjs', '**/*.cjs'], + extends: [tseslint.configs.disableTypeChecked], + }, +) diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..245a685 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from 'next' + +const config: NextConfig = { + output: 'standalone', + reactStrictMode: true, + poweredByHeader: false, + typedRoutes: true, + experimental: { + serverActions: { + bodySizeLimit: '2mb', + }, + }, +} + +export default config diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..45efbc7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12607 @@ +{ + "name": "planer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "planer", + "version": "0.1.0", + "dependencies": { + "@auth/prisma-adapter": "2.11.3", + "@date-fns/tz": "1.5.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slot": "1.3.3", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "bcryptjs": "3.0.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.4.0", + "lucide-react": "1.44.0", + "next": "16.3.4", + "next-auth": "5.0.0-beta.32", + "next-themes": "0.4.6", + "node-cron": "4.6.0", + "nodemailer": "10.0.3", + "pg": "8.23.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "server-only": "0.0.1", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "zod": "4.6.1" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "4.3.3", + "@testing-library/react": "16.3.3", + "@types/node": "24.13.4", + "@types/nodemailer": "8.0.1", + "@types/pg": "8.23.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "@vitest/coverage-v8": "5.0.0", + "dotenv": "17.4.2", + "eslint": "9.39.5", + "eslint-config-next": "16.3.4", + "jsdom": "30.0.1", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", + "prisma": "7.10.0", + "tailwindcss": "4.3.3", + "tsx": "4.23.13", + "tw-animate-css": "1.4.0", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "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": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "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": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "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/@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/@auth/prisma-adapter": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.3.tgz", + "integrity": "sha512-jZbpVAO6PTc9zNtdTWc0RLWG8qap4iMc54/3oWaWbuKdj92wHxzPrs3HivWB2mB9975GPW0l3YM/wFUWiUlTlg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.3" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, + "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-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==", + "devOptional": 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==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.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==", + "devOptional": 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/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "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/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", + "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", + "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", + "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" + } + }, + "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.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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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-array/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/@eslint/config-array/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/@eslint/config-array/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/@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/eslintrc/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/@eslint/eslintrc/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/@eslint/eslintrc/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/@eslint/eslintrc/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/@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/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "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/@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/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, + "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": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.4.tgz", + "integrity": "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.4.tgz", + "integrity": "sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/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/@next/swc-darwin-arm64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.4.tgz", + "integrity": "sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.4.tgz", + "integrity": "sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.4.tgz", + "integrity": "sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.4.tgz", + "integrity": "sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.4.tgz", + "integrity": "sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.4.tgz", + "integrity": "sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.4.tgz", + "integrity": "sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.4.tgz", + "integrity": "sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "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/@peculiar/asn1-android": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.9.4.tgz", + "integrity": "sha512-SYHm4SoWSI0nRCoos6jpGusIqhPH9bbGBqv7ohlZ+H6BunrDzzQPk2ePgDuEUzV82OdvbLgtW4twUDwhU9P3YQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-asym-key": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.9.4.tgz", + "integrity": "sha512-s7SJfjcXlR3MYMngDTeMLCy3VjGaIxeEy905CQ0j09pDbeYhNBvBGA7r7cAvCZYkVFo9kVg9RAki0K2iUz7SGQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-post-quantum": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.9.4.tgz", + "integrity": "sha512-GD0k7BY3dsEIeai7C7bVRPddj/PzZu+sTawRPRxdHKbdy1f9b58WQyk0eFJdr28GShyLVTU8poyi4+bTAZfEtQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-asym-key": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz", + "integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-csr": "^2.9.4", + "@peculiar/asn1-ecc": "^2.9.4", + "@peculiar/asn1-pkcs9": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz", + "integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.10.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz", + "integrity": "sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.10.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.0.tgz", + "integrity": "sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz", + "integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.17", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", + "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.3", + "@electric-sql/pglite-socket": "0.1.3", + "@electric-sql/pglite-tools": "0.3.3", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.11", + "find-my-way": "9.7.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.4.2", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/dev/node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz", + "integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", + "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/fetch-engine": "7.10.0", + "@prisma/get-platform": "7.10.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", + "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", + "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/get-platform": "7.10.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", + "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.2.0", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", + "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "@visx/curve": "4.0.1-alpha.0", + "@visx/event": "4.0.1-alpha.0", + "@visx/grid": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/responsive": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "d3-array": "3.2.4", + "d3-shape": "3.2.0", + "elkjs": "0.11.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "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-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "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-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-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "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-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-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "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-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "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-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/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "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", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simplewebauthn/browser": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-14.0.0.tgz", + "integrity": "sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.1.tgz", + "integrity": "sha512-kjWgcm9NdSv+Tobo4Swxd1WnEawsBogpYFMjg9pKsVLPN3FdCmR6BipkL+DcPDgmQ7UKbbBZqRqn3E3YaA7NBw==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "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/@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/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/@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/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/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "devOptional": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": 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/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.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.70.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.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.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/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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.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.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.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/@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/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.0.tgz", + "integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.0", + "vitest": "5.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", + "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", + "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "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/mocker/node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "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/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": "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/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", + "peer": true, + "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.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "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-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.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/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "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": "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/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "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/better-result": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", + "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "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.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "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": "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", + "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/chalk/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/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": 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/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/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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/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/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "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", + "peer": true, + "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==", + "devOptional": 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.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.426", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.426.tgz", + "integrity": "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "devOptional": true, + "license": "EPL-2.0" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "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.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/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": "16.3.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.4.tgz", + "integrity": "sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.3.4", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/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-config-next/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/eslint-config-next/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/eslint-config-next/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-config-next/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-config-next/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-config-next/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-config-next/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-config-next/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/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-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-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.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": "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/eslint/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/eslint/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/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/eslint/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/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/espree/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/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/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-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "devOptional": 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-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", + "devOptional": true, + "license": "MIT", + "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": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "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/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==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", + "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": 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-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==", + "devOptional": 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.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "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": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/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/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==", + "devOptional": true, + "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/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "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/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/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.44.0.tgz", + "integrity": "sha512-2egNApH4hX4j/qdCgRublh88+9u3mEhz9iSlW5ckm4kaQEqZbXbMr0l5u5JZLy8nmWRx2dbHGQkEDYz6C9aCgw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "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/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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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": "16.3.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", + "integrity": "sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.4", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.4", + "@next/swc-darwin-x64": "16.3.4", + "@next/swc-linux-arm64-gnu": "16.3.4", + "@next/swc-linux-arm64-musl": "16.3.4", + "@next/swc-linux-x64-gnu": "16.3.4", + "@next/swc-linux-x64-musl": "16.3.4", + "@next/swc-win32-arm64-msvc": "16.3.4", + "@next/swc-win32-x64-msvc": "16.3.4", + "sharp": "^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.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-cron": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.6.0.tgz", + "integrity": "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==", + "license": "ISC", + "engines": { + "node": ">=20" + } + }, + "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-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.3.tgz", + "integrity": "sha512-h5c8ypcMoOIh44bIcVROvTDbjRdOJX8AXTkxMht2C92HlSjkG+/8kjoqmNPypfjQaVNaUHWyZ3tqs5I3K91F3g==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.8", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.8.tgz", + "integrity": "sha512-8N28E+a/oxfXWBgOMt+ZP/JUf/XR+IFbvkAEPP3gznXOMv9BpAAwiIj0TFNz3tGTPc0ZQ8zmWBNgN1nAys0gng==", + "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/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": 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/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.1", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "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.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz", + "integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "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/prisma": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", + "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.10.0", + "@prisma/dev": "0.24.17", + "@prisma/engines": "7.10.0", + "@prisma/studio-core": "0.33.0", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "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/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "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/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "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": "3.1.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", + "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.7", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "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-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/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "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==", + "devOptional": 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/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "devOptional": true, + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "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-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/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "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.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "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/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "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==", + "devOptional": 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==", + "devOptional": 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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.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/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", + "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/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-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.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/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/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": 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/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", + "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.12" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", + "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "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/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/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "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": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.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/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": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "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/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "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.4.0 || ^0.5.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/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", + "peer": true, + "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/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/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" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.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/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "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/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": "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/whatwg-url": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": 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/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/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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "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/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.1.tgz", + "integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..75cd7f9 --- /dev/null +++ b/package.json @@ -0,0 +1,102 @@ +{ + "name": "planer", + "version": "0.1.0", + "private": true, + "description": "Aufgaben, Ideen, Projekte und Zeitblöcke mit Anbindung an Microsoft 365 und Google Kalender", + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "pruefen": "node scripts/pruefen.mjs", + "format": "prettier --write .", + "format:pruefen": "prettier --check .", + "lint": "eslint . --max-warnings 0", + "typen": "tsc --noEmit", + "test": "vitest run", + "test:beobachten": "vitest", + "test:integration": "tsx scripts/integrationstest.ts", + "test:migration": "tsx scripts/migrationstest.ts", + "test:e2e": "playwright test", + "screenshots": "tsx scripts/screenshots.ts", + "db:generieren": "prisma generate", + "db:migrieren": "prisma migrate dev", + "db:einspielen": "prisma migrate deploy", + "db:seed": "tsx prisma/seed.ts", + "db:studio": "prisma studio", + "db:demo-seed": "tsx prisma/demo-seed.ts" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "overrides": { + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "nodemailer": "10.0.3" + }, + "dependencies": { + "@auth/prisma-adapter": "2.11.3", + "@date-fns/tz": "1.5.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slot": "1.3.3", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "bcryptjs": "3.0.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.4.0", + "lucide-react": "1.44.0", + "next": "16.3.4", + "next-auth": "5.0.0-beta.32", + "next-themes": "0.4.6", + "node-cron": "4.6.0", + "nodemailer": "10.0.3", + "pg": "8.23.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "server-only": "0.0.1", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "zod": "4.6.1" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "4.3.3", + "@testing-library/react": "16.3.3", + "@types/node": "24.13.4", + "@types/nodemailer": "8.0.1", + "@types/pg": "8.23.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "@vitest/coverage-v8": "5.0.0", + "dotenv": "17.4.2", + "eslint": "9.39.5", + "eslint-config-next": "16.3.4", + "jsdom": "30.0.1", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", + "prisma": "7.10.0", + "tailwindcss": "4.3.3", + "tsx": "4.23.13", + "tw-animate-css": "1.4.0", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + }, + "allowScripts": { + "prisma@7.10.0": true, + "@prisma/engines@7.10.0": true, + "esbuild@0.28.2": true, + "unrs-resolver@1.12.2": true + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e7de6a3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test' +import 'dotenv/config' + +const PORT = Number(process.env.E2E_PORT ?? 3100) +const BASIS = `http://127.0.0.1:${PORT}` + +/** + * E2E-Tests gegen eine echte, gebaute Anwendung. + * + * Der Testserver läuft auf einem eigenen Port, damit er einer laufenden + * Entwicklungsinstanz nicht in die Quere kommt. Die Datenbank ist dieselbe wie + * im Entwicklungsbetrieb – die Tests legen nur Daten mit eigenem Präfix an und + * räumen hinter sich auf. + */ +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI ? 'github' : 'list', + timeout: 30_000, + + use: { + baseURL: BASIS, + locale: 'de-DE', + timezoneId: 'Europe/Berlin', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + + webServer: { + command: 'npm run start', + url: `${BASIS}/api/health`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + PORT: String(PORT), + APP_URL: BASIS, + WEBAUTHN_ORIGIN: BASIS, + WEBAUTHN_RP_ID: '127.0.0.1', + }, + }, +}) diff --git a/playwright.screenshots.config.ts b/playwright.screenshots.config.ts new file mode 100644 index 0000000..1c0fcda --- /dev/null +++ b/playwright.screenshots.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@playwright/test' + +const PORT = process.env.SCREENSHOT_APP_PORT ?? '3200' + +/** + * Eigene Konfiguration für die Screenshot-Instanz. Sie startet keinen Server — + * darum kümmert sich scripts/screenshots.ts über Docker Compose. + */ +export default defineConfig({ + testDir: './screenshots', + fullyParallel: false, + workers: 1, + reporter: 'list', + timeout: 60_000, + use: { + baseURL: `http://127.0.0.1:${PORT}`, + locale: 'de-DE', + timezoneId: 'Europe/Berlin', + }, +}) diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..a869506 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +} + +export default config diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..8ccc8cf --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,17 @@ +import 'dotenv/config' +import { defineConfig } from 'prisma/config' + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + seed: 'tsx prisma/seed.ts', + }, + datasource: { + // Bewusst über process.env statt über den env()-Helfer: "prisma generate" + // läuft auch beim Bauen des Abbilds, wo es noch gar keine Datenbank gibt. + // env() bricht dort ab. Fehlt die URL für migrate oder studio, meldet + // Prisma das selbst. + url: process.env.DATABASE_URL ?? '', + }, +}) diff --git a/prisma/demo-seed.ts b/prisma/demo-seed.ts new file mode 100644 index 0000000..2b5592c --- /dev/null +++ b/prisma/demo-seed.ts @@ -0,0 +1,172 @@ +/** + * Daten für die Screenshot-Instanz. + * + * Alles hier ist frei erfunden. Diese Datei darf niemals gegen eine + * Datenbank mit echten Daten laufen — deshalb bricht sie ab, wenn + * DEMO_MODUS nicht ausdrücklich auf true steht. + * + * Aufruf: über npm run screenshots, nicht von Hand. + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +if (process.env.DEMO_MODUS !== 'true') { + console.error( + 'Der Demo-Seed läuft nur mit DEMO_MODUS=true. Das ist Absicht: er würde ' + + 'sonst erfundene Daten in eine echte Datenbank schreiben.', + ) + process.exit(1) +} + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +export const DEMO_EMAIL = 'martina.beispiel@planer.invalid' +export const DEMO_PASSWORT = 'demo-instanz-nur-fuer-bilder' + +const BEREICHE = [ + { + name: 'Firma', + farbe: '#2563eb', + reihenfolge: 1, + arbeitsfensterVon: 450, + arbeitsfensterBis: 990, + kapazitaetMo: 360, + kapazitaetDi: 360, + kapazitaetMi: 360, + kapazitaetDo: 360, + kapazitaetFr: 240, + kapazitaetSa: 0, + kapazitaetSo: 0, + }, + { + name: 'Gewerbe', + farbe: '#7c3aed', + reihenfolge: 2, + arbeitsfensterVon: 1020, + arbeitsfensterBis: 1260, + kapazitaetMo: 90, + kapazitaetDi: 90, + kapazitaetMi: 90, + kapazitaetDo: 90, + kapazitaetFr: 60, + kapazitaetSa: 180, + kapazitaetSo: 0, + }, + { + name: 'Privat', + farbe: '#16a34a', + reihenfolge: 3, + arbeitsfensterVon: 1020, + arbeitsfensterBis: 1320, + kapazitaetMo: 60, + kapazitaetDi: 60, + kapazitaetMi: 60, + kapazitaetDo: 60, + kapazitaetFr: 120, + kapazitaetSa: 240, + kapazitaetSo: 240, + }, + { + name: 'Homelab', + farbe: '#ea580c', + reihenfolge: 4, + arbeitsfensterVon: 1140, + arbeitsfensterBis: 1380, + kapazitaetMo: 0, + kapazitaetDi: 60, + kapazitaetMi: 0, + kapazitaetDo: 60, + kapazitaetFr: 0, + kapazitaetSa: 120, + kapazitaetSo: 60, + }, +] + +/** Erfundene Protokolleinträge, damit die Protokollseite nicht leer wirkt. */ +const PROTOKOLL = [ + { aktion: 'bereich.angelegt', objektTyp: 'Bereich', details: { name: 'Homelab' } }, + { + aktion: 'bereich.geaendert', + objektTyp: 'Bereich', + details: { vorher: 'Nebenerwerb', nachher: 'Gewerbe' }, + }, + { + aktion: 'passkey.angelegt', + objektTyp: 'Passkey', + details: { bezeichnung: 'Diensthandy' }, + }, +] + +async function main(): Promise { + console.log('Demo-Daten einspielen.') + + for (const bereich of BEREICHE) { + await db.bereich.upsert({ + where: { name: bereich.name }, + update: bereich, + create: bereich, + }) + } + + const benutzer = await db.benutzer.upsert({ + where: { email: DEMO_EMAIL }, + update: { rolle: 'ADMIN', name: 'Martina Beispiel' }, + create: { + email: DEMO_EMAIL, + name: 'Martina Beispiel', + rolle: 'ADMIN', + passwortHash: await bcrypt.hash(DEMO_PASSWORT, 10), + }, + }) + + await db.auditEintrag.deleteMany({}) + // Feste Zeitpunkte, damit die Bilder zwischen zwei Läufen gleich bleiben. + let versatz = 0 + for (const eintrag of PROTOKOLL) { + versatz += 37 + await db.auditEintrag.create({ + data: { + zeitpunkt: new Date(Date.UTC(2026, 2, 17, 9, versatz % 60)), + benutzerId: benutzer.id, + benutzerEmail: benutzer.email, + aktion: eintrag.aktion, + objektTyp: eintrag.objektTyp, + details: eintrag.details, + instanz: 'daheim', + }, + }) + } + + await db.passkey.deleteMany({ where: { benutzerId: benutzer.id } }) + await db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: 'demo-passkey-diensthandy', + publicKey: Buffer.from([0]), + bezeichnung: 'Diensthandy', + gesichert: true, + erstelltAm: new Date(Date.UTC(2026, 1, 3, 8, 15)), + zuletztBenutzt: new Date(Date.UTC(2026, 2, 17, 7, 42)), + }, + }) + + console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`) +} + +main() + .then(async () => { + await db.$disconnect() + }) + .catch(async (fehler: unknown) => { + console.error(fehler) + await db.$disconnect() + process.exit(1) + }) diff --git a/prisma/migrations/20260910104400_fundament/migration.sql b/prisma/migrations/20260910104400_fundament/migration.sql new file mode 100644 index 0000000..5db03f3 --- /dev/null +++ b/prisma/migrations/20260910104400_fundament/migration.sql @@ -0,0 +1,147 @@ +-- CreateEnum +CREATE TYPE "Rolle" AS ENUM ('ADMIN', 'BENUTZER'); + +-- CreateTable +CREATE TABLE "benutzer" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "passwortHash" TEXT, + "rolle" "Rolle" NOT NULL DEFAULT 'BENUTZER', + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "bildUrl" TEXT, + "emailGeprueft" TIMESTAMP(3), + "zuletztAktiv" TIMESTAMP(3), + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "aktualisiertAm" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "benutzer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "konto" ( + "id" TEXT NOT NULL, + "benutzerId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "konto_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "passkey" ( + "id" TEXT NOT NULL, + "benutzerId" TEXT NOT NULL, + "credentialId" TEXT NOT NULL, + "publicKey" BYTEA NOT NULL, + "counter" BIGINT NOT NULL DEFAULT 0, + "transports" TEXT, + "geraetetyp" TEXT, + "gesichert" BOOLEAN NOT NULL DEFAULT false, + "bezeichnung" TEXT NOT NULL, + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "zuletztBenutzt" TIMESTAMP(3), + + CONSTRAINT "passkey_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "webauthn_challenge" ( + "id" TEXT NOT NULL, + "kennung" TEXT NOT NULL, + "challenge" TEXT NOT NULL, + "zweck" TEXT NOT NULL, + "gueltigBis" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "webauthn_challenge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "bereich" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "farbe" TEXT NOT NULL, + "reihenfolge" INTEGER NOT NULL DEFAULT 0, + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "kapazitaetMo" INTEGER NOT NULL DEFAULT 0, + "kapazitaetDi" INTEGER NOT NULL DEFAULT 0, + "kapazitaetMi" INTEGER NOT NULL DEFAULT 0, + "kapazitaetDo" INTEGER NOT NULL DEFAULT 0, + "kapazitaetFr" INTEGER NOT NULL DEFAULT 0, + "kapazitaetSa" INTEGER NOT NULL DEFAULT 0, + "kapazitaetSo" INTEGER NOT NULL DEFAULT 0, + "arbeitsfensterVon" INTEGER NOT NULL DEFAULT 480, + "arbeitsfensterBis" INTEGER NOT NULL DEFAULT 1020, + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "aktualisiertAm" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "bereich_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_eintrag" ( + "id" TEXT NOT NULL, + "zeitpunkt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "benutzerId" TEXT, + "benutzerEmail" TEXT NOT NULL, + "aktion" TEXT NOT NULL, + "objektTyp" TEXT NOT NULL, + "objektId" TEXT, + "details" JSONB, + "instanz" TEXT NOT NULL, + + CONSTRAINT "audit_eintrag_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "benutzer_email_key" ON "benutzer"("email"); + +-- CreateIndex +CREATE INDEX "benutzer_aktiv_idx" ON "benutzer"("aktiv"); + +-- CreateIndex +CREATE INDEX "konto_benutzerId_idx" ON "konto"("benutzerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "konto_provider_providerAccountId_key" ON "konto"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "passkey_credentialId_key" ON "passkey"("credentialId"); + +-- CreateIndex +CREATE INDEX "passkey_benutzerId_idx" ON "passkey"("benutzerId"); + +-- CreateIndex +CREATE INDEX "webauthn_challenge_kennung_zweck_idx" ON "webauthn_challenge"("kennung", "zweck"); + +-- CreateIndex +CREATE UNIQUE INDEX "bereich_name_key" ON "bereich"("name"); + +-- CreateIndex +CREATE INDEX "bereich_aktiv_reihenfolge_idx" ON "bereich"("aktiv", "reihenfolge"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_zeitpunkt_idx" ON "audit_eintrag"("zeitpunkt"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_objektTyp_objektId_idx" ON "audit_eintrag"("objektTyp", "objektId"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_benutzerId_idx" ON "audit_eintrag"("benutzerId"); + +-- AddForeignKey +ALTER TABLE "konto" ADD CONSTRAINT "konto_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "passkey" ADD CONSTRAINT "passkey_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_eintrag" ADD CONSTRAINT "audit_eintrag_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..da39cac --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,148 @@ +// Datenmodell des Planers. +// Fachbegriffe auf Deutsch, technische Felder auf Englisch. + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" + moduleFormat = "esm" +} + +datasource db { + provider = "postgresql" +} + +enum Rolle { + ADMIN + BENUTZER +} + +/// Ein Mensch, der sich anmelden darf. +model Benutzer { + id String @id @default(cuid()) + email String @unique + name String? + /// Nur gesetzt, wenn die Anmeldung per Passwort erlaubt ist. + passwortHash String? + rolle Rolle @default(BENUTZER) + aktiv Boolean @default(true) + bildUrl String? + emailGeprueft DateTime? + zuletztAktiv DateTime? + erstelltAm DateTime @default(now()) + aktualisiertAm DateTime @updatedAt + + konten Konto[] + passkeys Passkey[] + audit AuditEintrag[] + + @@index([aktiv]) + @@map("benutzer") +} + +/// Verknuepfung zu einem OIDC-Anbieter (Entra ID, authentik). +model Konto { + id String @id @default(cuid()) + benutzerId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([benutzerId]) + @@map("konto") +} + +/// Ein registrierter Passkey (WebAuthn) eines Benutzers. +model Passkey { + id String @id @default(cuid()) + benutzerId String + /// Base64URL der Credential-ID. + credentialId String @unique + publicKey Bytes + counter BigInt @default(0) + /// Kommagetrennt, z. B. "internal,hybrid". + transports String? + geraetetyp String? + gesichert Boolean @default(false) + bezeichnung String + erstelltAm DateTime @default(now()) + zuletztBenutzt DateTime? + + benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) + + @@index([benutzerId]) + @@map("passkey") +} + +/// Laufende WebAuthn-Herausforderung; kurzlebig, wird nach Gebrauch geloescht. +model WebauthnChallenge { + id String @id @default(cuid()) + /// E-Mail bei der Anmeldung, Benutzer-ID bei der Registrierung. + kennung String + challenge String + zweck String + gueltigBis DateTime + + @@index([kennung, zweck]) + @@map("webauthn_challenge") +} + +/// Lebensbereich mit Farbe, Tageskapazitaet und Arbeitsfenster. +model Bereich { + id String @id @default(cuid()) + name String @unique + /// Hex-Farbe inklusive Raute, z. B. "#2563eb". + farbe String + reihenfolge Int @default(0) + aktiv Boolean @default(true) + + /// Tageskapazitaet in Minuten, Montag bis Sonntag. + kapazitaetMo Int @default(0) + kapazitaetDi Int @default(0) + kapazitaetMi Int @default(0) + kapazitaetDo Int @default(0) + kapazitaetFr Int @default(0) + kapazitaetSa Int @default(0) + kapazitaetSo Int @default(0) + + /// Arbeitsfenster als Minuten seit Mitternacht, Europe/Berlin. + arbeitsfensterVon Int @default(480) + arbeitsfensterBis Int @default(1020) + + erstelltAm DateTime @default(now()) + aktualisiertAm DateTime @updatedAt + + @@index([aktiv, reihenfolge]) + @@map("bereich") +} + +/// Unveraenderlicher Nachweis jeder schreibenden Aktion. +model AuditEintrag { + id String @id @default(cuid()) + zeitpunkt DateTime @default(now()) + benutzerId String? + /// Bleibt erhalten, auch wenn der Benutzer spaeter geloescht wird. + benutzerEmail String + aktion String + objektTyp String + objektId String? + /// Nur fachliche Felder, niemals Tokens, Mailinhalte oder Termin-Betreffs. + details Json? + instanz String + + benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull) + + @@index([zeitpunkt]) + @@index([objektTyp, objektId]) + @@index([benutzerId]) + @@map("audit_eintrag") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..4cfa0a9 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,156 @@ +/** + * Grundausstattung für eine frische Datenbank. + * + * Läuft mehrfach ohne Schaden: alles wird per upsert angelegt, vorhandene + * Einträge bleiben unangetastet. Es entstehen keine erfundenen Aufgaben oder + * Termine – dafür gibt es prisma/demo-seed.ts. + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + throw new Error('DATABASE_URL ist nicht gesetzt.') +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +const BEREICHE = [ + { + name: 'Firma', + farbe: '#2563eb', + reihenfolge: 1, + arbeitsfensterVon: 7 * 60 + 30, + arbeitsfensterBis: 16 * 60 + 30, + kapazitaetMo: 360, + kapazitaetDi: 360, + kapazitaetMi: 360, + kapazitaetDo: 360, + kapazitaetFr: 240, + kapazitaetSa: 0, + kapazitaetSo: 0, + }, + { + name: 'Gewerbe', + farbe: '#7c3aed', + reihenfolge: 2, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 21 * 60, + kapazitaetMo: 90, + kapazitaetDi: 90, + kapazitaetMi: 90, + kapazitaetDo: 90, + kapazitaetFr: 60, + kapazitaetSa: 180, + kapazitaetSo: 0, + }, + { + name: 'Privat', + farbe: '#16a34a', + reihenfolge: 3, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 22 * 60, + kapazitaetMo: 60, + kapazitaetDi: 60, + kapazitaetMi: 60, + kapazitaetDo: 60, + kapazitaetFr: 120, + kapazitaetSa: 240, + kapazitaetSo: 240, + }, + { + name: 'Homelab', + farbe: '#ea580c', + reihenfolge: 4, + arbeitsfensterVon: 19 * 60, + arbeitsfensterBis: 23 * 60, + kapazitaetMo: 0, + kapazitaetDi: 60, + kapazitaetMi: 0, + kapazitaetDo: 60, + kapazitaetFr: 0, + kapazitaetSa: 120, + kapazitaetSo: 60, + }, +] + +async function bereicheAnlegen(): Promise { + for (const bereich of BEREICHE) { + await db.bereich.upsert({ + where: { name: bereich.name }, + update: {}, + create: bereich, + }) + } + console.log(`Bereiche geprüft: ${BEREICHE.map((b) => b.name).join(', ')}`) +} + +async function verwaltungAnlegen(): Promise { + const email = process.env.ADMIN_EMAIL?.trim().toLowerCase() + const passwort = process.env.ADMIN_PASSWORT + + if (!email) { + console.log( + 'ADMIN_EMAIL ist nicht gesetzt – kein Verwaltungskonto angelegt. ' + + 'Bei reiner OIDC-Anmeldung ist das richtig: die erste Person, die sich ' + + 'anmeldet, muss danach von Hand hochgestuft werden.', + ) + return + } + + const vorhanden = await db.benutzer.findUnique({ + where: { email }, + select: { id: true, rolle: true }, + }) + + if (vorhanden) { + if (vorhanden.rolle !== 'ADMIN') { + await db.benutzer.update({ where: { id: vorhanden.id }, data: { rolle: 'ADMIN' } }) + console.log(`${email} zur Verwaltung hochgestuft.`) + } else { + console.log(`${email} ist bereits Verwaltung.`) + } + return + } + + if (!passwort) { + await db.benutzer.create({ + data: { email, rolle: 'ADMIN', name: null }, + }) + console.log( + `${email} als Verwaltung angelegt, ohne Passwort. ` + + 'Anmeldung ist damit nur über OIDC oder einen Passkey möglich.', + ) + return + } + + if (passwort.length < 12) { + throw new Error('ADMIN_PASSWORT ist zu kurz, mindestens 12 Zeichen.') + } + + await db.benutzer.create({ + data: { + email, + rolle: 'ADMIN', + passwortHash: await bcrypt.hash(passwort, 12), + }, + }) + console.log(`${email} als Verwaltung mit Passwort angelegt.`) +} + +async function main(): Promise { + await bereicheAnlegen() + await verwaltungAnlegen() +} + +main() + .then(async () => { + await db.$disconnect() + }) + .catch(async (fehler: unknown) => { + console.error(fehler) + await db.$disconnect() + process.exit(1) + }) diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..fc68be5 --- /dev/null +++ b/public/.gitkeep @@ -0,0 +1 @@ +# Der Ordner bleibt im Git, damit der Docker-Bau ihn kopieren kann. diff --git a/screenshots/bilder.spec.ts b/screenshots/bilder.spec.ts new file mode 100644 index 0000000..067f83f --- /dev/null +++ b/screenshots/bilder.spec.ts @@ -0,0 +1,88 @@ +import { mkdir } from 'node:fs/promises' +import { test, type Page } from '@playwright/test' + +/** + * Nimmt die Bilder für das Handbuch auf. + * + * Je Seite vier Bilder: hell und dunkel, Schreibtisch und Handy. Die Uhr steht + * auf einem festen Zeitpunkt, damit sich zwischen zwei Läufen nur das ändert, + * was sich wirklich geändert hat. + */ + +const ZIEL = 'docs/bilder' +const SCHREIBTISCH = { width: 1440, height: 900 } +const HANDY = { width: 390, height: 844 } + +// Dienstag, 17. März 2026, 09:20 Uhr in Berlin. +const FESTE_ZEIT = new Date('2026-03-17T08:20:00.000Z') + +const EMAIL = 'martina.beispiel@planer.invalid' +const PASSWORT = 'demo-instanz-nur-fuer-bilder' + +type Aufnahme = { + name: string + pfad: string + /** Wird vor der Aufnahme ausgeführt, z. B. um ein Formular aufzuklappen. */ + vorbereiten?: (seite: Page) => Promise +} + +const AUFNAHMEN: Aufnahme[] = [ + { name: 'anmelden', pfad: '/anmelden' }, + { name: 'start', pfad: '/' }, + { name: 'bereiche', pfad: '/bereiche' }, + { name: 'einstellungen', pfad: '/einstellungen' }, + { name: 'protokoll', pfad: '/verwaltung/protokoll' }, +] + +async function anmelden(seite: Page): Promise { + await seite.goto('/anmelden') + await seite.getByLabel('E-Mail-Adresse').fill(EMAIL) + await seite.getByLabel('Passwort').fill(PASSWORT) + await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await seite.waitForURL('/') +} + +test.beforeAll(async () => { + await mkdir(ZIEL, { recursive: true }) +}) + +for (const schema of ['hell', 'dunkel'] as const) { + for (const [geraet, groesse] of [ + ['schreibtisch', SCHREIBTISCH], + ['handy', HANDY], + ] as const) { + test(`Bilder: ${schema}, ${geraet}`, async ({ browser }) => { + const kontext = await browser.newContext({ + viewport: groesse, + colorScheme: schema === 'dunkel' ? 'dark' : 'light', + deviceScaleFactor: 2, + }) + const seite = await kontext.newPage() + await seite.clock.setFixedTime(FESTE_ZEIT) + + for (const aufnahme of AUFNAHMEN) { + if (aufnahme.pfad !== '/anmelden') { + // Für alles außer der Anmeldeseite braucht es eine Sitzung. + if ( + !(await seite.context().cookies()).some((k) => k.name.includes('session')) + ) { + await anmelden(seite) + } + } + + await seite.goto(aufnahme.pfad) + await seite.waitForLoadState('networkidle') + await aufnahme.vorbereiten?.(seite) + // Der Farbschema-Umschalter und Schriften sollen fertig sein. + await seite.waitForTimeout(300) + + await seite.screenshot({ + path: `${ZIEL}/${aufnahme.name}-${schema}-${geraet}.png`, + fullPage: true, + }) + } + + await kontext.close() + }) + } +} diff --git a/scripts/integrationstest.ts b/scripts/integrationstest.ts new file mode 100644 index 0000000..41f217c --- /dev/null +++ b/scripts/integrationstest.ts @@ -0,0 +1,208 @@ +/** + * Integrationstest gegen eine echte Datenbank. + * + * Prüft, was Vitest nicht abdeckt: Eindeutigkeit, Fremdschlüssel, Kaskaden + * und dass das Audit-Protokoll den Benutzer überlebt. + * + * Bewusst ohne umschließende Transaktion: Der Test prüft absichtlich Fälle, + * die scheitern müssen, und ein gescheiterter Befehl bricht in PostgreSQL die + * ganze Transaktion ab. Stattdessen tragen alle Testdaten dieselbe Kennung + * und werden am Ende wieder entfernt — auch wenn der Test unterwegs abbricht. + * + * Aufruf: npm run test:integration + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from '../src/generated/prisma/client.js' + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +type Pruefung = { name: string; ok: boolean; hinweis?: string } +const ergebnisse: Pruefung[] = [] + +function pruefe(name: string, bedingung: boolean, hinweis?: string): void { + ergebnisse.push(hinweis ? { name, ok: bedingung, hinweis } : { name, ok: bedingung }) +} + +/** Erwartet, dass die Aktion scheitert. */ +async function scheitert(name: string, tun: () => Promise): Promise { + try { + await tun() + pruefe(name, false, 'Die Aktion war erlaubt, obwohl sie scheitern sollte.') + } catch { + pruefe(name, true) + } +} + +const kennung = `zzz-test-${Date.now()}` +const testEmail = `${kennung}@example.invalid` + +async function aufraeumen(): Promise { + // Reihenfolge egal, die Kaskaden erledigen den Rest; der Audit-Eintrag + // hängt bewusst an keinem Benutzer mehr. + await db.auditEintrag.deleteMany({ where: { benutzerEmail: testEmail } }) + await db.benutzer.deleteMany({ where: { email: testEmail } }) + await db.bereich.deleteMany({ where: { name: { startsWith: kennung } } }) + await db.webauthnChallenge.deleteMany({ where: { kennung } }) +} + +async function main(): Promise { + // --- Benutzer und Eindeutigkeit der Adresse ------------------------------ + const benutzer = await db.benutzer.create({ + data: { email: testEmail, name: 'Testkonto' }, + }) + pruefe('Benutzer anlegen', benutzer.id.length > 0) + pruefe('Rolle ist im Zweifel BENUTZER', benutzer.rolle === 'BENUTZER') + pruefe('Konto ist im Zweifel aktiv', benutzer.aktiv === true) + + await scheitert('E-Mail-Adresse ist eindeutig', () => + db.benutzer.create({ data: { email: testEmail } }), + ) + + // --- Bereich und Eindeutigkeit des Namens -------------------------------- + const bereich = await db.bereich.create({ + data: { name: `${kennung}-bereich`, farbe: '#2563eb', kapazitaetMo: 120 }, + }) + pruefe('Bereich anlegen', bereich.name.endsWith('-bereich')) + pruefe( + 'Arbeitsfenster hat brauchbare Standardwerte', + bereich.arbeitsfensterVon === 480 && bereich.arbeitsfensterBis === 1020, + ) + + await scheitert('Bereichsname ist eindeutig', () => + db.bereich.create({ data: { name: bereich.name, farbe: '#000000' } }), + ) + + // --- Passkey, Eindeutigkeit und Fremdschlüssel --------------------------- + const passkey = await db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: `${kennung}-cred`, + publicKey: Buffer.from([1, 2, 3]), + bezeichnung: 'Testgerät', + }, + }) + pruefe('Passkey anlegen', passkey.counter === 0n) + pruefe( + 'Zähler ist ein BigInt und beginnt bei null', + typeof passkey.counter === 'bigint', + ) + + await scheitert('Credential-ID ist eindeutig', () => + db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: passkey.credentialId, + publicKey: Buffer.from([4]), + bezeichnung: 'Zweitgerät', + }, + }), + ) + + await scheitert('Passkey ohne gültigen Benutzer ist nicht möglich', () => + db.passkey.create({ + data: { + benutzerId: 'gibt-es-nicht', + credentialId: `${kennung}-waise`, + publicKey: Buffer.from([9]), + bezeichnung: 'Waise', + }, + }), + ) + + // --- Öffentlicher Schlüssel kommt unverändert zurück --------------------- + const gelesen = await db.passkey.findUnique({ where: { id: passkey.id } }) + pruefe( + 'Der öffentliche Schlüssel kommt Byte für Byte zurück', + Buffer.from(gelesen?.publicKey ?? []).equals(Buffer.from([1, 2, 3])), + ) + + // --- Konto und Eindeutigkeit je Anbieter --------------------------------- + await db.konto.create({ + data: { + benutzerId: benutzer.id, + type: 'oidc', + provider: 'oidc', + providerAccountId: kennung, + }, + }) + await scheitert('Ein Anbieterkonto ist nur einmal verknüpfbar', () => + db.konto.create({ + data: { + benutzerId: benutzer.id, + type: 'oidc', + provider: 'oidc', + providerAccountId: kennung, + }, + }), + ) + + // --- Audit überlebt den Benutzer ---------------------------------------- + const audit = await db.auditEintrag.create({ + data: { + benutzerId: benutzer.id, + benutzerEmail: benutzer.email, + aktion: 'test.durchgefuehrt', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name }, + instanz: 'daheim', + }, + }) + pruefe('Audit-Eintrag anlegen', audit.id.length > 0) + + await db.benutzer.delete({ where: { id: benutzer.id } }) + + const nachher = await db.auditEintrag.findUnique({ where: { id: audit.id } }) + pruefe('Audit-Eintrag überlebt das Löschen des Benutzers', nachher !== null) + pruefe( + 'Die Kennung wird gelöst, die Adresse bleibt als Nachweis stehen', + nachher?.benutzerId === null && nachher?.benutzerEmail === testEmail, + ) + + pruefe( + 'Passkeys verschwinden mit dem Benutzer', + (await db.passkey.count({ where: { credentialId: passkey.credentialId } })) === 0, + ) + pruefe( + 'Verknüpfte Konten verschwinden mit dem Benutzer', + (await db.konto.count({ where: { providerAccountId: kennung } })) === 0, + ) +} + +let abbruch: unknown = null + +main() + .catch((fehler: unknown) => { + abbruch = fehler + }) + .finally(async () => { + await aufraeumen() + + const uebrig = + (await db.benutzer.count({ where: { email: testEmail } })) + + (await db.bereich.count({ where: { name: { startsWith: kennung } } })) + pruefe('Aufräumen hat nichts übrig gelassen', uebrig === 0) + + await db.$disconnect() + + if (abbruch) { + console.error('Der Integrationstest ist abgebrochen:', abbruch) + process.exit(1) + } + + const gefallen = ergebnisse.filter((e) => !e.ok) + for (const e of ergebnisse) { + console.log(`${e.ok ? '✓' : '✗'} ${e.name}${e.hinweis ? ` – ${e.hinweis}` : ''}`) + } + console.log( + `\n${ergebnisse.length - gefallen.length} von ${ergebnisse.length} Prüfungen bestanden.`, + ) + process.exit(gefallen.length === 0 ? 0 : 1) + }) diff --git a/scripts/migrationstest.ts b/scripts/migrationstest.ts new file mode 100644 index 0000000..52fbe4f --- /dev/null +++ b/scripts/migrationstest.ts @@ -0,0 +1,121 @@ +/** + * Migrationstest. + * + * Legt eine Wegwerf-Datenbank an, spielt alle Migrationen der Reihe nach ein + * und prüft anschließend, ob das Ergebnis genau dem Schema entspricht. Damit + * fällt auf, wenn jemand das Schema ändert, ohne eine Migration zu erzeugen — + * beim Aufspielen im Betrieb wäre das sonst erst spät zu merken. + * + * Aufruf: npm run test:migration + */ +import 'dotenv/config' +import { spawnSync } from 'node:child_process' +import { join } from 'node:path' +import { Client } from 'pg' + +const WURZEL = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1') + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const testDatenbank = `planer_migrationstest_${Date.now()}` + +/** Dieselbe Verbindung, aber auf eine andere Datenbank gerichtet. */ +function mitDatenbank(url: string, name: string): string { + const zerlegt = new URL(url) + zerlegt.pathname = `/${name}` + return zerlegt.toString() +} + +function prisma(argumente: string[], zusatz: Record): number { + const ergebnis = spawnSync( + process.execPath, + [join(WURZEL, 'node_modules', 'prisma', 'build', 'index.js'), ...argumente], + { + cwd: WURZEL, + stdio: 'inherit', + env: { ...process.env, ...zusatz }, + }, + ) + return ergebnis.status ?? 1 +} + +async function aufVerwaltungsdatenbank( + tun: (klient: Client) => Promise, +): Promise { + // "postgres" ist auf jedem Server vorhanden und wird hier nur benutzt, um + // die Wegwerf-Datenbank anzulegen und wieder zu entfernen. + const klient = new Client({ connectionString: mitDatenbank(verbindung!, 'postgres') }) + await klient.connect() + try { + return await tun(klient) + } finally { + await klient.end() + } +} + +async function main(): Promise { + console.log(`▸ Wegwerf-Datenbank ${testDatenbank} anlegen`) + await aufVerwaltungsdatenbank((k) => k.query(`CREATE DATABASE "${testDatenbank}"`)) + + const testUrl = mitDatenbank(verbindung!, testDatenbank) + + console.log('▸ Alle Migrationen einspielen') + if (prisma(['migrate', 'deploy'], { DATABASE_URL: testUrl }) !== 0) { + console.error('✗ Die Migrationen ließen sich nicht einspielen.') + return 1 + } + + console.log('▸ Ergebnis mit dem Schema vergleichen') + // --exit-code liefert 2, wenn ein Unterschied bleibt. + const abweichung = prisma( + [ + 'migrate', + 'diff', + // Die Quelle ist die eben eingespielte Wegwerf-Datenbank, das Ziel das + // Schema. So braucht der Vergleich keine Schattendatenbank. + '--from-config-datasource', + '--to-schema', + 'prisma/schema.prisma', + '--exit-code', + ], + { DATABASE_URL: testUrl }, + ) + + if (abweichung === 2) { + console.error( + '\n✗ Nach dem Einspielen weicht die Datenbank vom Schema ab.\n' + + ' Vermutlich fehlt eine Migration: "npx prisma migrate dev --name ".', + ) + return 1 + } + if (abweichung !== 0) { + console.error('✗ Der Vergleich ließ sich nicht durchführen.') + return 1 + } + + console.log('\n✓ Die Migrationen ergeben genau das Schema.') + return 0 +} + +let code = 1 +try { + code = await main() +} catch (fehler) { + console.error('Der Migrationstest ist abgebrochen:', fehler) +} finally { + await aufVerwaltungsdatenbank(async (k) => { + // Offene Verbindungen trennen, sonst lässt sich die Datenbank nicht löschen. + await k.query( + 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1', + [testDatenbank], + ) + await k.query(`DROP DATABASE IF EXISTS "${testDatenbank}"`) + }) + console.log(`▸ Wegwerf-Datenbank ${testDatenbank} entfernt`) +} + +process.exit(code) diff --git a/scripts/pruefen.mjs b/scripts/pruefen.mjs new file mode 100644 index 0000000..38f1c38 --- /dev/null +++ b/scripts/pruefen.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * Sammelpruefung vor jedem Commit: Format, Lint, Typen, Tests, Shellskripte. + * Bricht beim ersten Fehler ab und nennt den Schritt, der gefallen ist. + * + * Die Werkzeuge werden ueber ihren JavaScript-Einstiegspunkt gestartet, nicht + * ueber npx. Das spart eine Shell und laeuft unter Windows wie unter Linux + * gleich. + */ +import { spawnSync } from 'node:child_process' +import { existsSync, readdirSync, statSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { fileURLToPath } from 'node:url' + +const WURZEL = fileURLToPath(new URL('..', import.meta.url)) +const istWindows = process.platform === 'win32' + +/** Windows-Pfad in Schraegstrich-Schreibweise, wie Docker sie erwartet. */ +function mitSchraegstrich(pfad) { + return pfad.split(sep).join('/') +} + +/** Ein mitgeliefertes Werkzeug im selben Node starten. */ +function werkzeug(paket, einstieg, argumente) { + const ergebnis = spawnSync( + process.execPath, + [join(WURZEL, 'node_modules', paket, einstieg), ...argumente], + { cwd: WURZEL, stdio: 'inherit' }, + ) + return ergebnis.status ?? 1 +} + +function programm(befehl, argumente, optionen = {}) { + const ergebnis = spawnSync(befehl, argumente, { + cwd: WURZEL, + stdio: 'inherit', + ...optionen, + }) + return ergebnis.status ?? 1 +} + +function vorhanden(befehl) { + const ergebnis = spawnSync(istWindows ? 'where' : 'which', [befehl], { + cwd: WURZEL, + stdio: 'ignore', + }) + return ergebnis.status === 0 +} + +const UEBERGEHEN = new Set([ + 'node_modules', + '.git', + '.next', + 'coverage', + 'logs', + 'sicherungen', + 'playwright-report', + 'test-results', +]) + +/** Alle .sh-Dateien im Projekt, ohne node_modules und Konsorten. */ +function shellskripteFinden(verzeichnis = WURZEL, gefunden = []) { + for (const eintrag of readdirSync(verzeichnis)) { + if (UEBERGEHEN.has(eintrag)) continue + const pfad = join(verzeichnis, eintrag) + if (statSync(pfad).isDirectory()) { + shellskripteFinden(pfad, gefunden) + } else if (eintrag.endsWith('.sh')) { + gefunden.push(mitSchraegstrich(relative(WURZEL, pfad))) + } + } + return gefunden +} + +function shellskriptePruefen() { + const skripte = shellskripteFinden() + if (skripte.length === 0) { + console.log(' Keine Shellskripte gefunden.') + return 0 + } + console.log(` ${skripte.length} Skript(e): ${skripte.join(', ')}`) + const argumente = ['--shell=bash', '--severity=style', ...skripte] + + if (vorhanden('shellcheck')) { + return programm('shellcheck', argumente) + } + + if (programm('docker', ['info'], { stdio: 'ignore' }) === 0) { + console.log(' shellcheck ist nicht installiert, nehme das Docker-Abbild.') + return programm('docker', [ + 'run', + '--rm', + '-v', + `${mitSchraegstrich(WURZEL)}:/mnt`, + 'koalaman/shellcheck:stable', + ...argumente, + ]) + } + + console.error( + 'Fehler: Weder shellcheck noch ein laufender Docker-Daemon gefunden. ' + + 'Entweder shellcheck installieren (https://www.shellcheck.net/) oder ' + + 'Docker starten. Die Shellskripte bleiben sonst ungeprueft.', + ) + return 1 +} + +const schritte = [ + { + name: 'Format (Prettier)', + tun: () => werkzeug('prettier', 'bin/prettier.cjs', ['--check', '.']), + }, + { + name: 'Lint (ESLint)', + tun: () => werkzeug('eslint', 'bin/eslint.js', ['.', '--max-warnings', '0']), + }, + { + name: 'Typen (TypeScript)', + tun: () => werkzeug('typescript', 'bin/tsc', ['--noEmit']), + }, + { name: 'Tests (Vitest)', tun: () => werkzeug('vitest', 'vitest.mjs', ['run']) }, + { name: 'Shellskripte (ShellCheck)', tun: shellskriptePruefen }, +] + +// Ohne erzeugten Prisma-Client schlagen Typen und Tests fehl. +if (!existsSync(join(WURZEL, 'src', 'generated', 'prisma', 'client.ts'))) { + console.log('▸ Prisma-Client fehlt, erzeuge ihn') + if (werkzeug('prisma', 'build/index.js', ['generate']) !== 0) process.exit(1) +} + +let gefallen = null +for (const schritt of schritte) { + console.log(`▸ ${schritt.name}`) + if (schritt.tun() !== 0) { + gefallen = schritt.name + break + } +} + +if (gefallen) { + console.error(`✗ Gescheitert: ${gefallen}`) + process.exit(1) +} + +console.log('✓ Alle Pruefungen bestanden.') diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..e0a60a8 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Setzt eine neue Version: package.json, CHANGELOG, Commit und Tag. +# Pusht bewusst nicht – das geschieht erst nach ausdrücklicher Freigabe. +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$WURZEL" + +meldung() { printf '▸ %s\n' "$1"; } +fehler() { + printf 'Fehler: %s\n' "$1" >&2 + exit 1 +} + +hilfe() { + cat <<'ENDE' +Verwendung: scripts/release.sh + + Semantische Version ohne führendes v, z. B. 0.1.0 + +Ablauf: + 1. Sauberen Arbeitsbaum und grüne Prüfungen sicherstellen + 2. Version in package.json setzen + 3. "Unveröffentlicht" im CHANGELOG in einen datierten Abschnitt überführen + 4. chore(release): committen und Tag v anlegen + +Es wird nicht gepusht. +ENDE +} + +VERSION="${1:-}" +case "$VERSION" in + ''|--hilfe|-h) hilfe; [ -z "$VERSION" ] && exit 1; exit 0 ;; +esac + +printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \ + || fehler "\"$VERSION\" sieht nicht nach einer semantischen Version aus (z. B. 0.1.0)." + +TAG="v$VERSION" + +# --- 1. Voraussetzungen ---------------------------------------------------- +meldung 'Arbeitsbaum prüfen.' +[ -d .git ] || fehler 'Kein Git-Arbeitsbaum.' +[ -z "$(git status --porcelain)" ] || fehler 'Der Arbeitsbaum ist nicht sauber.' + +git rev-parse -q --verify "refs/tags/$TAG" >/dev/null \ + && fehler "Den Tag $TAG gibt es schon." + +[ -f CHANGELOG.md ] || fehler 'CHANGELOG.md fehlt.' +grep -q '^## \[Unveröffentlicht\]' CHANGELOG.md \ + || fehler 'Im CHANGELOG fehlt der Abschnitt "## [Unveröffentlicht]".' + +INHALT="$(awk ' + /^## \[Unveröffentlicht\]/ { sammeln = 1; next } + sammeln && /^## / { exit } + sammeln { print } +' CHANGELOG.md | grep -v '^[[:space:]]*$' || true)" +[ -n "$INHALT" ] || fehler 'Der Abschnitt "Unveröffentlicht" ist leer.' + +meldung 'Prüfungen laufen lassen.' +npm run pruefen || fehler 'Die Prüfungen sind nicht grün.' + +# --- 2. Version setzen ----------------------------------------------------- +meldung "Version auf $VERSION setzen." +npm version "$VERSION" --no-git-tag-version --allow-same-version >/dev/null + +# --- 3. CHANGELOG ---------------------------------------------------------- +meldung 'CHANGELOG fortschreiben.' +HEUTE="$(date '+%Y-%m-%d')" +TEMP="$(mktemp)" +awk -v version="$VERSION" -v heute="$HEUTE" ' + /^## \[Unveröffentlicht\]/ { + print "## [Unveröffentlicht]" + print "" + print "## [" version "] – " heute + next + } + { print } +' CHANGELOG.md > "$TEMP" +mv "$TEMP" CHANGELOG.md + +npx prettier --write package.json CHANGELOG.md >/dev/null 2>&1 || true + +# --- 4. Commit und Tag ----------------------------------------------------- +meldung "Commit und Tag $TAG anlegen." +git add package.json package-lock.json CHANGELOG.md +git commit -m "chore(release): $VERSION" +git tag -a "$TAG" -m "Planer $VERSION" + +meldung "Fertig. $TAG ist angelegt, aber nicht gepusht." +printf '\nNach der Freigabe:\n' +printf ' git push origin %s\n' "$(git rev-parse --abbrev-ref HEAD)" +printf ' git push origin %s\n' "$TAG" diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..ede8135 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,142 @@ +/** + * Nimmt die Bilder für das Handbuch auf. + * + * Startet eine Wegwerf-Instanz in einem eigenen Compose-Projekt mit eigener + * Datenbank, spielt ausschließlich erfundene Daten ein, fährt die Seiten mit + * Playwright ab und räumt am Ende alles wieder weg — auch wenn unterwegs etwas + * schiefgeht. + * + * An eine Instanz mit echten Daten kommt dieser Ablauf nicht heran: die + * Datenbank entsteht neu, wird nur über ihren eigenen Port angesprochen und + * samt Volume wieder entfernt. + * + * Aufruf: npm run screenshots + */ +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const WURZEL = fileURLToPath(new URL('..', import.meta.url)) +const PROJEKT = 'planer-screenshots' +const COMPOSE_DATEI = 'docker-compose.screenshots.yml' + +const APP_PORT = process.env.SCREENSHOT_APP_PORT ?? '3200' +const DB_PORT = process.env.SCREENSHOT_DB_PORT ?? '5455' + +const UMGEBUNG = { + ...process.env, + SCREENSHOT_APP_PORT: APP_PORT, + SCREENSHOT_DB_PORT: DB_PORT, +} + +function meldung(text: string): void { + console.log(`▸ ${text}`) +} + +function laufen( + befehl: string, + argumente: string[], + zusatz: Record = {}, +): number { + const ergebnis = spawnSync(befehl, argumente, { + cwd: WURZEL, + stdio: 'inherit', + env: { ...UMGEBUNG, ...zusatz }, + }) + return ergebnis.status ?? 1 +} + +function compose(argumente: string[]): number { + return laufen('docker', ['compose', '-p', PROJEKT, '-f', COMPOSE_DATEI, ...argumente]) +} + +function node(skript: string, zusatz: Record): number { + return laufen( + process.execPath, + [`${WURZEL}node_modules/tsx/dist/cli.mjs`, skript], + zusatz, + ) +} + +async function wartenAufGesundheit(sekunden: number): Promise { + const ziel = `http://127.0.0.1:${APP_PORT}/api/health` + const frist = Date.now() + sekunden * 1000 + + while (Date.now() < frist) { + try { + const antwort = await fetch(ziel) + if (antwort.ok) { + const inhalt = (await antwort.json()) as { status?: string } + if (inhalt.status === 'ok') return true + } + } catch { + // Noch nicht da, gleich noch einmal. + } + await new Promise((weiter) => setTimeout(weiter, 2000)) + } + return false +} + +function aufraeumen(): void { + meldung('Wegwerf-Instanz entfernen.') + compose(['down', '-v', '--remove-orphans']) +} + +async function main(): Promise { + if (laufen('docker', ['info']) !== 0) { + console.error('Der Docker-Daemon läuft nicht.') + return 1 + } + + // Reste eines abgebrochenen Laufs zuerst wegräumen. + compose(['down', '-v', '--remove-orphans']) + + meldung(`Wegwerf-Instanz bauen und starten (App auf ${APP_PORT}, DB auf ${DB_PORT}).`) + if (compose(['up', '-d', '--build']) !== 0) { + console.error('Die Wegwerf-Instanz ließ sich nicht starten.') + return 1 + } + + meldung('Auf die Anwendung warten.') + if (!(await wartenAufGesundheit(180))) { + console.error('Die Anwendung ist nicht gesund geworden.') + compose(['logs', '--tail', '50', 'app']) + return 1 + } + + meldung('Erfundene Daten einspielen.') + const demoUrl = `postgresql://demo:demo@127.0.0.1:${DB_PORT}/demo?schema=public` + if (node('prisma/demo-seed.ts', { DATABASE_URL: demoUrl, DEMO_MODUS: 'true' }) !== 0) { + console.error('Der Demo-Seed ist fehlgeschlagen.') + return 1 + } + + meldung('Bilder aufnehmen.') + const playwright = laufen( + process.execPath, + [ + `${WURZEL}node_modules/@playwright/test/cli.js`, + 'test', + '--config', + 'playwright.screenshots.config.ts', + ], + { SCREENSHOT_APP_PORT: APP_PORT }, + ) + if (playwright !== 0) { + console.error('Die Aufnahme ist fehlgeschlagen.') + return 1 + } + + meldung('Fertig. Die Bilder liegen in docs/bilder/.') + return 0 +} + +let code = 1 +try { + code = await main() +} catch (fehler) { + console.error('Der Screenshot-Lauf ist abgebrochen:', fehler) +} finally { + aufraeumen() +} + +process.exit(code) diff --git a/scripts/zeit.sh b/scripts/zeit.sh new file mode 100755 index 0000000..6d6d719 --- /dev/null +++ b/scripts/zeit.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Zeiterfassung für den Planer. +# Schreibt nach devlog/zeiten.csv: datum;start;ende;minuten;meilenstein;art;notiz +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CSV="$WURZEL/devlog/zeiten.csv" +KOPF='datum;start;ende;minuten;meilenstein;art;notiz' +OFFEN="$WURZEL/devlog/.zeit-offen" + +hilfe() { + cat <<'ENDE' +Verwendung: zeit.sh [argumente] + + start [Notiz] Eintrag öffnen (Art: claude) + stop [Notiz] Offenen Eintrag schließen + nachtrag + Eigene Zeit nachtragen + (Art: review, test, einrichtung) + summe [Meilenstein] Summen je Meilenstein und Art + status Zeigt, ob ein Eintrag offen ist + hilfe Diese Übersicht +ENDE +} + +fehler() { + printf 'Fehler: %s\n' "$1" >&2 + exit 1 +} + +# Semikolons und Zeilenumbrüche würden die CSV zerlegen. +saeubern() { + printf '%s' "$1" | tr '\n\r;' ' ' | sed 's/ */ /g; s/^ //; s/ $//' +} + +csv_anlegen() { + mkdir -p "$(dirname "$CSV")" + if [ ! -f "$CSV" ]; then + printf '%s\n' "$KOPF" > "$CSV" + fi +} + +# Minuten seit Mitternacht, damit auch ohne GNU date gerechnet werden kann. +zu_minuten() { + local uhr="$1" + local h="${uhr%%:*}" + local m="${uhr##*:}" + printf '%s' "$((10#$h * 60 + 10#$m))" +} + +befehl_start() { + local meilenstein="${1:-}" + [ -n "$meilenstein" ] || fehler 'Meilenstein fehlt. Beispiel: zeit.sh start M1 "Gerüst"' + shift + local notiz + notiz="$(saeubern "${*:-}")" + + if [ -f "$OFFEN" ]; then + local alt + alt="$(cat "$OFFEN")" + fehler "Es läuft bereits ein Eintrag ($alt). Erst 'zeit.sh stop'." + fi + + csv_anlegen + mkdir -p "$(dirname "$OFFEN")" + printf '%s;%s;%s;%s\n' "$(date '+%Y-%m-%d')" "$(date '+%H:%M')" \ + "$(saeubern "$meilenstein")" "$notiz" > "$OFFEN" + printf 'Gestartet: %s um %s Uhr%s\n' "$meilenstein" "$(date '+%H:%M')" \ + "${notiz:+ – $notiz}" +} + +befehl_stop() { + [ -f "$OFFEN" ] || fehler "Kein offener Eintrag. Erst 'zeit.sh start '." + + local zeile datum start meilenstein notiz_start + zeile="$(cat "$OFFEN")" + IFS=';' read -r datum start meilenstein notiz_start <<< "$zeile" + + local ende notiz + ende="$(date '+%H:%M')" + notiz="$(saeubern "${*:-}")" + [ -n "$notiz" ] || notiz="$notiz_start" + + local von bis minuten + von="$(zu_minuten "$start")" + bis="$(zu_minuten "$ende")" + minuten=$((bis - von)) + # Über Mitternacht hinweg. + [ "$minuten" -ge 0 ] || minuten=$((minuten + 1440)) + + csv_anlegen + printf '%s;%s;%s;%s;%s;claude;%s\n' \ + "$datum" "$start" "$ende" "$minuten" "$meilenstein" "$notiz" >> "$CSV" + rm -f "$OFFEN" + printf 'Gestoppt: %s, %s Minuten (%s–%s)\n' "$meilenstein" "$minuten" "$start" "$ende" +} + +befehl_nachtrag() { + local meilenstein="${1:-}" minuten="${2:-}" art="${3:-}" + [ -n "$meilenstein" ] || fehler 'Meilenstein fehlt.' + [ -n "$minuten" ] || fehler 'Minuten fehlen.' + [ -n "$art" ] || fehler 'Art fehlt (review, test, einrichtung).' + case "$minuten" in + ''|*[!0-9]*) fehler "Minuten müssen eine ganze Zahl sein, nicht '$minuten'." ;; + esac + shift 3 + local notiz + notiz="$(saeubern "${*:-}")" + [ -n "$notiz" ] || fehler 'Notiz fehlt.' + + csv_anlegen + printf '%s;;;%s;%s;%s;%s\n' \ + "$(date '+%Y-%m-%d')" "$minuten" "$(saeubern "$meilenstein")" \ + "$(saeubern "$art")" "$notiz" >> "$CSV" + printf 'Nachgetragen: %s Minuten auf %s (%s)\n' "$minuten" "$meilenstein" "$art" +} + +befehl_summe() { + local filter="${1:-}" + [ -f "$CSV" ] || fehler "Noch keine Zeiten erfasst ($CSV fehlt)." + + awk -F';' -v filter="$filter" ' + NR == 1 { next } + $5 == "" { next } + filter != "" && $5 != filter { next } + { + schluessel = $5 ";" $6 + summe[schluessel] += $4 + je_meilenstein[$5] += $4 + gesamt += $4 + if (!($5 in gesehen)) { gesehen[$5] = 1; reihenfolge[++anzahl] = $5 } + } + END { + if (gesamt == 0) { print "Keine Einträge gefunden."; exit } + printf "%-14s %-14s %10s %10s\n", "Meilenstein", "Art", "Minuten", "Stunden" + printf "%s\n", "------------------------------------------------------" + for (i = 1; i <= anzahl; i++) { + m = reihenfolge[i] + for (s in summe) { + split(s, teil, ";") + if (teil[1] == m) { + printf "%-14s %-14s %10d %10.2f\n", m, teil[2], summe[s], summe[s] / 60 + } + } + printf "%-14s %-14s %10d %10.2f\n", m, "= Summe", je_meilenstein[m], je_meilenstein[m] / 60 + printf "%s\n", "------------------------------------------------------" + } + printf "%-29s %10d %10.2f\n", "Gesamt", gesamt, gesamt / 60 + } + ' "$CSV" +} + +befehl_status() { + if [ -f "$OFFEN" ]; then + local zeile datum start meilenstein notiz + zeile="$(cat "$OFFEN")" + IFS=';' read -r datum start meilenstein notiz <<< "$zeile" + printf 'Offen seit %s %s Uhr: %s%s\n' "$datum" "$start" "$meilenstein" "${notiz:+ – $notiz}" + else + printf 'Kein offener Eintrag.\n' + fi +} + +befehl="${1:-hilfe}" +shift || true + +case "$befehl" in + start) befehl_start "$@" ;; + stop) befehl_stop "$@" ;; + nachtrag) befehl_nachtrag "$@" ;; + summe) befehl_summe "$@" ;; + status) befehl_status ;; + hilfe|-h|--hilfe|--help) hilfe ;; + *) fehler "Unbekannter Befehl '$befehl'. 'zeit.sh hilfe' zeigt die Übersicht." ;; +esac diff --git a/src/app/(app)/bereiche/bereich-formular.tsx b/src/app/(app)/bereiche/bereich-formular.tsx new file mode 100644 index 0000000..978d696 --- /dev/null +++ b/src/app/(app)/bereiche/bereich-formular.tsx @@ -0,0 +1,235 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Loader2, Trash2 } from 'lucide-react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { KAPAZITAET_FELDER, WOCHENTAGE } from '@/lib/bereich' +import { + bereichAendern, + bereichAnlegen, + bereichLoeschen, + type Rueckmeldung, +} from '@/server/aktionen/bereiche' + +type Werte = { + id: string + name: string + farbe: string + reihenfolge: number + aktiv: boolean + arbeitsfensterVon: string + arbeitsfensterBis: string +} & Record<(typeof KAPAZITAET_FELDER)[number], number> + +const LEER = { + name: '', + farbe: '#2563eb', + reihenfolge: 0, + aktiv: true, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '17:00', + kapazitaetMo: 0, + kapazitaetDi: 0, + kapazitaetMi: 0, + kapazitaetDo: 0, + kapazitaetFr: 0, + kapazitaetSa: 0, + kapazitaetSo: 0, +} satisfies Omit + +export function BereichFormular({ bereich }: { bereich?: Werte }) { + const [laeuft, starte] = useTransition() + const [felder, setFelder] = useState>({}) + const [offen, setOffen] = useState(!bereich) + const werte = bereich ?? LEER + + function verarbeite(antwort: Rueckmeldung, formular: HTMLFormElement) { + if (antwort.ok) { + setFelder({}) + toast.success(antwort.meldung) + if (!bereich) formular.reset() + return + } + setFelder(antwort.felder ?? {}) + toast.error(antwort.fehler) + } + + function absenden(ereignis: React.FormEvent) { + ereignis.preventDefault() + const formular = ereignis.currentTarget + const daten = new FormData(formular) + starte(async () => { + const antwort = bereich + ? await bereichAendern(bereich.id, daten) + : await bereichAnlegen(daten) + verarbeite(antwort, formular) + }) + } + + function loeschen() { + if (!bereich) return + const daten = new FormData() + daten.set('id', bereich.id) + starte(async () => { + const antwort = await bereichLoeschen(daten) + if (antwort.ok) toast.success(antwort.meldung) + else toast.error(antwort.fehler) + }) + } + + if (bereich && !offen) { + return ( + + ) + } + + return ( +
+
+
+ + + {felder.name && ( +

+ {felder.name} +

+ )} +
+ +
+ + +
+ +
+ + +
+ +
+ + + {felder.arbeitsfensterBis && ( +

{felder.arbeitsfensterBis}

+ )} +
+
+ +
+ Tageskapazität in Minuten +
+ {KAPAZITAET_FELDER.map((feld, i) => ( +
+ + +
+ ))} +
+ {felder.kapazitaetMo && ( +

{felder.kapazitaetMo}

+ )} +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + + {bereich && ( + <> + + + + )} +
+
+ ) +} diff --git a/src/app/(app)/bereiche/page.tsx b/src/app/(app)/bereiche/page.tsx new file mode 100644 index 0000000..b3e602e --- /dev/null +++ b/src/app/(app)/bereiche/page.tsx @@ -0,0 +1,124 @@ +import type { Metadata } from 'next' +import { BereichFormular } from '@/app/(app)/bereiche/bereich-formular' +import { Card, CardInhalt, CardKopf, CardTitel } from '@/components/ui/card' +import { KAPAZITAET_FELDER, WOCHENTAGE, wochenKapazitaet } from '@/lib/bereich' +import { istAdmin } from '@/lib/rollen' +import { alsDauer, alsUhrzeit } from '@/lib/zeit' +import { verlangeAnmeldung } from '@/server/berechtigung' +import { db } from '@/server/db' + +export const metadata: Metadata = { title: 'Bereiche' } + +export default async function BereicheSeite() { + const benutzer = await verlangeAnmeldung('/bereiche') + const darfAendern = istAdmin(benutzer.rolle) + + const bereiche = await db.bereich.findMany({ + orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }], + }) + + return ( +
+
+

Bereiche

+

+ Farbe, Arbeitsfenster und Tageskapazität je Lebensbereich. Die Kapazität + begrenzt später, wie viel an einem Tag eingeplant werden kann. +

+
+ + {bereiche.length === 0 ? ( + + + Noch keine Bereiche angelegt. + + + ) : ( +
    + {bereiche.map((bereich) => ( +
  • + + + + {bereich.name} + {!bereich.aktiv && ( + + stillgelegt + + )} + + + +
    +
    +
    Arbeitsfenster:
    +
    + {alsUhrzeit(bereich.arbeitsfensterVon)} bis{' '} + {alsUhrzeit(bereich.arbeitsfensterBis)} Uhr +
    +
    +
    +
    Woche gesamt:
    +
    {alsDauer(wochenKapazitaet(bereich))}
    +
    +
    + +
      + {KAPAZITAET_FELDER.map((feld, i) => ( +
    • + + {WOCHENTAGE[i]?.slice(0, 2)} + {' '} + {alsDauer(bereich[feld])} +
    • + ))} +
    + + {darfAendern && ( + + )} +
    +
    +
  • + ))} +
+ )} + + {darfAendern && ( + + + Neuen Bereich anlegen + + + + + + )} +
+ ) +} diff --git a/src/app/(app)/einstellungen/page.tsx b/src/app/(app)/einstellungen/page.tsx new file mode 100644 index 0000000..a47c835 --- /dev/null +++ b/src/app/(app)/einstellungen/page.tsx @@ -0,0 +1,77 @@ +import type { Metadata } from 'next' +import { PasskeyVerwaltung } from '@/app/(app)/einstellungen/passkey-verwaltung' +import { + Card, + CardBeschreibung, + CardInhalt, + CardKopf, + CardTitel, +} from '@/components/ui/card' +import { ROLLEN_NAME } from '@/lib/rollen' +import { alsDatumUhrzeit } from '@/lib/zeit' +import { verlangeAnmeldung } from '@/server/berechtigung' +import { db } from '@/server/db' + +export const metadata: Metadata = { title: 'Einstellungen' } + +export default async function EinstellungenSeite() { + const benutzer = await verlangeAnmeldung('/einstellungen') + + const passkeys = await db.passkey.findMany({ + where: { benutzerId: benutzer.id }, + orderBy: { erstelltAm: 'asc' }, + select: { + id: true, + bezeichnung: true, + erstelltAm: true, + zuletztBenutzt: true, + gesichert: true, + }, + }) + + return ( +
+
+

Einstellungen

+
+ + + + Konto + + +
+
Name
+
{benutzer.name ?? '—'}
+
E-Mail
+
{benutzer.email}
+
Rolle
+
{ROLLEN_NAME[benutzer.rolle]}
+
+
+
+ + + + Passkeys + + Mit einem Passkey meldest du dich per Fingerabdruck, Gesicht oder Geräte-PIN + an – ohne Passwort. Hinterlege am besten zwei, damit du bei Verlust eines + Geräts nicht ausgesperrt bist. + + + + ({ + id: p.id, + bezeichnung: p.bezeichnung, + erstelltAm: alsDatumUhrzeit(p.erstelltAm), + zuletztBenutzt: p.zuletztBenutzt ? alsDatumUhrzeit(p.zuletztBenutzt) : null, + gesichert: p.gesichert, + }))} + /> + + +
+ ) +} diff --git a/src/app/(app)/einstellungen/passkey-verwaltung.tsx b/src/app/(app)/einstellungen/passkey-verwaltung.tsx new file mode 100644 index 0000000..e7107fb --- /dev/null +++ b/src/app/(app)/einstellungen/passkey-verwaltung.tsx @@ -0,0 +1,153 @@ +'use client' + +import { useState, useTransition } from 'react' +import { useRouter } from 'next/navigation' +import { startRegistration } from '@simplewebauthn/browser' +import { Fingerprint, Loader2, Trash2 } from 'lucide-react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { passkeyEntfernen } from '@/server/aktionen/passkeys' + +export type PasskeyAnzeige = { + id: string + bezeichnung: string + erstelltAm: string + zuletztBenutzt: string | null + gesichert: boolean +} + +export function PasskeyVerwaltung({ passkeys }: { passkeys: PasskeyAnzeige[] }) { + const router = useRouter() + const [bezeichnung, setBezeichnung] = useState('') + const [legtAn, setLegtAn] = useState(false) + const [laeuft, starte] = useTransition() + + async function anlegen(ereignis: React.FormEvent) { + ereignis.preventDefault() + const name = bezeichnung.trim() + if (!name) { + toast.error('Bitte einen Namen für das Gerät angeben.') + return + } + + setLegtAn(true) + try { + const antwortOptionen = await fetch('/api/passkey/registrierung') + if (!antwortOptionen.ok) { + toast.error('Die Anfrage wurde abgelehnt.') + return + } + + const optionen = (await antwortOptionen.json()) as Parameters< + typeof startRegistration + >[0]['optionsJSON'] + + const antwort = await startRegistration({ optionsJSON: optionen }) + + const gespeichert = await fetch('/api/passkey/registrierung', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bezeichnung: name, antwort }), + }) + + const ergebnis = (await gespeichert.json()) as { fehler?: string } + if (!gespeichert.ok) { + toast.error(ergebnis.fehler ?? 'Der Passkey konnte nicht gespeichert werden.') + return + } + + toast.success(`Passkey „${name}" angelegt.`) + setBezeichnung('') + router.refresh() + } catch (fehler) { + // Bricht der Benutzer den Systemdialog ab, ist das kein Fehler. + if (fehler instanceof Error && fehler.name === 'NotAllowedError') return + if (fehler instanceof Error && fehler.name === 'InvalidStateError') { + toast.error('Dieses Gerät ist hier schon hinterlegt.') + return + } + toast.error('Der Passkey konnte nicht angelegt werden.') + } finally { + setLegtAn(false) + } + } + + function entfernen(id: string) { + const daten = new FormData() + daten.set('id', id) + starte(async () => { + const antwort = await passkeyEntfernen(daten) + if (antwort.ok) { + toast.success(antwort.meldung) + router.refresh() + } else { + toast.error(antwort.fehler) + } + }) + } + + return ( +
+ {passkeys.length === 0 ? ( +

+ Noch kein Passkey hinterlegt. +

+ ) : ( +
    + {passkeys.map((passkey) => ( +
  • + +
    +

    {passkey.bezeichnung}

    +

    + Angelegt {passkey.erstelltAm} + {passkey.zuletztBenutzt + ? ` · zuletzt benutzt ${passkey.zuletztBenutzt}` + : ' · noch nicht benutzt'} + {passkey.gesichert ? ' · geräteübergreifend' : ' · nur dieses Gerät'} +

    +
    + +
  • + ))} +
+ )} + +
void anlegen(e)} className="flex flex-wrap items-end gap-3"> +
+ + setBezeichnung(e.target.value)} + placeholder="z. B. Diensthandy" + maxLength={60} + disabled={legtAn} + /> +
+ +
+
+ ) +} diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..b59e22c --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,38 @@ +import { Kopfleiste } from '@/components/kopfleiste' +import { INSTANZ_NAME } from '@/lib/instanz' +import { versionAnzeige } from '@/lib/version' +import { angefragterPfad, verlangeAnmeldung } from '@/server/berechtigung' +import { aktuelleInstanz } from '@/server/instanz' + +/** + * Alles unterhalb von (app) setzt eine Anmeldung voraus. Der Riegel sitzt + * hier im Layout und zusaetzlich in jeder Server Action – nicht nur in der + * Oberflaeche. + */ +export default async function AngemeldetesLayout({ + children, +}: { + children: React.ReactNode +}) { + // Der Pfad kommt aus der Kopfzeile des Proxys, damit es nach der + // Anmeldung dort weitergeht, wo man hinwollte. + const benutzer = await verlangeAnmeldung(await angefragterPfad()) + const instanz = aktuelleInstanz() + + return ( +
+ + +
{children}
+ +
+ Planer {versionAnzeige()} · {INSTANZ_NAME[instanz]} +
+
+ ) +} diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx new file mode 100644 index 0000000..5ca34da --- /dev/null +++ b/src/app/(app)/page.tsx @@ -0,0 +1,81 @@ +import Link from 'next/link' +import { + Card, + CardBeschreibung, + CardInhalt, + CardKopf, + CardTitel, +} from '@/components/ui/card' +import { wochenKapazitaet } from '@/lib/bereich' +import { alsDatum, alsDauer } from '@/lib/zeit' +import { verlangeAnmeldung } from '@/server/berechtigung' +import { db } from '@/server/db' +import { jetzt } from '@/server/zeitgeber' + +export default async function StartSeite() { + const benutzer = await verlangeAnmeldung('/') + const heute = jetzt() + + const bereiche = await db.bereich.findMany({ + where: { aktiv: true }, + orderBy: [{ reihenfolge: 'asc' }, { name: 'asc' }], + }) + + const anrede = benutzer.name?.split(' ')[0] ?? benutzer.email + + return ( +
+
+

Guten Tag, {anrede}

+

{alsDatum(heute)}

+
+ +
+

Deine Bereiche

+ + {bereiche.length === 0 ? ( + + + Noch keine Bereiche angelegt.{' '} + + Jetzt einrichten + + + + ) : ( +
    + {bereiche.map((bereich) => ( +
  • + + + + {bereich.name} + + +

    + {alsDauer(wochenKapazitaet(bereich))} in der Woche eingeplant +

    +
    +
    +
  • + ))} +
+ )} +
+ + + + Was als Nächstes kommt + + Aufgaben und Ideen folgen mit M2, das Lesen der Kalender mit M3, die + Wochenplanung per Ziehen und Ablegen mit M4. + + + +
+ ) +} diff --git a/src/app/(app)/verwaltung/protokoll/page.tsx b/src/app/(app)/verwaltung/protokoll/page.tsx new file mode 100644 index 0000000..2f6d2f2 --- /dev/null +++ b/src/app/(app)/verwaltung/protokoll/page.tsx @@ -0,0 +1,91 @@ +import type { Metadata } from 'next' +import { Card, CardInhalt } from '@/components/ui/card' +import { alsDatumUhrzeit } from '@/lib/zeit' +import { verlangeRolle } from '@/server/berechtigung' +import { db } from '@/server/db' + +export const metadata: Metadata = { title: 'Protokoll' } + +const SEITENGROESSE = 100 + +export default async function ProtokollSeite() { + // Zweiter Riegel: das Layout prueft die Anmeldung, hier zusaetzlich die Rolle. + await verlangeRolle('ADMIN', '/verwaltung/protokoll') + + const eintraege = await db.auditEintrag.findMany({ + orderBy: { zeitpunkt: 'desc' }, + take: SEITENGROESSE, + select: { + id: true, + zeitpunkt: true, + benutzerEmail: true, + aktion: true, + objektTyp: true, + objektId: true, + details: true, + instanz: true, + }, + }) + + return ( +
+
+

Protokoll

+

+ Die letzten {SEITENGROESSE} schreibenden Aktionen. Einträge lassen sich nicht + ändern und nicht löschen. +

+
+ + {eintraege.length === 0 ? ( + + + Noch nichts protokolliert. + + + ) : ( + + + + + + + + + + + + + {eintraege.map((eintrag) => ( + + + + + + + ))} + +
+ Zeitpunkt + + Wer + + Was + + Objekt +
+ {alsDatumUhrzeit(eintrag.zeitpunkt)} + {eintrag.benutzerEmail}{eintrag.aktion} + {eintrag.objektTyp} + {eintrag.details ? ( + + {JSON.stringify(eintrag.details)} + + ) : null} +
+
+
+ )} +
+ ) +} diff --git a/src/app/anmelden/anmelde-formular.tsx b/src/app/anmelden/anmelde-formular.tsx new file mode 100644 index 0000000..90f4014 --- /dev/null +++ b/src/app/anmelden/anmelde-formular.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { startAuthentication } from '@simplewebauthn/browser' +import { Fingerprint, KeyRound, Loader2 } from 'lucide-react' +import { signIn } from 'next-auth/react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Card, CardInhalt } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Separator } from '@/components/ui/separator' + +type Eigenschaften = { + ziel: string + oidcAktiv: boolean + oidcName: string + passwortAktiv: boolean + fehlerAusUrl?: string | undefined +} + +/** + * Bewusst dieselbe Meldung für „gibt es nicht", „falsches Passwort" und + * „gesperrt". Sonst lässt sich abfragen, wer hier ein Konto hat. + */ +const ABGELEHNT = 'Anmeldung nicht möglich. Bitte Angaben prüfen.' + +export function AnmeldeFormular({ + ziel, + oidcAktiv, + oidcName, + passwortAktiv, + fehlerAusUrl, +}: Eigenschaften) { + const router = useRouter() + const [email, setEmail] = useState('') + const [passwort, setPasswort] = useState('') + const [laeuft, setLaeuft] = useState(null) + + useEffect(() => { + if (fehlerAusUrl) toast.error(ABGELEHNT) + }, [fehlerAusUrl]) + + async function mitOidc() { + setLaeuft('oidc') + await signIn('oidc', { callbackUrl: ziel }) + } + + async function mitPasskey(ereignis: React.FormEvent) { + ereignis.preventDefault() + if (!email.trim()) { + toast.error('Bitte zuerst die E-Mail-Adresse eintragen.') + return + } + + setLaeuft('passkey') + try { + const antwortOptionen = await fetch('/api/passkey/anmeldung', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + if (!antwortOptionen.ok) { + toast.error(ABGELEHNT) + return + } + + const optionen = (await antwortOptionen.json()) as Parameters< + typeof startAuthentication + >[0]['optionsJSON'] + + const antwort = await startAuthentication({ optionsJSON: optionen }) + + const ergebnis = await signIn('passkey', { + email, + antwort: JSON.stringify(antwort), + redirect: false, + }) + + if (ergebnis?.error) { + toast.error(ABGELEHNT) + return + } + router.push(ziel) + router.refresh() + } catch (fehler) { + // Bricht der Benutzer den Systemdialog ab, ist das kein Fehler. + if (fehler instanceof Error && fehler.name === 'NotAllowedError') return + toast.error('Die Anmeldung mit Passkey hat nicht geklappt.') + } finally { + setLaeuft(null) + } + } + + async function mitPasswort(ereignis: React.FormEvent) { + ereignis.preventDefault() + setLaeuft('passwort') + try { + const ergebnis = await signIn('passwort', { + email, + passwort, + redirect: false, + }) + if (ergebnis?.error) { + toast.error(ABGELEHNT) + return + } + router.push(ziel) + router.refresh() + } finally { + setLaeuft(null) + } + } + + const gesperrt = laeuft !== null + + return ( + + + {oidcAktiv && ( + <> + +
+ + oder + +
+ + )} + +
void mitPasskey(e)}> +
+ + setEmail(e.target.value)} + disabled={gesperrt} + /> +
+ + +
+ + {passwortAktiv && ( +
void mitPasswort(e)}> +
+ + setPasswort(e.target.value)} + disabled={gesperrt} + /> +
+ +
+ )} +
+
+ ) +} diff --git a/src/app/anmelden/page.tsx b/src/app/anmelden/page.tsx new file mode 100644 index 0000000..3d2683b --- /dev/null +++ b/src/app/anmelden/page.tsx @@ -0,0 +1,56 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' +import { AnmeldeFormular } from '@/app/anmelden/anmelde-formular' +import { INSTANZ_NAME } from '@/lib/instanz' +import { versionAnzeige } from '@/lib/version' +import { angemeldet } from '@/server/berechtigung' +import { aktuelleInstanz } from '@/server/instanz' +import { umgebung } from '@/server/umgebung' + +export const metadata: Metadata = { title: 'Anmelden' } + +/** Nur Pfade innerhalb der App sind als Ziel erlaubt – keine fremden Adressen. */ +function sicheresZiel(weiter: string | undefined): string { + if (!weiter) return '/' + if (!weiter.startsWith('/') || weiter.startsWith('//')) return '/' + return weiter +} + +export default async function AnmeldeSeite({ + searchParams, +}: { + searchParams: Promise<{ weiter?: string; error?: string }> +}) { + const parameter = await searchParams + const ziel = sicheresZiel(parameter.weiter) + + if (await angemeldet()) { + redirect(ziel) + } + + const einst = umgebung() + const instanz = aktuelleInstanz() + + return ( +
+
+
+

Planer

+

+ {INSTANZ_NAME[instanz]} +

+
+ + +
+ +

{versionAnzeige()}

+
+ ) +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7ed0211 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@/server/auth' + +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..89e1881 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import { VERSION } from '@/lib/version' +import { db } from '@/server/db' +import { aktuelleInstanz } from '@/server/instanz' + +export const dynamic = 'force-dynamic' + +/** + * Auskunft fuer Betrieb und update.sh. Bewusst ohne Anmeldung, deshalb steht + * hier nichts, was nicht ohnehin oeffentlich sein darf – insbesondere keine + * Zaehlstaende, Namen oder Adressen. + */ +export async function GET() { + let datenbank: 'ok' | 'fehler' = 'ok' + try { + await db.$queryRaw`SELECT 1` + } catch { + datenbank = 'fehler' + } + + const status = datenbank === 'ok' ? 'ok' : 'fehler' + + return NextResponse.json( + { + status, + version: VERSION, + instanz: aktuelleInstanz(), + datenbank, + zeit: new Date().toISOString(), + }, + { + status: status === 'ok' ? 200 : 503, + headers: { 'Cache-Control': 'no-store' }, + }, + ) +} diff --git a/src/app/api/passkey/anmeldung/route.ts b/src/app/api/passkey/anmeldung/route.ts new file mode 100644 index 0000000..cbcc186 --- /dev/null +++ b/src/app/api/passkey/anmeldung/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { anmeldeOptionen } from '@/server/auth/passkey' + +export const dynamic = 'force-dynamic' + +const eingabe = z.object({ email: z.email() }) + +/** + * Liefert die WebAuthn-Optionen fuer die Anmeldung. + * + * Antwortet fuer unbekannte Adressen genauso wie fuer bekannte – sonst + * liesse sich hier abfragen, wer ein Konto hat. + */ +export async function POST(anfrage: Request) { + let inhalt: unknown + try { + inhalt = await anfrage.json() + } catch { + return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 }) + } + + const gelesen = eingabe.safeParse(inhalt) + if (!gelesen.success) { + return NextResponse.json( + { fehler: 'Bitte eine gültige E-Mail-Adresse angeben.' }, + { status: 400 }, + ) + } + + const optionen = await anmeldeOptionen(gelesen.data.email) + return NextResponse.json(optionen, { headers: { 'Cache-Control': 'no-store' } }) +} diff --git a/src/app/api/passkey/registrierung/route.ts b/src/app/api/passkey/registrierung/route.ts new file mode 100644 index 0000000..392167f --- /dev/null +++ b/src/app/api/passkey/registrierung/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' +import { z } from 'zod' +import type { RegistrationResponseJSON } from '@simplewebauthn/server' +import { auditSchreiben } from '@/server/audit' +import { registrierungPruefen, registrierungsOptionen } from '@/server/auth/passkey' +import { verlangeAnmeldungFuerApi } from '@/server/berechtigung' + +export const dynamic = 'force-dynamic' + +const abschluss = z.object({ + bezeichnung: z.string().trim().min(1).max(60), + antwort: z.looseObject({ id: z.string().min(1) }), +}) + +/** Schritt 1: Optionen fuer einen neuen Passkey des angemeldeten Benutzers. */ +export async function GET() { + const benutzer = await verlangeAnmeldungFuerApi() + if (!benutzer) { + return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 }) + } + + const optionen = await registrierungsOptionen(benutzer.id) + return NextResponse.json(optionen, { headers: { 'Cache-Control': 'no-store' } }) +} + +/** Schritt 2: Antwort des Browsers pruefen und den Passkey speichern. */ +export async function POST(anfrage: Request) { + const benutzer = await verlangeAnmeldungFuerApi() + if (!benutzer) { + return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 }) + } + + let inhalt: unknown + try { + inhalt = await anfrage.json() + } catch { + return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 }) + } + + const gelesen = abschluss.safeParse(inhalt) + if (!gelesen.success) { + return NextResponse.json( + { fehler: 'Bitte einen Namen für das Gerät angeben.' }, + { status: 400 }, + ) + } + + try { + const ergebnis = await registrierungPruefen( + benutzer.id, + gelesen.data.antwort as unknown as RegistrationResponseJSON, + gelesen.data.bezeichnung, + ) + + await auditSchreiben({ + akteur: { id: benutzer.id, email: benutzer.email }, + aktion: 'passkey.angelegt', + objektTyp: 'Passkey', + objektId: ergebnis.passkeyId, + details: { bezeichnung: ergebnis.bezeichnung }, + }) + + return NextResponse.json({ ok: true, bezeichnung: ergebnis.bezeichnung }) + } catch (fehler) { + const meldung = + fehler instanceof Error + ? fehler.message + : 'Der Passkey konnte nicht gespeichert werden.' + return NextResponse.json({ fehler: meldung }, { status: 400 }) + } +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..24f0a63 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,90 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +/* + * Farben als OKLCH, damit Hell und Dunkel dieselbe wahrgenommene Helligkeit + * halten. Die Bereichsfarben kommen aus der Datenbank und werden zur Laufzeit + * gesetzt, nicht hier. + */ +:root { + --radius: 0.625rem; + + --hintergrund: oklch(0.99 0.002 250); + --vordergrund: oklch(0.21 0.02 260); + --karte: oklch(1 0 0); + --karte-vordergrund: oklch(0.21 0.02 260); + --gedaempft: oklch(0.96 0.004 250); + --gedaempft-vordergrund: oklch(0.5 0.015 260); + --rand: oklch(0.91 0.006 255); + --eingabe: oklch(0.91 0.006 255); + --ring: oklch(0.55 0.16 258); + + --betont: oklch(0.52 0.17 258); + --betont-vordergrund: oklch(0.99 0.002 250); + --gefahr: oklch(0.55 0.21 27); + --gefahr-vordergrund: oklch(0.99 0.002 250); + --erfolg: oklch(0.6 0.13 155); + --warnung: oklch(0.7 0.15 75); +} + +.dark { + --hintergrund: oklch(0.17 0.015 262); + --vordergrund: oklch(0.95 0.005 250); + --karte: oklch(0.21 0.018 262); + --karte-vordergrund: oklch(0.95 0.005 250); + --gedaempft: oklch(0.26 0.018 262); + --gedaempft-vordergrund: oklch(0.7 0.015 258); + --rand: oklch(0.31 0.018 262); + --eingabe: oklch(0.31 0.018 262); + --ring: oklch(0.62 0.15 258); + + --betont: oklch(0.62 0.16 258); + --betont-vordergrund: oklch(0.17 0.015 262); + --gefahr: oklch(0.62 0.19 25); + --gefahr-vordergrund: oklch(0.98 0.002 250); + --erfolg: oklch(0.68 0.12 155); + --warnung: oklch(0.78 0.13 75); +} + +@theme inline { + --color-hintergrund: var(--hintergrund); + --color-vordergrund: var(--vordergrund); + --color-karte: var(--karte); + --color-karte-vordergrund: var(--karte-vordergrund); + --color-gedaempft: var(--gedaempft); + --color-gedaempft-vordergrund: var(--gedaempft-vordergrund); + --color-rand: var(--rand); + --color-eingabe: var(--eingabe); + --color-ring: var(--ring); + --color-betont: var(--betont); + --color-betont-vordergrund: var(--betont-vordergrund); + --color-gefahr: var(--gefahr); + --color-gefahr-vordergrund: var(--gefahr-vordergrund); + --color-erfolg: var(--erfolg); + --color-warnung: var(--warnung); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +@layer base { + * { + @apply border-rand outline-ring/50; + } + + body { + @apply bg-hintergrund text-vordergrund; + font-feature-settings: + 'liga' 1, + 'calt' 1; + } + + /* Wer mit der Tastatur bedient, muss sehen, wo er ist. */ + :focus-visible { + @apply outline-ring outline-2 outline-offset-2; + } +} diff --git a/src/app/keine-berechtigung/page.tsx b/src/app/keine-berechtigung/page.tsx new file mode 100644 index 0000000..bba52ca --- /dev/null +++ b/src/app/keine-berechtigung/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { ShieldAlert } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export const metadata: Metadata = { title: 'Keine Berechtigung' } + +export default function KeineBerechtigungSeite() { + return ( +
+ +

Dazu fehlt dir die Berechtigung

+

+ Diese Seite ist der Verwaltung vorbehalten. Wenn du meinst, dass das ein Versehen + ist, wende dich an die Verwaltung dieser Instanz. +

+ +
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..29b3fac --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata, Viewport } from 'next' +import { Toaster } from 'sonner' +import { FarbschemaAnbieter } from '@/components/farbschema-anbieter' +import './globals.css' + +export const metadata: Metadata = { + title: { + default: 'Planer', + template: '%s – Planer', + }, + description: 'Aufgaben, Ideen, Projekte und Zeitblöcke an einem Ort.', + applicationName: 'Planer', + robots: { index: false, follow: false }, +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#fbfbfd' }, + { media: '(prefers-color-scheme: dark)', color: '#1a1c22' }, + ], +} + +export default function WurzelLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + + ) +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..0641832 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { Button } from '@/components/ui/button' + +export const metadata: Metadata = { title: 'Nicht gefunden' } + +export default function NichtGefunden() { + return ( +
+

Fehler 404

+

Diese Seite gibt es nicht

+

+ Möglicherweise ist sie in dieser Instanz auch gar nicht vorgesehen. +

+ +
+ ) +} diff --git a/src/components/farbschema-anbieter.tsx b/src/components/farbschema-anbieter.tsx new file mode 100644 index 0000000..afdbc20 --- /dev/null +++ b/src/components/farbschema-anbieter.tsx @@ -0,0 +1,20 @@ +'use client' + +import { ThemeProvider } from 'next-themes' + +/** + * Hell, Dunkel oder nach Systemeinstellung. Die Wahl liegt im Browser des + * Benutzers, nicht in der Datenbank. + */ +export function FarbschemaAnbieter({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/src/components/kopfleiste.tsx b/src/components/kopfleiste.tsx new file mode 100644 index 0000000..7cff907 --- /dev/null +++ b/src/components/kopfleiste.tsx @@ -0,0 +1,117 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useTheme } from 'next-themes' +import { LogOut, Moon, Shield, Sun } from 'lucide-react' +import { signOut } from 'next-auth/react' +import type { Rolle } from '@/generated/prisma/enums' +import { ROLLEN_NAME, istAdmin } from '@/lib/rollen' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' + +type Eintrag = { pfad: string; name: string; nurAdmin?: boolean } + +const EINTRAEGE: Eintrag[] = [ + { pfad: '/', name: 'Start' }, + { pfad: '/bereiche', name: 'Bereiche' }, + { pfad: '/einstellungen', name: 'Einstellungen' }, + { pfad: '/verwaltung/protokoll', name: 'Protokoll', nurAdmin: true }, +] + +export function Kopfleiste({ + name, + email, + rolle, + instanzName, +}: { + name: string + email: string + rolle: Rolle + instanzName: string +}) { + const pfad = usePathname() + const sichtbar = EINTRAEGE.filter((e) => !e.nurAdmin || istAdmin(rolle)) + + return ( +
+ {/* + Bewusst eine einzige Zeile auf allen Breiten: Wird es eng, scrollt die + Navigation waagerecht, statt umzubrechen und den Namen der Anwendung in + die zweite Zeile zu drängen. + */} +
+ + Planer + + + + +
+ + + {name} + + +
+
+
+ ) +} + +function FarbschemaKnopf() { + const { resolvedTheme, setTheme } = useTheme() + + // Welches Symbol gilt, entscheidet die dark-Klasse und damit CSS. next-themes + // setzt sie schon vor der Hydratation, deshalb braucht es hier weder einen + // Effekt noch einen Zustand – und es blitzt nichts auf. + return ( + + ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..557e2c3 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,47 @@ +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const knopfStile = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0", + { + variants: { + art: { + betont: 'bg-betont text-betont-vordergrund hover:bg-betont/90', + umriss: 'border border-rand bg-karte hover:bg-gedaempft', + leise: 'hover:bg-gedaempft', + gefahr: 'bg-gefahr text-gefahr-vordergrund hover:bg-gefahr/90', + verweis: 'text-betont underline-offset-4 hover:underline', + }, + groesse: { + normal: 'h-9 px-4 py-2', + klein: 'h-8 rounded-md px-3 text-xs', + gross: 'h-11 rounded-md px-6', + quadrat: 'size-9', + }, + }, + defaultVariants: { + art: 'betont', + groesse: 'normal', + }, + }, +) + +export type KnopfEigenschaften = React.ComponentProps<'button'> & + VariantProps & { + /** Rendert stattdessen das Kindelement, z. B. einen Link. */ + alsKind?: boolean + } + +function Button({ + className, + art, + groesse, + alsKind = false, + ...rest +}: KnopfEigenschaften) { + const Element = alsKind ? Slot : 'button' + return +} + +export { Button, knopfStile } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..eb75234 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,40 @@ +import { cn } from '@/lib/utils' + +function Card({ className, ...rest }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardKopf({ className, ...rest }: React.ComponentProps<'div'>) { + return
+} + +function CardTitel({ className, ...rest }: React.ComponentProps<'h2'>) { + return ( +

+ ) +} + +function CardBeschreibung({ className, ...rest }: React.ComponentProps<'p'>) { + return

+} + +function CardInhalt({ className, ...rest }: React.ComponentProps<'div'>) { + return

+} + +function CardFuss({ className, ...rest }: React.ComponentProps<'div'>) { + return
+} + +export { Card, CardKopf, CardTitel, CardBeschreibung, CardInhalt, CardFuss } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..450e8bc --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import { cn } from '@/lib/utils' + +function Input({ className, type, ...rest }: React.ComponentProps<'input'>) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..b8a8beb --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +'use client' + +import * as LabelPrimitive from '@radix-ui/react-label' +import { cn } from '@/lib/utils' + +function Label({ className, ...rest }: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..6110590 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +'use client' + +import * as SeparatorPrimitive from '@radix-ui/react-separator' +import { cn } from '@/lib/utils' + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...rest +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/lib/audit.test.ts b/src/lib/audit.test.ts new file mode 100644 index 0000000..6dfbef1 --- /dev/null +++ b/src/lib/audit.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest' +import { ENTFERNT, auditDetails, detailsSaeubern } from './audit' + +describe('auditDetails', () => { + it('laesst fachliche Felder unangetastet', () => { + expect(auditDetails({ name: 'Gewerbe', farbe: '#2563eb', reihenfolge: 2 })).toEqual({ + name: 'Gewerbe', + farbe: '#2563eb', + reihenfolge: 2, + }) + }) + + it('gibt fuer nichts auch nichts zurueck', () => { + expect(auditDetails(undefined)).toBeUndefined() + }) + + it('entfernt Passwoerter und Tokens', () => { + expect( + auditDetails({ + email: 'a@b.de', + passwort: 'geheim123', + passwortHash: '$2b$12$...', + access_token: 'ey...', + }), + ).toEqual({ + email: 'a@b.de', + passwort: ENTFERNT, + passwortHash: ENTFERNT, + access_token: ENTFERNT, + }) + }) + + it('entfernt Termin-Betreffs und Mailinhalte', () => { + expect( + auditDetails({ betreff: 'Personalgespräch', body: 'Text', dauer: 30 }), + ).toEqual({ betreff: ENTFERNT, body: ENTFERNT, dauer: 30 }) + }) + + it('achtet nicht auf Gross- und Kleinschreibung', () => { + expect(auditDetails({ ClientSecret: 'x', REFRESH_TOKEN: 'y' })).toEqual({ + ClientSecret: ENTFERNT, + REFRESH_TOKEN: ENTFERNT, + }) + }) +}) + +describe('detailsSaeubern', () => { + it('greift auch in verschachtelten Objekten', () => { + expect(detailsSaeubern({ konto: { anbieter: 'GRAPH', id_token: 'ey...' } })).toEqual({ + konto: { anbieter: 'GRAPH', id_token: ENTFERNT }, + }) + }) + + it('greift in Listen', () => { + expect(detailsSaeubern([{ name: 'a', token: 'x' }])).toEqual([ + { name: 'a', token: ENTFERNT }, + ]) + }) + + it('schreibt Datumsangaben als ISO-Zeichenkette', () => { + expect(detailsSaeubern({ am: new Date('2026-09-10T12:00:00Z') })).toEqual({ + am: '2026-09-10T12:00:00.000Z', + }) + }) + + it('schneidet zu tiefe Verschachtelung ab, statt endlos abzusteigen', () => { + expect(detailsSaeubern({ a: { b: { c: { d: { e: { f: 1 } } } } } })).toEqual({ + a: { b: { c: { d: { e: ENTFERNT } } } }, + }) + }) + + it('kommt mit einfachen Werten und null zurecht', () => { + expect(detailsSaeubern('text')).toBe('text') + expect(detailsSaeubern(42)).toBe(42) + expect(detailsSaeubern(null)).toBeNull() + }) +}) diff --git a/src/lib/audit.ts b/src/lib/audit.ts new file mode 100644 index 0000000..f1d4a5a --- /dev/null +++ b/src/lib/audit.ts @@ -0,0 +1,66 @@ +/** + * Aufbereitung der Audit-Details. Reine Logik, damit sie ohne Datenbank + * geprueft werden kann. + * + * In die Details gehoeren nur fachliche Felder – niemals Tokens, Passwoerter, + * Mailinhalte oder Termin-Betreffs. Was hier landet, steht spaeter im + * Klartext in der Datenbank und in jeder Sicherung. + */ + +/** Feldnamen, die auch versehentlich nie in einem Audit-Eintrag landen duerfen. */ +const VERBOTEN = [ + 'passwort', + 'password', + 'token', + 'secret', + 'geheim', + 'clientsecret', + 'betreff', + 'subject', + 'inhalt', + 'body', + 'nachricht', +] + +export const ENTFERNT = '[entfernt]' + +function istVerdaechtig(schluessel: string): boolean { + const klein = schluessel.toLowerCase() + return VERBOTEN.some((v) => klein.includes(v)) +} + +/** + * Zweiter Riegel gegen Unachtsamkeit: Felder mit verdaechtigem Namen werden + * ersetzt, statt gespeichert zu werden. Wirkt auch in verschachtelten + * Objekten und in Listen. + */ +export function detailsSaeubern(wert: unknown, tiefe = 0): unknown { + // Tief verschachtelte Strukturen gehoeren nicht in ein Audit-Detail; ab + // einer gewissen Tiefe wird abgeschnitten statt endlos abgestiegen. + if (tiefe > 4) return ENTFERNT + + if (Array.isArray(wert)) { + return wert.map((eintrag) => detailsSaeubern(eintrag, tiefe + 1)) + } + + if (wert !== null && typeof wert === 'object') { + if (wert instanceof Date) return wert.toISOString() + const sauber: Record = {} + for (const [schluessel, inhalt] of Object.entries(wert)) { + sauber[schluessel] = istVerdaechtig(schluessel) + ? ENTFERNT + : detailsSaeubern(inhalt, tiefe + 1) + } + return sauber + } + + return wert +} + +/** Bequemer Einstieg fuer den Normalfall: ein flaches Objekt. */ +export function auditDetails( + details: Record | undefined, +): Record | undefined { + if (!details) return undefined + return detailsSaeubern(details) as Record +} diff --git a/src/lib/bereich.test.ts b/src/lib/bereich.test.ts new file mode 100644 index 0000000..b6292a2 --- /dev/null +++ b/src/lib/bereich.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest' +import { + KAPAZITAET_FELDER, + bereichSchema, + farbeSchema, + kapazitaetAmTag, + wochenKapazitaet, +} from './bereich' + +const gueltig = { + name: 'Gewerbe', + farbe: '#2563EB', + reihenfolge: 1, + aktiv: true, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '17:00', + kapazitaetMo: 240, + kapazitaetDi: 240, + kapazitaetMi: 240, + kapazitaetDo: 240, + kapazitaetFr: 120, + kapazitaetSa: 0, + kapazitaetSo: 0, +} + +describe('farbeSchema', () => { + it('nimmt sechsstellige und dreistellige Hex-Farben', () => { + expect(farbeSchema.parse('#2563eb')).toBe('#2563eb') + expect(farbeSchema.parse('#abc')).toBe('#abc') + }) + + it('macht Grossbuchstaben klein, damit Vergleiche stimmen', () => { + expect(farbeSchema.parse('#2563EB')).toBe('#2563eb') + }) + + it('weist Farben ohne Raute und mit falscher Laenge ab', () => { + expect(farbeSchema.safeParse('2563eb').success).toBe(false) + expect(farbeSchema.safeParse('#12345').success).toBe(false) + expect(farbeSchema.safeParse('blau').success).toBe(false) + }) +}) + +describe('bereichSchema', () => { + it('nimmt einen vollstaendigen Bereich an und rechnet Uhrzeiten in Minuten um', () => { + const ergebnis = bereichSchema.parse(gueltig) + expect(ergebnis.arbeitsfensterVon).toBe(480) + expect(ergebnis.arbeitsfensterBis).toBe(1020) + expect(ergebnis.farbe).toBe('#2563eb') + }) + + it('verlangt einen Namen mit mindestens zwei Zeichen', () => { + const ergebnis = bereichSchema.safeParse({ ...gueltig, name: 'A' }) + expect(ergebnis.success).toBe(false) + }) + + it('weist ein Arbeitsfenster ab, das vor seinem Beginn endet', () => { + const ergebnis = bereichSchema.safeParse({ + ...gueltig, + arbeitsfensterVon: '17:00', + arbeitsfensterBis: '08:00', + }) + expect(ergebnis.success).toBe(false) + if (!ergebnis.success) { + expect(ergebnis.error.issues[0]?.message).toMatch(/nach seinem Beginn enden/) + } + }) + + it('weist ein Arbeitsfenster ohne Dauer ab', () => { + const ergebnis = bereichSchema.safeParse({ + ...gueltig, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '08:00', + }) + expect(ergebnis.success).toBe(false) + }) + + it('weist eine Kapazitaet ab, die laenger ist als das Arbeitsfenster', () => { + // Fenster 08:00–17:00 sind 540 Minuten. + const ergebnis = bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 600 }) + expect(ergebnis.success).toBe(false) + if (!ergebnis.success) { + expect(ergebnis.error.issues[0]?.message).toMatch(/Arbeitsfenster/) + } + }) + + it('laesst eine Kapazitaet genau in Fensterlaenge zu', () => { + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 540 }).success).toBe(true) + }) + + it('weist negative und uebergrosse Kapazitaeten ab', () => { + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: -1 }).success).toBe(false) + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 1441 }).success).toBe( + false, + ) + }) + + it('weist eine unsinnige Uhrzeit ab', () => { + expect( + bereichSchema.safeParse({ ...gueltig, arbeitsfensterVon: '25:00' }).success, + ).toBe(false) + }) +}) + +describe('wochenKapazitaet', () => { + it('summiert alle sieben Tage', () => { + const bereich = bereichSchema.parse(gueltig) + expect(wochenKapazitaet(bereich)).toBe(1080) + }) + + it('ist null, wenn nichts eingeplant ist', () => { + const leer = Object.fromEntries(KAPAZITAET_FELDER.map((f) => [f, 0])) as Record< + (typeof KAPAZITAET_FELDER)[number], + number + > + expect(wochenKapazitaet(leer)).toBe(0) + }) +}) + +describe('kapazitaetAmTag', () => { + it('trifft den richtigen Wochentag, Montag ist null', () => { + const bereich = bereichSchema.parse(gueltig) + expect(kapazitaetAmTag(bereich, 0)).toBe(240) + expect(kapazitaetAmTag(bereich, 4)).toBe(120) + expect(kapazitaetAmTag(bereich, 6)).toBe(0) + }) + + it('gibt fuer einen Index ausserhalb der Woche null zurueck', () => { + const bereich = bereichSchema.parse(gueltig) + expect(kapazitaetAmTag(bereich, 7)).toBe(0) + expect(kapazitaetAmTag(bereich, -1)).toBe(0) + }) +}) diff --git a/src/lib/bereich.ts b/src/lib/bereich.ts new file mode 100644 index 0000000..cdc05b5 --- /dev/null +++ b/src/lib/bereich.ts @@ -0,0 +1,100 @@ +import { z } from 'zod' +import { ausUhrzeit } from '@/lib/zeit' + +/** + * Regeln fuer Bereiche. Reine Logik, damit Formular und Server Action + * dieselbe Antwort geben. + */ + +export const WOCHENTAGE = [ + 'Montag', + 'Dienstag', + 'Mittwoch', + 'Donnerstag', + 'Freitag', + 'Samstag', + 'Sonntag', +] as const + +export const KAPAZITAET_FELDER = [ + 'kapazitaetMo', + 'kapazitaetDi', + 'kapazitaetMi', + 'kapazitaetDo', + 'kapazitaetFr', + 'kapazitaetSa', + 'kapazitaetSo', +] as const + +export type KapazitaetFeld = (typeof KAPAZITAET_FELDER)[number] + +/** Hex-Farbe mit Raute, drei- oder sechsstellig. */ +export const farbeSchema = z + .string() + .trim() + .regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, 'Bitte eine Farbe wie #2563eb angeben.') + .transform((f) => f.toLowerCase()) + +const uhrzeitSchema = z + .string() + .trim() + .refine((w) => ausUhrzeit(w) !== null, 'Bitte eine Uhrzeit wie 08:00 angeben.') + .transform((w) => ausUhrzeit(w) as number) + +/** Eine Tageskapazitaet: 0 bis 24 Stunden, in Minuten. */ +const kapazitaetSchema = z.coerce + .number() + .int('Bitte ganze Minuten angeben.') + .min(0, 'Nicht weniger als 0 Minuten.') + .max(1440, 'Nicht mehr als 1440 Minuten, also 24 Stunden.') + +export const bereichSchema = z + .object({ + name: z + .string() + .trim() + .min(2, 'Mindestens zwei Zeichen.') + .max(40, 'Höchstens 40 Zeichen.'), + farbe: farbeSchema, + reihenfolge: z.coerce.number().int().min(0).max(999).default(0), + aktiv: z.coerce.boolean().default(true), + arbeitsfensterVon: uhrzeitSchema, + arbeitsfensterBis: uhrzeitSchema, + kapazitaetMo: kapazitaetSchema, + kapazitaetDi: kapazitaetSchema, + kapazitaetMi: kapazitaetSchema, + kapazitaetDo: kapazitaetSchema, + kapazitaetFr: kapazitaetSchema, + kapazitaetSa: kapazitaetSchema, + kapazitaetSo: kapazitaetSchema, + }) + .refine((b) => b.arbeitsfensterVon < b.arbeitsfensterBis, { + path: ['arbeitsfensterBis'], + message: 'Das Arbeitsfenster muss nach seinem Beginn enden.', + }) + .refine( + (b) => + KAPAZITAET_FELDER.every( + (feld) => b[feld] <= b.arbeitsfensterBis - b.arbeitsfensterVon, + ), + { + path: ['kapazitaetMo'], + message: 'Keine Tageskapazität darf länger sein als das Arbeitsfenster.', + }, + ) + +export type BereichEingabe = z.infer + +/** Summe der Wochenkapazitaet in Minuten. */ +export function wochenKapazitaet(bereich: Record): number { + return KAPAZITAET_FELDER.reduce((summe, feld) => summe + bereich[feld], 0) +} + +/** Kapazitaet des Wochentags, 0 = Montag. */ +export function kapazitaetAmTag( + bereich: Record, + wochentagIndex: number, +): number { + const feld = KAPAZITAET_FELDER[wochentagIndex] + return feld ? bereich[feld] : 0 +} diff --git a/src/lib/instanz.test.ts b/src/lib/instanz.test.ts new file mode 100644 index 0000000..0261a21 --- /dev/null +++ b/src/lib/instanz.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { + FAEHIGKEITEN, + INSTANZEN, + faehigkeitenVon, + istErlaubt, + istInstanz, + kalenderAnbieterVon, + leseInstanz, +} from './instanz' + +describe('leseInstanz', () => { + it('nimmt die erlaubten Werte an', () => { + expect(leseInstanz('firma')).toBe('firma') + expect(leseInstanz('daheim')).toBe('daheim') + }) + + it('ist unempfindlich gegen Gross- und Kleinschreibung und Leerzeichen', () => { + expect(leseInstanz(' FIRMA ')).toBe('firma') + }) + + it('bricht ab, wenn nichts gesetzt ist', () => { + expect(() => leseInstanz(undefined)).toThrow(/nicht gesetzt/) + expect(() => leseInstanz(' ')).toThrow(/nicht gesetzt/) + }) + + it('faellt bei einem Tippfehler nicht still auf einen Standard zurueck', () => { + expect(() => leseInstanz('fimra')).toThrow(/unbekannten Wert/) + }) +}) + +describe('istInstanz', () => { + it('erkennt gueltige Werte', () => { + expect(istInstanz('firma')).toBe(true) + expect(istInstanz('daheim')).toBe(true) + }) + + it('weist alles andere ab', () => { + expect(istInstanz('privat')).toBe(false) + expect(istInstanz(undefined)).toBe(false) + expect(istInstanz(42)).toBe(false) + }) +}) + +describe('istErlaubt', () => { + it('trennt die Kalenderanbieter der beiden Instanzen', () => { + expect(istErlaubt('firma', 'kalender:graph')).toBe(true) + expect(istErlaubt('firma', 'kalender:google')).toBe(false) + expect(istErlaubt('daheim', 'kalender:google')).toBe(true) + }) + + it('haelt die Integrationen auseinander', () => { + expect(istErlaubt('firma', 'integration:zammad')).toBe(true) + expect(istErlaubt('daheim', 'integration:zammad')).toBe(false) + expect(istErlaubt('daheim', 'integration:telegram')).toBe(true) + expect(istErlaubt('firma', 'integration:telegram')).toBe(false) + }) + + it('richtet den Spiegel in genau eine Richtung ein', () => { + expect(istErlaubt('firma', 'spiegel:senden')).toBe(true) + expect(istErlaubt('firma', 'spiegel:empfangen')).toBe(false) + expect(istErlaubt('daheim', 'spiegel:empfangen')).toBe(true) + expect(istErlaubt('daheim', 'spiegel:senden')).toBe(false) + }) +}) + +describe('faehigkeitenVon', () => { + it('liefert nur bekannte Faehigkeiten', () => { + for (const instanz of INSTANZEN) { + for (const faehigkeit of faehigkeitenVon(instanz)) { + expect(FAEHIGKEITEN).toContain(faehigkeit) + } + } + }) + + it('gibt keiner Instanz alles', () => { + for (const instanz of INSTANZEN) { + expect(faehigkeitenVon(instanz).length).toBeLessThan(FAEHIGKEITEN.length) + } + }) +}) + +describe('kalenderAnbieterVon', () => { + it('nennt fuer die Firma nur Graph', () => { + expect(kalenderAnbieterVon('firma')).toEqual(['GRAPH']) + }) + + it('nennt daheim beide', () => { + expect(kalenderAnbieterVon('daheim')).toEqual(['GRAPH', 'GOOGLE']) + }) +}) diff --git a/src/lib/instanz.ts b/src/lib/instanz.ts new file mode 100644 index 0000000..6726ae3 --- /dev/null +++ b/src/lib/instanz.ts @@ -0,0 +1,96 @@ +/** + * Instanzsteuerung. + * + * Ein Image, zwei Deployments. Welche Provider, Integrationen und Routen + * erlaubt sind, haengt allein an INSTANZ und wird serverseitig erzwungen – + * nicht in der Oberflaeche versteckt. + * + * Diese Datei ist reine Logik: kein Zugriff auf process.env, keine Systemzeit. + */ + +export const INSTANZEN = ['firma', 'daheim'] as const +export type Instanz = (typeof INSTANZEN)[number] + +/** + * Alles, was je nach Instanz an- oder abgeschaltet ist. Eine Faehigkeit, die + * hier nicht steht, gibt es ueberall. + */ +export const FAEHIGKEITEN = [ + // Kalender- und Mailanbindung + 'kalender:graph', + 'kalender:google', + 'mail:graph', + 'mail:gmail', + // Integrationen + 'integration:zammad', + 'integration:wartungsplaner', + 'integration:telegram', + 'spiegel:senden', + 'spiegel:empfangen', +] as const +export type Faehigkeit = (typeof FAEHIGKEITEN)[number] + +const ERLAUBT: Readonly> = { + firma: [ + 'kalender:graph', + 'mail:graph', + 'integration:zammad', + 'integration:wartungsplaner', + 'spiegel:senden', + ], + daheim: [ + 'kalender:graph', + 'kalender:google', + 'mail:graph', + 'mail:gmail', + 'integration:telegram', + 'spiegel:empfangen', + ], +} + +/** Anzeigename der Instanz fuer die Oberflaeche. */ +export const INSTANZ_NAME: Readonly> = { + firma: 'Firma', + daheim: 'Daheim', +} + +export function istInstanz(wert: unknown): wert is Instanz { + return typeof wert === 'string' && (INSTANZEN as readonly string[]).includes(wert) +} + +/** + * Liest den Konfigurationswert und bricht bei Unsinn ab. Ein falsch + * geschriebenes INSTANZ darf nicht stillschweigend zu "daheim" werden – + * sonst laufen in der Firma Provider an, die dort nichts zu suchen haben. + */ +export function leseInstanz(wert: string | undefined): Instanz { + const bereinigt = wert?.trim().toLowerCase() + if (!bereinigt) { + throw new Error('INSTANZ ist nicht gesetzt. Erlaubt: ' + INSTANZEN.join(', ')) + } + if (!istInstanz(bereinigt)) { + throw new Error( + `INSTANZ hat den unbekannten Wert "${wert ?? ''}". Erlaubt: ` + + INSTANZEN.join(', '), + ) + } + return bereinigt +} + +/** Ist diese Faehigkeit in dieser Instanz erlaubt? */ +export function istErlaubt(instanz: Instanz, faehigkeit: Faehigkeit): boolean { + return ERLAUBT[instanz].includes(faehigkeit) +} + +/** Alle in dieser Instanz erlaubten Faehigkeiten, in fester Reihenfolge. */ +export function faehigkeitenVon(instanz: Instanz): readonly Faehigkeit[] { + return FAEHIGKEITEN.filter((f) => istErlaubt(instanz, f)) +} + +/** Welche Kalenderanbieter darf diese Instanz verbinden? */ +export function kalenderAnbieterVon(instanz: Instanz): readonly ('GRAPH' | 'GOOGLE')[] { + const anbieter: ('GRAPH' | 'GOOGLE')[] = [] + if (istErlaubt(instanz, 'kalender:graph')) anbieter.push('GRAPH') + if (istErlaubt(instanz, 'kalender:google')) anbieter.push('GOOGLE') + return anbieter +} diff --git a/src/lib/passwort.test.ts b/src/lib/passwort.test.ts new file mode 100644 index 0000000..28596be --- /dev/null +++ b/src/lib/passwort.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { + PASSWORT_HOECHSTLAENGE, + PASSWORT_MINDESTLAENGE, + passwortIstGut, + passwortMaengel, +} from './passwort' + +describe('passwortMaengel', () => { + it('nimmt ein ausreichend langes Passwort an', () => { + expect(passwortMaengel('richtiges pferd batterie klammer')).toEqual([]) + }) + + it('bemaengelt zu kurze Passwoerter', () => { + expect(passwortMaengel('kurz')).toContain( + `Mindestens ${PASSWORT_MINDESTLAENGE} Zeichen.`, + ) + }) + + it('setzt die Grenze bei genau der Mindestlaenge', () => { + expect( + passwortMaengel('a'.repeat(PASSWORT_MINDESTLAENGE - 1)).length, + ).toBeGreaterThan(0) + // Genau die Mindestlaenge, aber ein einziges wiederholtes Zeichen. + expect(passwortMaengel('a'.repeat(PASSWORT_MINDESTLAENGE))).toEqual([ + 'Darf nicht aus einem einzigen wiederholten Zeichen bestehen.', + ]) + expect(passwortMaengel('abcdefghijkl')).toEqual([]) + }) + + it('weist ueberlange Eingaben ab, damit das Hashen begrenzt bleibt', () => { + expect(passwortMaengel('a'.repeat(PASSWORT_HOECHSTLAENGE + 1))).toContain( + `Höchstens ${PASSWORT_HOECHSTLAENGE} Zeichen.`, + ) + }) + + it('weist reine Leerzeichen ab', () => { + expect(passwortMaengel(' '.repeat(20))).toContain( + 'Darf nicht nur aus Leerzeichen bestehen.', + ) + }) + + it('weist ein einzelnes wiederholtes Zeichen ab', () => { + expect(passwortMaengel('xxxxxxxxxxxxxxxx')).toContain( + 'Darf nicht aus einem einzigen wiederholten Zeichen bestehen.', + ) + }) + + it('meldet mehrere Maengel gleichzeitig', () => { + expect(passwortMaengel('aaa').length).toBe(2) + }) +}) + +describe('passwortIstGut', () => { + it('fasst das Ergebnis zusammen', () => { + expect(passwortIstGut('richtiges pferd batterie')).toBe(true) + expect(passwortIstGut('kurz')).toBe(false) + }) +}) diff --git a/src/lib/passwort.ts b/src/lib/passwort.ts new file mode 100644 index 0000000..67ac1cc --- /dev/null +++ b/src/lib/passwort.ts @@ -0,0 +1,37 @@ +/** + * Regeln fuer Passwoerter. Reine Logik, damit sie in Formular und Server + * Action dieselbe Antwort gibt. + */ + +export const PASSWORT_MINDESTLAENGE = 12 +export const PASSWORT_HOECHSTLAENGE = 200 + +/** + * Prueft ein Passwort und gibt alle Verstoesse zurueck. Eine leere Liste + * heisst: in Ordnung. + * + * Bewusst keine Pflicht zu Sonderzeichen – Laenge traegt mehr zur Sicherheit + * bei als erzwungene Zeichenklassen, die zu "Passwort1!" fuehren. + */ +export function passwortMaengel(passwort: string): string[] { + const maengel: string[] = [] + + if (passwort.length < PASSWORT_MINDESTLAENGE) { + maengel.push(`Mindestens ${PASSWORT_MINDESTLAENGE} Zeichen.`) + } + if (passwort.length > PASSWORT_HOECHSTLAENGE) { + maengel.push(`Höchstens ${PASSWORT_HOECHSTLAENGE} Zeichen.`) + } + if (passwort.trim().length === 0) { + maengel.push('Darf nicht nur aus Leerzeichen bestehen.') + } + if (/^(.)\1*$/.test(passwort) && passwort.length > 0) { + maengel.push('Darf nicht aus einem einzigen wiederholten Zeichen bestehen.') + } + + return maengel +} + +export function passwortIstGut(passwort: string): boolean { + return passwortMaengel(passwort).length === 0 +} diff --git a/src/lib/rollen.test.ts b/src/lib/rollen.test.ts new file mode 100644 index 0000000..07c6ae5 --- /dev/null +++ b/src/lib/rollen.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { ROLLEN_NAME, istAdmin, rolleReichtAus } from './rollen' + +describe('rolleReichtAus', () => { + it('laesst jede Rolle das, was sie selbst ist', () => { + expect(rolleReichtAus('BENUTZER', 'BENUTZER')).toBe(true) + expect(rolleReichtAus('ADMIN', 'ADMIN')).toBe(true) + }) + + it('laesst die Verwaltung auch alles Niedrigere', () => { + expect(rolleReichtAus('ADMIN', 'BENUTZER')).toBe(true) + }) + + it('laesst einen Benutzer nicht in die Verwaltung', () => { + expect(rolleReichtAus('BENUTZER', 'ADMIN')).toBe(false) + }) +}) + +describe('istAdmin', () => { + it('erkennt nur die Verwaltung', () => { + expect(istAdmin('ADMIN')).toBe(true) + expect(istAdmin('BENUTZER')).toBe(false) + }) +}) + +describe('ROLLEN_NAME', () => { + it('benennt jede Rolle deutsch', () => { + expect(ROLLEN_NAME.BENUTZER).toBe('Benutzer') + expect(ROLLEN_NAME.ADMIN).toBe('Verwaltung') + }) +}) diff --git a/src/lib/rollen.ts b/src/lib/rollen.ts new file mode 100644 index 0000000..3cfa889 --- /dev/null +++ b/src/lib/rollen.ts @@ -0,0 +1,25 @@ +import type { Rolle } from '@/generated/prisma/enums' + +/** + * Rangfolge der Rollen. Reine Logik, damit Seite, Server Action und + * Oberflaeche dieselbe Antwort geben. + */ + +const RANG: Record = { + BENUTZER: 1, + ADMIN: 2, +} + +export const ROLLEN_NAME: Record = { + BENUTZER: 'Benutzer', + ADMIN: 'Verwaltung', +} + +/** Reicht die vorhandene Rolle fuer das Verlangte? */ +export function rolleReichtAus(vorhanden: Rolle, verlangt: Rolle): boolean { + return RANG[vorhanden] >= RANG[verlangt] +} + +export function istAdmin(rolle: Rolle): boolean { + return rolleReichtAus(rolle, 'ADMIN') +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3b367b0 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +/** Klassennamen zusammenfuehren, spaetere Tailwind-Klassen gewinnen. */ +export function cn(...eingaben: ClassValue[]): string { + return twMerge(clsx(eingaben)) +} diff --git a/src/lib/version.test.ts b/src/lib/version.test.ts new file mode 100644 index 0000000..3442a19 --- /dev/null +++ b/src/lib/version.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { VERSION, versionAnzeige } from './version' + +describe('VERSION', () => { + it('kommt aus der package.json und sieht nach Semver aus', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+(?:-[\w.]+)?$/) + }) +}) + +describe('versionAnzeige', () => { + it('stellt ein v voran', () => { + expect(versionAnzeige('0.1.0')).toBe('v0.1.0') + }) +}) diff --git a/src/lib/version.ts b/src/lib/version.ts new file mode 100644 index 0000000..e343e70 --- /dev/null +++ b/src/lib/version.ts @@ -0,0 +1,9 @@ +import paket from '../../package.json' with { type: 'json' } + +/** Die Version steht ausschliesslich in package.json. */ +export const VERSION: string = paket.version + +/** Fuer die Fusszeile: "v0.1.0". */ +export function versionAnzeige(version: string = VERSION): string { + return `v${version}` +} diff --git a/src/lib/zeit.test.ts b/src/lib/zeit.test.ts new file mode 100644 index 0000000..b2493c0 --- /dev/null +++ b/src/lib/zeit.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it } from 'vitest' +import { + alsDatum, + alsDatumUhrzeit, + alsDauer, + alsTagesschluessel, + alsUhrzeit, + ausUhrzeit, + gleicherTag, + minutenSeitMitternacht, + tageDazwischen, + tagesBeginn, + tagesEnde, + wochenBeginn, + wochenTage, + wochentagIndex, +} from './zeit' + +// Winterzeit: Berlin ist UTC+1. Sommerzeit: UTC+2. +const winter = new Date('2026-01-15T12:00:00Z') +const sommer = new Date('2026-07-15T12:00:00Z') + +describe('tagesBeginn', () => { + it('schneidet in der Winterzeit bei 23:00 UTC des Vortags', () => { + expect(tagesBeginn(winter).toISOString()).toBe('2026-01-14T23:00:00.000Z') + }) + + it('schneidet in der Sommerzeit bei 22:00 UTC des Vortags', () => { + expect(tagesBeginn(sommer).toISOString()).toBe('2026-07-14T22:00:00.000Z') + }) + + it('ordnet 22:30 UTC im Winter noch demselben Berliner Tag zu', () => { + // 22:30 UTC = 23:30 Berlin, also derselbe Tag. + expect(tagesBeginn(new Date('2026-01-15T22:30:00Z')).toISOString()).toBe( + '2026-01-14T23:00:00.000Z', + ) + }) + + it('ordnet 23:30 UTC im Winter bereits dem Folgetag zu', () => { + // 23:30 UTC = 00:30 Berlin am 16. + expect(tagesBeginn(new Date('2026-01-15T23:30:00Z')).toISOString()).toBe( + '2026-01-15T23:00:00.000Z', + ) + }) +}) + +describe('tagesEnde', () => { + it('endet eine Millisekunde vor dem naechsten Tagesbeginn', () => { + expect(tagesEnde(winter).getTime()).toBe( + tagesBeginn(new Date('2026-01-16T12:00:00Z')).getTime() - 1, + ) + }) +}) + +describe('wochenBeginn', () => { + it('beginnt am Montag, nicht am Sonntag', () => { + // 2026-01-15 ist ein Donnerstag, der Montag davor ist der 12. + expect(alsTagesschluessel(wochenBeginn(winter))).toBe('2026-01-12') + }) + + it('bleibt am Montag selbst stehen', () => { + const montag = new Date('2026-01-12T09:00:00Z') + expect(alsTagesschluessel(wochenBeginn(montag))).toBe('2026-01-12') + }) + + it('rechnet den Sonntag zur ablaufenden Woche', () => { + const sonntag = new Date('2026-01-18T09:00:00Z') + expect(alsTagesschluessel(wochenBeginn(sonntag))).toBe('2026-01-12') + }) +}) + +describe('wochenTage', () => { + it('liefert sieben Tage von Montag bis Sonntag', () => { + const tage = wochenTage(winter).map((t) => alsTagesschluessel(t)) + expect(tage).toEqual([ + '2026-01-12', + '2026-01-13', + '2026-01-14', + '2026-01-15', + '2026-01-16', + '2026-01-17', + '2026-01-18', + ]) + }) + + it('haelt die sieben Tage auch ueber die Zeitumstellung hinweg', () => { + // Die Sommerzeit beginnt 2026 am 29. Maerz. + const tage = wochenTage(new Date('2026-03-25T12:00:00Z')).map((t) => + alsTagesschluessel(t), + ) + expect(tage).toEqual([ + '2026-03-23', + '2026-03-24', + '2026-03-25', + '2026-03-26', + '2026-03-27', + '2026-03-28', + '2026-03-29', + ]) + }) +}) + +describe('wochentagIndex', () => { + it('zaehlt Montag als 0 und Sonntag als 6', () => { + expect(wochentagIndex(new Date('2026-01-12T12:00:00Z'))).toBe(0) + expect(wochentagIndex(new Date('2026-01-18T12:00:00Z'))).toBe(6) + }) +}) + +describe('tageDazwischen', () => { + it('zaehlt Kalendertage, nicht 24-Stunden-Bloecke', () => { + const spaetAbends = new Date('2026-01-15T22:30:00Z') // 23:30 Berlin + const frueh = new Date('2026-01-16T06:00:00Z') // 07:00 Berlin am Folgetag + expect(tageDazwischen(spaetAbends, frueh)).toBe(1) + }) +}) + +describe('gleicherTag', () => { + it('erkennt denselben Berliner Tag trotz unterschiedlicher UTC-Tage', () => { + expect( + gleicherTag(new Date('2026-01-15T22:30:00Z'), new Date('2026-01-15T08:00:00Z')), + ).toBe(true) + }) + + it('trennt zwei Tage', () => { + expect(gleicherTag(winter, new Date('2026-01-16T12:00:00Z'))).toBe(false) + }) +}) + +describe('minutenSeitMitternacht', () => { + it('rechnet in Berliner Ortszeit', () => { + expect(minutenSeitMitternacht(winter)).toBe(13 * 60) // 12:00 UTC = 13:00 Berlin + expect(minutenSeitMitternacht(sommer)).toBe(14 * 60) // 12:00 UTC = 14:00 Berlin + }) +}) + +describe('alsUhrzeit und ausUhrzeit', () => { + it('wandelt Minuten in eine Uhrzeit', () => { + expect(alsUhrzeit(0)).toBe('00:00') + expect(alsUhrzeit(480)).toBe('08:00') + expect(alsUhrzeit(1020)).toBe('17:00') + expect(alsUhrzeit(1439)).toBe('23:59') + }) + + it('faengt Werte ausserhalb des Tages ab', () => { + expect(alsUhrzeit(1440)).toBe('00:00') + expect(alsUhrzeit(-60)).toBe('23:00') + }) + + it('liest eine Uhrzeit zurueck', () => { + expect(ausUhrzeit('08:00')).toBe(480) + expect(ausUhrzeit('8:05')).toBe(485) + expect(ausUhrzeit(' 23:59 ')).toBe(1439) + }) + + it('weist Unsinn ab', () => { + expect(ausUhrzeit('24:00')).toBeNull() + expect(ausUhrzeit('08:60')).toBeNull() + expect(ausUhrzeit('acht')).toBeNull() + expect(ausUhrzeit('')).toBeNull() + }) + + it('ist in beide Richtungen stimmig', () => { + for (const minuten of [0, 1, 59, 60, 480, 719, 720, 1439]) { + expect(ausUhrzeit(alsUhrzeit(minuten))).toBe(minuten) + } + }) +}) + +describe('alsDauer', () => { + it('schreibt Minuten, Stunden und beides', () => { + expect(alsDauer(45)).toBe('45 min') + expect(alsDauer(60)).toBe('1 h') + expect(alsDauer(90)).toBe('1 h 30 min') + expect(alsDauer(0)).toBe('0 min') + }) + + it('rundet und bleibt bei Unsinn bei null', () => { + expect(alsDauer(89.6)).toBe('1 h 30 min') + expect(alsDauer(-30)).toBe('0 min') + }) +}) + +describe('Anzeigeformate', () => { + it('schreibt das Datum deutsch mit Wochentag', () => { + expect(alsDatum(winter)).toBe('Do., 15.01.2026') + }) + + it('schreibt Datum und Uhrzeit in Berliner Zeit', () => { + expect(alsDatumUhrzeit(sommer)).toBe('15.07.2026, 14:00') + }) + + it('liefert einen sortierbaren Tagesschluessel', () => { + expect(alsTagesschluessel(sommer)).toBe('2026-07-15') + }) +}) diff --git a/src/lib/zeit.ts b/src/lib/zeit.ts new file mode 100644 index 0000000..4d196de --- /dev/null +++ b/src/lib/zeit.ts @@ -0,0 +1,123 @@ +/** + * Zeitrechnung des Planers. + * + * Alle Anzeigen und Tagesgrenzen gelten in Europe/Berlin, unabhaengig davon, + * in welcher Zeitzone der Server laeuft. Reine Logik: die aktuelle Zeit wird + * immer als Parameter hereingereicht, nie hier gelesen. + */ +import { TZDate } from '@date-fns/tz' +import { + addDays, + differenceInCalendarDays, + endOfDay, + format, + startOfDay, + startOfWeek, +} from 'date-fns' +import { de } from 'date-fns/locale' + +export const ZEITZONE = 'Europe/Berlin' + +/** Dasselbe Zeitpunkt, betrachtet in der Zeitzone des Planers. */ +export function inZeitzone(zeitpunkt: Date, zeitzone: string = ZEITZONE): TZDate { + return new TZDate(zeitpunkt, zeitzone) +} + +/** 00:00:00 des Tages, in dem der Zeitpunkt in Europe/Berlin liegt. */ +export function tagesBeginn(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date(startOfDay(inZeitzone(zeitpunkt, zeitzone)).getTime()) +} + +/** 23:59:59.999 desselben Tages. */ +export function tagesEnde(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date(endOfDay(inZeitzone(zeitpunkt, zeitzone)).getTime()) +} + +/** Montag 00:00 der Woche, in der der Zeitpunkt liegt. */ +export function wochenBeginn(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date( + startOfWeek(inZeitzone(zeitpunkt, zeitzone), { weekStartsOn: 1 }).getTime(), + ) +} + +/** Die sieben Tagesanfaenge der Woche, Montag zuerst. */ +export function wochenTage(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date[] { + const montag = inZeitzone(wochenBeginn(zeitpunkt, zeitzone), zeitzone) + return Array.from({ length: 7 }, (_, i) => new Date(addDays(montag, i).getTime())) +} + +/** + * Wochentag als Index 0 = Montag bis 6 = Sonntag. Die Kapazitaetsfelder eines + * Bereichs stehen in dieser Reihenfolge. + */ +export function wochentagIndex(zeitpunkt: Date, zeitzone: string = ZEITZONE): number { + return (inZeitzone(zeitpunkt, zeitzone).getDay() + 6) % 7 +} + +/** Ganze Kalendertage zwischen zwei Zeitpunkten, in Europe/Berlin gezaehlt. */ +export function tageDazwischen( + von: Date, + bis: Date, + zeitzone: string = ZEITZONE, +): number { + return differenceInCalendarDays(inZeitzone(bis, zeitzone), inZeitzone(von, zeitzone)) +} + +/** Liegen beide Zeitpunkte am selben Kalendertag? */ +export function gleicherTag(a: Date, b: Date, zeitzone: string = ZEITZONE): boolean { + return tagesBeginn(a, zeitzone).getTime() === tagesBeginn(b, zeitzone).getTime() +} + +/** Minuten seit Mitternacht, z. B. fuer das Arbeitsfenster eines Bereichs. */ +export function minutenSeitMitternacht( + zeitpunkt: Date, + zeitzone: string = ZEITZONE, +): number { + const lokal = inZeitzone(zeitpunkt, zeitzone) + return lokal.getHours() * 60 + lokal.getMinutes() +} + +/** "08:00" aus 480. */ +export function alsUhrzeit(minutenSeitMitternacht: number): string { + const gesamt = ((minutenSeitMitternacht % 1440) + 1440) % 1440 + const stunden = Math.floor(gesamt / 60) + const minuten = gesamt % 60 + return `${String(stunden).padStart(2, '0')}:${String(minuten).padStart(2, '0')}` +} + +/** 480 aus "08:00". Gibt null zurueck, wenn die Eingabe keine Uhrzeit ist. */ +export function ausUhrzeit(text: string): number | null { + const treffer = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(text.trim()) + if (!treffer) return null + return Number(treffer[1]) * 60 + Number(treffer[2]) +} + +/** "1 h 30 min", "45 min", "2 h". Fuer Aufwaende und Kapazitaeten. */ +export function alsDauer(minuten: number): string { + const gerundet = Math.max(0, Math.round(minuten)) + const stunden = Math.floor(gerundet / 60) + const rest = gerundet % 60 + if (stunden === 0) return `${rest} min` + if (rest === 0) return `${stunden} h` + return `${stunden} h ${rest} min` +} + +/** "Mo., 10.09.2026" */ +export function alsDatum(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'EEE, dd.MM.yyyy', { locale: de }) +} + +/** "10.09.2026, 14:30" */ +export function alsDatumUhrzeit(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'dd.MM.yyyy, HH:mm', { locale: de }) +} + +/** "14:30" */ +export function alsUhrzeitAnzeige(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'HH:mm', { locale: de }) +} + +/** ISO-Datum "2026-09-10" in Europe/Berlin, taugt als Schluessel je Tag. */ +export function alsTagesschluessel(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'yyyy-MM-dd') +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..a67268b --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,25 @@ +import { NextResponse, type NextRequest } from 'next/server' + +export const PFAD_KOPFZEILE = 'x-planer-pfad' + +/** + * Reicht den angefragten Pfad als Kopfzeile weiter. + * + * Ein Layout kennt seinen eigenen Pfad nicht, braucht ihn aber, um nach der + * Anmeldung wieder dorthin zurückzuführen. Hier steht bewusst keine + * Anmeldelogik: der Proxy läuft im Edge-Umfeld, in dem weder Prisma noch + * der Adapter verfügbar sind. Geprüft wird weiterhin im Layout, in der Seite + * und in jeder Server Action. + */ +export function proxy(anfrage: NextRequest) { + const kopfzeilen = new Headers(anfrage.headers) + kopfzeilen.set(PFAD_KOPFZEILE, `${anfrage.nextUrl.pathname}${anfrage.nextUrl.search}`) + return NextResponse.next({ request: { headers: kopfzeilen } }) +} + +export const config = { + matcher: [ + // Alles außer den Auth-Routen, statischen Dateien und Bildern. + '/((?!api/auth|_next/static|_next/image|favicon.ico).*)', + ], +} diff --git a/src/server/aktionen/bereiche.ts b/src/server/aktionen/bereiche.ts new file mode 100644 index 0000000..f26c9d8 --- /dev/null +++ b/src/server/aktionen/bereiche.ts @@ -0,0 +1,169 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { z } from 'zod' +import { bereichSchema } from '@/lib/bereich' +import { auditSchreiben } from '@/server/audit' +import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung' +import { db } from '@/server/db' + +/** + * Server Actions fuer Bereiche. + * + * Jede prueft die Rolle selbst – die Seite tut es ebenfalls, aber eine Action + * ist ein eigener Einstiegspunkt und darf sich darauf nicht verlassen. Jede + * schreibende Aktion hinterlaesst einen Audit-Eintrag. + */ + +export type Rueckmeldung = + | { ok: true; meldung: string } + | { ok: false; fehler: string; felder?: Record } + +function feldFehler(fehler: z.ZodError): Record { + const felder: Record = {} + for (const problem of fehler.issues) { + const feld = problem.path.join('.') + felder[feld] ??= problem.message + } + return felder +} + +function alsFormularwerte(daten: FormData): Record { + const werte = Object.fromEntries(daten.entries()) + return { + ...werte, + // Ein nicht angehaktes Kaestchen taucht im FormData gar nicht auf. + aktiv: daten.get('aktiv') !== null, + } +} + +export async function bereichAnlegen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = bereichSchema.safeParse(alsFormularwerte(daten)) + if (!gelesen.success) { + return { + ok: false, + fehler: 'Bitte die markierten Felder prüfen.', + felder: feldFehler(gelesen.error), + } + } + + const vorhanden = await db.bereich.findUnique({ + where: { name: gelesen.data.name }, + select: { id: true }, + }) + if (vorhanden) { + return { + ok: false, + fehler: 'Diesen Namen gibt es schon.', + felder: { name: 'Diesen Namen gibt es schon.' }, + } + } + + const bereich = await db.bereich.create({ + data: gelesen.data, + select: { id: true, name: true }, + }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.angelegt', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name, farbe: gelesen.data.farbe }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${bereich.name}" angelegt.` } +} + +export async function bereichAendern(id: string, daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = bereichSchema.safeParse(alsFormularwerte(daten)) + if (!gelesen.success) { + return { + ok: false, + fehler: 'Bitte die markierten Felder prüfen.', + felder: feldFehler(gelesen.error), + } + } + + const vorher = await db.bereich.findUnique({ + where: { id }, + select: { id: true, name: true }, + }) + if (!vorher) return { ok: false, fehler: 'Diesen Bereich gibt es nicht mehr.' } + + const namensdublette = await db.bereich.findFirst({ + where: { name: gelesen.data.name, id: { not: id } }, + select: { id: true }, + }) + if (namensdublette) { + return { + ok: false, + fehler: 'Diesen Namen gibt es schon.', + felder: { name: 'Diesen Namen gibt es schon.' }, + } + } + + await db.bereich.update({ where: { id }, data: gelesen.data }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.geaendert', + objektTyp: 'Bereich', + objektId: id, + details: { vorher: vorher.name, nachher: gelesen.data.name }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${gelesen.data.name}" gespeichert.` } +} + +const loeschEingabe = z.object({ id: z.string().min(1) }) + +export async function bereichLoeschen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = loeschEingabe.safeParse({ id: daten.get('id') }) + if (!gelesen.success) return { ok: false, fehler: 'Ungültige Anfrage.' } + + const bereich = await db.bereich.findUnique({ + where: { id: gelesen.data.id }, + select: { id: true, name: true }, + }) + if (!bereich) return { ok: false, fehler: 'Diesen Bereich gibt es nicht mehr.' } + + await db.bereich.delete({ where: { id: bereich.id } }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.geloescht', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${bereich.name}" gelöscht.` } +} diff --git a/src/server/aktionen/passkeys.ts b/src/server/aktionen/passkeys.ts new file mode 100644 index 0000000..1ede0b5 --- /dev/null +++ b/src/server/aktionen/passkeys.ts @@ -0,0 +1,48 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { z } from 'zod' +import { auditSchreiben } from '@/server/audit' +import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung' +import { db } from '@/server/db' + +export type Rueckmeldung = { ok: true; meldung: string } | { ok: false; fehler: string } + +const eingabe = z.object({ id: z.string().min(1) }) + +/** + * Entfernt einen Passkey. Jeder darf nur seine eigenen entfernen – deshalb + * geht die Benutzer-Kennung in die where-Bedingung und wird nicht nur + * nachträglich verglichen. + */ +export async function passkeyEntfernen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('BENUTZER') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = eingabe.safeParse({ id: daten.get('id') }) + if (!gelesen.success) return { ok: false, fehler: 'Ungültige Anfrage.' } + + const passkey = await db.passkey.findFirst({ + where: { id: gelesen.data.id, benutzerId: akteur.id }, + select: { id: true, bezeichnung: true }, + }) + if (!passkey) return { ok: false, fehler: 'Diesen Passkey gibt es nicht mehr.' } + + await db.passkey.delete({ where: { id: passkey.id } }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'passkey.entfernt', + objektTyp: 'Passkey', + objektId: passkey.id, + details: { bezeichnung: passkey.bezeichnung }, + }) + + revalidatePath('/einstellungen') + return { ok: true, meldung: `Passkey „${passkey.bezeichnung}" entfernt.` } +} diff --git a/src/server/audit.ts b/src/server/audit.ts new file mode 100644 index 0000000..88bae52 --- /dev/null +++ b/src/server/audit.ts @@ -0,0 +1,44 @@ +import 'server-only' +import type { Prisma } from '@/generated/prisma/client' +import { auditDetails } from '@/lib/audit' +import { db } from '@/server/db' +import { aktuelleInstanz } from '@/server/instanz' + +/** + * Nachweis jeder schreibenden Aktion. Die Details werden vorher gesaeubert + * (siehe @/lib/audit), damit keine Geheimnisse in der Datenbank landen. + */ + +export type AuditAkteur = { + id: string | null + email: string +} + +export type AuditEingabe = { + akteur: AuditAkteur + /** Was geschah, in der Form "objekt.verb", z. B. "bereich.angelegt". */ + aktion: string + objektTyp: string + objektId?: string | null + details?: Record | undefined +} + +/** + * Schreibt einen Audit-Eintrag. Schlaegt das fehl, darf die eigentliche + * Aktion nicht als geglueckt gelten – deshalb wird der Fehler weitergereicht + * und nicht verschluckt. + */ +export async function auditSchreiben(eingabe: AuditEingabe): Promise { + const details = auditDetails(eingabe.details) + await db.auditEintrag.create({ + data: { + benutzerId: eingabe.akteur.id, + benutzerEmail: eingabe.akteur.email, + aktion: eingabe.aktion, + objektTyp: eingabe.objektTyp, + objektId: eingabe.objektId ?? null, + ...(details ? { details: details as Prisma.InputJsonObject } : {}), + instanz: aktuelleInstanz(), + }, + }) +} diff --git a/src/server/auth/adapter.ts b/src/server/auth/adapter.ts new file mode 100644 index 0000000..62317e5 --- /dev/null +++ b/src/server/auth/adapter.ts @@ -0,0 +1,123 @@ +import 'server-only' +import type { Adapter, AdapterAccount, AdapterUser } from 'next-auth/adapters' +import type { PrismaClient } from '@/generated/prisma/client' + +/** + * Adapter fuer Auth.js auf unsere deutschen Modellnamen. + * + * Der mitgelieferte @auth/prisma-adapter greift fest auf prisma.user, + * prisma.account und prisma.session zu. Unsere Modelle heissen Benutzer und + * Konto, deshalb dieser schmale Zwischenschritt. + * + * Sitzungen fuehrt Auth.js hier als JWT – das verlangt der Credentials-Provider. + * Die Sitzungsmethoden des Adapters bleiben daher bewusst unbesetzt. + */ + +type BenutzerZeile = { + id: string + email: string + name: string | null + bildUrl: string | null + emailGeprueft: Date | null +} + +const AUSWAHL = { + id: true, + email: true, + name: true, + bildUrl: true, + emailGeprueft: true, +} as const + +function alsAdapterBenutzer(zeile: BenutzerZeile): AdapterUser { + return { + id: zeile.id, + email: zeile.email, + name: zeile.name, + image: zeile.bildUrl, + emailVerified: zeile.emailGeprueft, + } +} + +export function prismaAdapter(db: PrismaClient): Adapter { + return { + async createUser(daten) { + // Neue Benutzer entstehen ueber OIDC. Die Rolle bleibt der Standard; + // hochgestuft wird nur von Hand in der Verwaltung. + const zeile = await db.benutzer.create({ + data: { + email: daten.email, + name: daten.name ?? null, + bildUrl: daten.image ?? null, + emailGeprueft: daten.emailVerified ?? null, + }, + select: AUSWAHL, + }) + return alsAdapterBenutzer(zeile) + }, + + async getUser(id) { + const zeile = await db.benutzer.findUnique({ where: { id }, select: AUSWAHL }) + return zeile ? alsAdapterBenutzer(zeile) : null + }, + + async getUserByEmail(email) { + const zeile = await db.benutzer.findUnique({ where: { email }, select: AUSWAHL }) + return zeile ? alsAdapterBenutzer(zeile) : null + }, + + async getUserByAccount({ provider, providerAccountId }) { + const konto = await db.konto.findUnique({ + where: { provider_providerAccountId: { provider, providerAccountId } }, + select: { benutzer: { select: AUSWAHL } }, + }) + return konto ? alsAdapterBenutzer(konto.benutzer) : null + }, + + async updateUser(daten) { + if (!daten.id) throw new Error('updateUser ohne id aufgerufen.') + const zeile = await db.benutzer.update({ + where: { id: daten.id }, + data: { + ...(daten.email === undefined ? {} : { email: daten.email }), + ...(daten.name === undefined ? {} : { name: daten.name }), + ...(daten.image === undefined ? {} : { bildUrl: daten.image }), + ...(daten.emailVerified === undefined + ? {} + : { emailGeprueft: daten.emailVerified }), + }, + select: AUSWAHL, + }) + return alsAdapterBenutzer(zeile) + }, + + async deleteUser(id) { + await db.benutzer.delete({ where: { id } }) + }, + + async linkAccount(konto: AdapterAccount) { + await db.konto.create({ + data: { + benutzerId: konto.userId, + type: konto.type, + provider: konto.provider, + providerAccountId: konto.providerAccountId, + refresh_token: konto.refresh_token ?? null, + access_token: konto.access_token ?? null, + expires_at: konto.expires_at ?? null, + token_type: konto.token_type ?? null, + scope: konto.scope ?? null, + id_token: konto.id_token ?? null, + session_state: + typeof konto.session_state === 'string' ? konto.session_state : null, + }, + }) + }, + + async unlinkAccount({ provider, providerAccountId }) { + await db.konto.delete({ + where: { provider_providerAccountId: { provider, providerAccountId } }, + }) + }, + } +} diff --git a/src/server/auth/config.ts b/src/server/auth/config.ts new file mode 100644 index 0000000..8a1a3b8 --- /dev/null +++ b/src/server/auth/config.ts @@ -0,0 +1,171 @@ +import 'server-only' +import type { NextAuthConfig } from 'next-auth' +import Credentials from 'next-auth/providers/credentials' +import { z } from 'zod' +import type { Rolle } from '@/generated/prisma/enums' +import { db } from '@/server/db' +import { prismaAdapter } from '@/server/auth/adapter' +import { passwortStimmt } from '@/server/auth/passwort' +import { passkeyAnmeldungPruefen } from '@/server/auth/passkey' +import { umgebung } from '@/server/umgebung' + +const zugangsdaten = z.object({ + email: z.email(), + passwort: z.string().min(1).max(200), +}) + +const passkeyEingabe = z.object({ + email: z.email(), + antwort: z.string().min(1).max(20_000), +}) + +/** Anzeigename fuer den OIDC-Knopf, mit brauchbarem Rueckfallwert. */ +function oidcName(): string { + return umgebung().OIDC_NAME || 'Unternehmensanmeldung' +} + +function providerZusammenstellen(): NextAuthConfig['providers'] { + const einst = umgebung() + const provider: NextAuthConfig['providers'] = [] + + if (einst.oidcAktiv) { + provider.push({ + id: 'oidc', + name: oidcName(), + type: 'oidc', + issuer: einst.OIDC_ISSUER, + clientId: einst.OIDC_CLIENT_ID, + clientSecret: einst.OIDC_CLIENT_SECRET, + // Je Instanz gibt es genau einen vertrauenswuerdigen Anbieter, der die + // E-Mail-Adresse selbst prueft. Deshalb darf ein bereits angelegter + // Benutzer beim ersten OIDC-Login mit derselben Adresse verknuepft + // werden, statt an OAuthAccountNotLinked zu scheitern. + allowDangerousEmailAccountLinking: true, + }) + } + + // Passkeys laufen ueber SimpleWebAuthn, nicht ueber den experimentellen + // WebAuthn-Provider von Auth.js. + provider.push( + Credentials({ + id: 'passkey', + name: 'Passkey', + credentials: { + email: { label: 'E-Mail', type: 'email' }, + antwort: { label: 'Antwort', type: 'text' }, + }, + authorize: async (eingabe) => { + const gelesen = passkeyEingabe.safeParse(eingabe) + if (!gelesen.success) return null + return passkeyAnmeldungPruefen(gelesen.data.email, gelesen.data.antwort) + }, + }), + ) + + if (einst.ANMELDUNG_PASSWORT) { + provider.push( + Credentials({ + id: 'passwort', + name: 'E-Mail und Passwort', + credentials: { + email: { label: 'E-Mail', type: 'email' }, + passwort: { label: 'Passwort', type: 'password' }, + }, + authorize: async (eingabe) => { + const gelesen = zugangsdaten.safeParse(eingabe) + if (!gelesen.success) return null + + const benutzer = await db.benutzer.findUnique({ + where: { email: gelesen.data.email.toLowerCase() }, + select: { + id: true, + email: true, + name: true, + bildUrl: true, + aktiv: true, + passwortHash: true, + }, + }) + + const stimmt = await passwortStimmt( + gelesen.data.passwort, + benutzer?.passwortHash ?? null, + ) + // Gleiche Antwort fuer "gibt es nicht", "falsches Passwort" und + // "gesperrt" – sonst laesst sich abfragen, wer ein Konto hat. + if (!benutzer || !benutzer.aktiv || !stimmt) return null + + return { + id: benutzer.id, + email: benutzer.email, + name: benutzer.name, + image: benutzer.bildUrl, + } + }, + }), + ) + } + + return provider +} + +export function authKonfiguration(): NextAuthConfig { + return { + adapter: prismaAdapter(db), + // Credentials-Provider verlangen JWT. Damit eine Sperre trotzdem sofort + // wirkt, wird die Rolle bei jeder Anfrage frisch aus der Datenbank + // gelesen (siehe session-Callback). + session: { strategy: 'jwt', maxAge: 60 * 60 * 24 * 30 }, + trustHost: umgebung().AUTH_TRUST_HOST, + pages: { + signIn: '/anmelden', + error: '/anmelden', + }, + callbacks: { + signIn: async ({ user }) => { + if (!user.email) return false + const benutzer = await db.benutzer.findUnique({ + where: { email: user.email.toLowerCase() }, + select: { aktiv: true }, + }) + // Unbekannte Adressen legt der Adapter ueber OIDC neu an; gesperrte + // Konten kommen nicht durch. + return benutzer ? benutzer.aktiv : true + }, + + jwt: ({ token, user }) => { + if (user?.id) token.sub = user.id + return token + }, + + session: async ({ session, token }) => { + if (!token.sub) return session + const benutzer = await db.benutzer.findUnique({ + where: { id: token.sub }, + select: { + id: true, + email: true, + name: true, + bildUrl: true, + rolle: true, + aktiv: true, + }, + }) + if (!benutzer?.aktiv) { + // Gesperrt oder geloescht: Sitzung sofort entwerten. + return { ...session, user: undefined } as unknown as typeof session + } + session.user = { + ...session.user, + id: benutzer.id, + email: benutzer.email, + name: benutzer.name, + image: benutzer.bildUrl, + rolle: benutzer.rolle satisfies Rolle, + } + return session + }, + }, + providers: providerZusammenstellen(), + } +} diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts new file mode 100644 index 0000000..6690451 --- /dev/null +++ b/src/server/auth/index.ts @@ -0,0 +1,13 @@ +import 'server-only' +import NextAuth from 'next-auth' +import { authKonfiguration } from '@/server/auth/config' + +/** + * Einstiegspunkt für Auth.js. + * + * Die Konfiguration wird als Funktion übergeben und dadurch erst bei der + * ersten Anfrage ausgewertet. Sie prüft Umgebungsvariablen, die es beim Bauen + * des Abbilds noch nicht gibt – bei sofortiger Auswertung bräche der Bau ab, + * sobald Next die Seitendaten der Auth-Routen sammelt. + */ +export const { handlers, auth, signIn, signOut } = NextAuth(() => authKonfiguration()) diff --git a/src/server/auth/passkey.ts b/src/server/auth/passkey.ts new file mode 100644 index 0000000..ffe69c4 --- /dev/null +++ b/src/server/auth/passkey.ts @@ -0,0 +1,281 @@ +import 'server-only' +import { + generateAuthenticationOptions, + generateRegistrationOptions, + verifyAuthenticationResponse, + verifyRegistrationResponse, +} from '@simplewebauthn/server' +import type { + AuthenticationResponseJSON, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/server' +import { db } from '@/server/db' +import { umgebung } from '@/server/umgebung' + +/** + * Passkeys mit SimpleWebAuthn. + * + * Die Herausforderung wird serverseitig abgelegt und nach einem Versuch + * geloescht – gleich ob er geglueckt ist. Ohne das liesse sich eine Antwort + * beliebig oft einreichen. + */ + +const CHALLENGE_GUELTIG_MINUTEN = 5 + +type Zweck = 'anmeldung' | 'registrierung' + +export type AngemeldeterBenutzer = { + id: string + email: string + name: string | null + image: string | null +} + +function frist(jetzt: Date): Date { + return new Date(jetzt.getTime() + CHALLENGE_GUELTIG_MINUTEN * 60_000) +} + +async function challengeMerken( + kennung: string, + zweck: Zweck, + challenge: string, + jetzt: Date, +): Promise { + // Aeltere Herausforderungen desselben Zwecks verfallen sofort, damit nie + // mehrere gleichzeitig gueltig sind. + await db.webauthnChallenge.deleteMany({ where: { kennung, zweck } }) + await db.webauthnChallenge.create({ + data: { kennung, zweck, challenge, gueltigBis: frist(jetzt) }, + }) +} + +/** Holt die Herausforderung und verbraucht sie im selben Zug. */ +async function challengeVerbrauchen( + kennung: string, + zweck: Zweck, + jetzt: Date, +): Promise { + const eintrag = await db.webauthnChallenge.findFirst({ + where: { kennung, zweck }, + orderBy: { gueltigBis: 'desc' }, + }) + if (!eintrag) return null + + await db.webauthnChallenge.deleteMany({ where: { kennung, zweck } }) + if (eintrag.gueltigBis.getTime() < jetzt.getTime()) return null + return eintrag.challenge +} + +/** Abgelaufene Herausforderungen aufraeumen; laeuft spaeter im Hintergrundjob mit. */ +export async function abgelaufeneChallengesEntfernen(jetzt: Date): Promise { + const ergebnis = await db.webauthnChallenge.deleteMany({ + where: { gueltigBis: { lt: jetzt } }, + }) + return ergebnis.count +} + +function alsErlaubteZugangsdaten( + passkeys: { credentialId: string; transports: string | null }[], +): { id: string; transports?: string[] }[] { + return passkeys.map((p) => + p.transports + ? { id: p.credentialId, transports: p.transports.split(',') } + : { id: p.credentialId }, + ) +} + +// --- Registrierung --------------------------------------------------------- + +export async function registrierungsOptionen( + benutzerId: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const benutzer = await db.benutzer.findUnique({ + where: { id: benutzerId }, + select: { + id: true, + email: true, + name: true, + passkeys: { select: { credentialId: true, transports: true } }, + }, + }) + if (!benutzer) throw new Error('Benutzer nicht gefunden.') + + const optionen = await generateRegistrationOptions({ + rpName: einst.WEBAUTHN_RP_NAME, + rpID: einst.WEBAUTHN_RP_ID, + userName: benutzer.email, + userDisplayName: benutzer.name ?? benutzer.email, + userID: new TextEncoder().encode(benutzer.id), + attestationType: 'none', + // Bereits hinterlegte Passkeys ausschliessen, damit dasselbe Geraet nicht + // zweimal registriert wird. + excludeCredentials: alsErlaubteZugangsdaten(benutzer.passkeys), + authenticatorSelection: { + residentKey: 'preferred', + userVerification: 'preferred', + }, + }) + + await challengeMerken(benutzer.id, 'registrierung', optionen.challenge, jetzt) + return optionen +} + +export async function registrierungPruefen( + benutzerId: string, + antwort: RegistrationResponseJSON, + bezeichnung: string, + jetzt: Date = new Date(), +): Promise<{ passkeyId: string; bezeichnung: string }> { + const einst = umgebung() + const erwartet = await challengeVerbrauchen(benutzerId, 'registrierung', jetzt) + if (!erwartet) { + throw new Error('Die Anfrage ist abgelaufen. Bitte noch einmal beginnen.') + } + + const ergebnis = await verifyRegistrationResponse({ + response: antwort, + expectedChallenge: erwartet, + expectedOrigin: einst.WEBAUTHN_ORIGIN, + expectedRPID: einst.WEBAUTHN_RP_ID, + requireUserVerification: false, + }) + + if (!ergebnis.verified || !ergebnis.registrationInfo) { + throw new Error('Der Passkey konnte nicht geprüft werden.') + } + + const { credential, credentialDeviceType, credentialBackedUp } = + ergebnis.registrationInfo + const name = bezeichnung.trim() || 'Unbenanntes Gerät' + + const gespeichert = await db.passkey.create({ + data: { + benutzerId, + credentialId: credential.id, + publicKey: Buffer.from(credential.publicKey), + counter: BigInt(credential.counter), + transports: credential.transports?.join(',') ?? null, + geraetetyp: credentialDeviceType, + gesichert: credentialBackedUp, + bezeichnung: name, + }, + select: { id: true }, + }) + + return { passkeyId: gespeichert.id, bezeichnung: name } +} + +// --- Anmeldung ------------------------------------------------------------- + +export async function anmeldeOptionen( + email: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const kennung = email.trim().toLowerCase() + + const benutzer = await db.benutzer.findUnique({ + where: { email: kennung }, + select: { + aktiv: true, + passkeys: { select: { credentialId: true, transports: true } }, + }, + }) + + // Auch fuer unbekannte Adressen werden Optionen erzeugt. Sonst liesse sich + // hier abfragen, wer ein Konto mit Passkey hat. + const erlaubte = + benutzer?.aktiv === true ? alsErlaubteZugangsdaten(benutzer.passkeys) : [] + + const optionen = await generateAuthenticationOptions({ + rpID: einst.WEBAUTHN_RP_ID, + allowCredentials: erlaubte, + userVerification: 'preferred', + }) + + await challengeMerken(kennung, 'anmeldung', optionen.challenge, jetzt) + return optionen +} + +/** + * Prueft die Antwort des Browsers und gibt den Benutzer zurueck, wenn alles + * stimmt. Wird vom Credentials-Provider "passkey" aufgerufen. + */ +export async function passkeyAnmeldungPruefen( + email: string, + antwortRoh: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const kennung = email.trim().toLowerCase() + + const erwartet = await challengeVerbrauchen(kennung, 'anmeldung', jetzt) + if (!erwartet) return null + + let antwort: AuthenticationResponseJSON + try { + antwort = JSON.parse(antwortRoh) as AuthenticationResponseJSON + } catch { + return null + } + if (typeof antwort.id !== 'string') return null + + const passkey = await db.passkey.findUnique({ + where: { credentialId: antwort.id }, + select: { + id: true, + publicKey: true, + counter: true, + transports: true, + benutzer: { + select: { id: true, email: true, name: true, bildUrl: true, aktiv: true }, + }, + }, + }) + + // Der Passkey muss zu genau der Adresse gehoeren, fuer die die + // Herausforderung ausgestellt wurde. + if (!passkey || !passkey.benutzer.aktiv || passkey.benutzer.email !== kennung) { + return null + } + + let ergebnis + try { + ergebnis = await verifyAuthenticationResponse({ + response: antwort, + expectedChallenge: erwartet, + expectedOrigin: einst.WEBAUTHN_ORIGIN, + expectedRPID: einst.WEBAUTHN_RP_ID, + requireUserVerification: false, + credential: { + id: antwort.id, + publicKey: new Uint8Array(passkey.publicKey), + counter: Number(passkey.counter), + ...(passkey.transports ? { transports: passkey.transports.split(',') } : {}), + }, + }) + } catch { + return null + } + + if (!ergebnis.verified) return null + + await db.passkey.update({ + where: { id: passkey.id }, + data: { + counter: BigInt(ergebnis.authenticationInfo.newCounter), + zuletztBenutzt: jetzt, + }, + }) + + return { + id: passkey.benutzer.id, + email: passkey.benutzer.email, + name: passkey.benutzer.name, + image: passkey.benutzer.bildUrl, + } +} diff --git a/src/server/auth/passwort.ts b/src/server/auth/passwort.ts new file mode 100644 index 0000000..abd7b6b --- /dev/null +++ b/src/server/auth/passwort.ts @@ -0,0 +1,30 @@ +import 'server-only' +import bcrypt from 'bcryptjs' +import { PASSWORT_HOECHSTLAENGE } from '@/lib/passwort' + +const RUNDEN = 12 + +export async function passwortHashen(passwort: string): Promise { + if (passwort.length > PASSWORT_HOECHSTLAENGE) { + throw new Error('Passwort ist zu lang.') + } + return bcrypt.hash(passwort, RUNDEN) +} + +/** + * Vergleicht Passwort und Hash. Ist kein Hash hinterlegt, wird trotzdem + * gerechnet: sonst verraet die Antwortzeit, ob es das Konto gibt. + */ +export async function passwortStimmt( + passwort: string, + hash: string | null, +): Promise { + if (!hash) { + await bcrypt.compare( + passwort, + '$2b$12$ungueltigerHashAlsPlatzhalterXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + ) + return false + } + return bcrypt.compare(passwort, hash) +} diff --git a/src/server/berechtigung.ts b/src/server/berechtigung.ts new file mode 100644 index 0000000..5458a8a --- /dev/null +++ b/src/server/berechtigung.ts @@ -0,0 +1,86 @@ +import 'server-only' +import { headers } from 'next/headers' +import { redirect } from 'next/navigation' +import type { Session } from 'next-auth' +import type { Rolle } from '@/generated/prisma/enums' +import { rolleReichtAus } from '@/lib/rollen' +import { auth } from '@/server/auth' +import { PFAD_KOPFZEILE } from '@/proxy' + +/** + * Rollenpruefung. + * + * Jede schreibende Aktion prueft serverseitig – in der Seite und noch einmal + * in der Server Action. Die Oberflaeche blendet Knoepfe nur zusaetzlich aus; + * verlassen wird sich darauf nie. + */ + +export type Angemeldet = Session['user'] + +/** Fehler, den Server Actions dem Aufrufer zurueckgeben. */ +export class NichtErlaubt extends Error { + constructor(nachricht = 'Dazu fehlt dir die Berechtigung.') { + super(nachricht) + this.name = 'NichtErlaubt' + } +} + +/** Die angemeldete Person, oder null. */ +export async function angemeldet(): Promise { + const sitzung = await auth() + return sitzung?.user?.id ? sitzung.user : null +} + +/** + * Riegel fuer Seiten: ohne Anmeldung geht es zur Anmeldeseite zurueck, + * mit Ziel, damit es danach dort weitergeht, wo man hinwollte. + */ +export async function verlangeAnmeldung(zielPfad?: string): Promise { + const benutzer = await angemeldet() + if (!benutzer) { + const ziel = zielPfad ? `?weiter=${encodeURIComponent(zielPfad)}` : '' + redirect(`/anmelden${ziel}`) + } + return benutzer +} + +/** Riegel fuer Seiten, die eine bestimmte Rolle verlangen. */ +export async function verlangeRolle( + verlangt: Rolle, + zielPfad?: string, +): Promise { + const benutzer = await verlangeAnmeldung(zielPfad) + if (!rolleReichtAus(benutzer.rolle, verlangt)) { + redirect('/keine-berechtigung') + } + return benutzer +} + +/** + * Riegel fuer Server Actions. Hier wird nicht umgeleitet, sondern geworfen – + * der Aufrufer wandelt das in eine Meldung fuer die Oberflaeche. + */ +export async function pruefeRolle(verlangt: Rolle): Promise { + const benutzer = await angemeldet() + if (!benutzer) throw new NichtErlaubt('Bitte zuerst anmelden.') + if (!rolleReichtAus(benutzer.rolle, verlangt)) throw new NichtErlaubt() + return benutzer +} + +/** + * Riegel fuer Route Handler: gibt null zurueck, statt umzuleiten. Der + * Aufrufer antwortet dann mit 401. + */ +export async function verlangeAnmeldungFuerApi(): Promise { + return angemeldet() +} + +/** + * Der angefragte Pfad, den der Proxy durchgereicht hat. Wird gebraucht, + * um nach der Anmeldung wieder dorthin zurueckzufuehren. + */ +export async function angefragterPfad(): Promise { + const kopfzeilen = await headers() + const pfad = kopfzeilen.get(PFAD_KOPFZEILE) + return pfad && pfad.startsWith('/') ? pfad : undefined +} diff --git a/src/server/db.ts b/src/server/db.ts new file mode 100644 index 0000000..bd22821 --- /dev/null +++ b/src/server/db.ts @@ -0,0 +1,47 @@ +import 'server-only' +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from '@/generated/prisma/client' + +/** + * Ein einziger Prisma-Client je Prozess. + * + * Der Client wird erst beim ersten Zugriff erzeugt, nicht beim Laden des + * Moduls. Beim Bauen des Abbilds gibt es noch keine DATABASE_URL – Next sammelt + * dabei aber Seitendaten und lädt dieses Modul. Würde hier sofort verbunden, + * bräche der Bau ab. + * + * Im Entwicklungsbetrieb überlebt der Client das Neuladen der Module, damit + * nicht bei jedem Speichern ein neuer Verbindungspool entsteht. + */ + +const globalerBereich = globalThis as unknown as { + planerPrisma: PrismaClient | undefined +} + +function erzeugen(): PrismaClient { + const verbindung = process.env.DATABASE_URL + if (!verbindung) { + throw new Error('DATABASE_URL ist nicht gesetzt.') + } + return new PrismaClient({ + adapter: new PrismaPg({ connectionString: verbindung }), + log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], + }) +} + +function klient(): PrismaClient { + globalerBereich.planerPrisma ??= erzeugen() + return globalerBereich.planerPrisma +} + +export const db: PrismaClient = new Proxy({} as PrismaClient, { + get(_ziel, eigenschaft) { + const echt = klient() as unknown as Record + const wert = echt[eigenschaft] + // Methoden an den echten Client binden, sonst zeigt "this" auf den Proxy. + if (typeof wert === 'function') { + return (wert as (...argumente: unknown[]) => unknown).bind(echt) + } + return wert + }, +}) diff --git a/src/server/instanz.ts b/src/server/instanz.ts new file mode 100644 index 0000000..aaefe84 --- /dev/null +++ b/src/server/instanz.ts @@ -0,0 +1,46 @@ +import 'server-only' +import { notFound } from 'next/navigation' +import { + type Faehigkeit, + type Instanz, + istErlaubt, + kalenderAnbieterVon, +} from '@/lib/instanz' +import { umgebung } from '@/server/umgebung' + +/** Welche Instanz laeuft hier? */ +export function aktuelleInstanz(): Instanz { + return umgebung().instanz +} + +/** Ist die Faehigkeit in dieser Instanz erlaubt? */ +export function kann(faehigkeit: Faehigkeit): boolean { + return istErlaubt(aktuelleInstanz(), faehigkeit) +} + +/** + * Riegel fuer Seiten und Route Handler: was diese Instanz nicht darf, + * existiert hier auch nicht. Kein Hinweis, kein 403 – schlicht 404. + */ +export function erzwingeFaehigkeit(faehigkeit: Faehigkeit): void { + if (!kann(faehigkeit)) { + notFound() + } +} + +/** + * Riegel fuer Server Actions und alles ausserhalb des Renderns, wo notFound() + * nicht greift. + */ +export function pruefeFaehigkeit(faehigkeit: Faehigkeit): void { + if (!kann(faehigkeit)) { + throw new Error( + `Die Faehigkeit "${faehigkeit}" ist in der Instanz "${aktuelleInstanz()}" nicht erlaubt.`, + ) + } +} + +/** Kalenderanbieter, die in dieser Instanz verbunden werden duerfen. */ +export function erlaubteKalenderAnbieter(): readonly ('GRAPH' | 'GOOGLE')[] { + return kalenderAnbieterVon(aktuelleInstanz()) +} diff --git a/src/server/umgebung.ts b/src/server/umgebung.ts new file mode 100644 index 0000000..5900dfb --- /dev/null +++ b/src/server/umgebung.ts @@ -0,0 +1,71 @@ +import 'server-only' +import { z } from 'zod' +import { INSTANZEN, type Instanz } from '@/lib/instanz' + +/** + * Jede Einstellung von aussen wird geprueft, bevor sie benutzt wird. Fehlt + * etwas Pflichtiges, faellt das beim ersten Zugriff auf – mit einer Meldung, + * die den Variablennamen nennt, nie deren Wert. + */ + +const jaNein = z + .enum(['true', 'false', '1', '0', 'ja', 'nein']) + .transform((w) => w === 'true' || w === '1' || w === 'ja') + +const schema = z.object({ + INSTANZ: z.enum(INSTANZEN), + APP_URL: z.url().transform((u) => u.replace(/\/+$/, '')), + DATABASE_URL: z.string().min(1), + + AUTH_SECRET: z.string().min(16, 'AUTH_SECRET ist zu kurz, mindestens 16 Zeichen.'), + AUTH_TRUST_HOST: jaNein.default(false), + ANMELDUNG_PASSWORT: jaNein.default(false), + + OIDC_NAME: z.string().trim().default(''), + OIDC_ISSUER: z.string().trim().default(''), + OIDC_CLIENT_ID: z.string().trim().default(''), + OIDC_CLIENT_SECRET: z.string().trim().default(''), + + WEBAUTHN_RP_ID: z.string().min(1), + WEBAUTHN_RP_NAME: z.string().min(1).default('Planer'), + WEBAUTHN_ORIGIN: z.url(), + + DEMO_MODUS: jaNein.default(false), +}) + +export type Umgebung = z.infer & { + instanz: Instanz + /** OIDC ist nur eingerichtet, wenn alle vier Werte gesetzt sind. */ + oidcAktiv: boolean +} + +let zwischengespeichert: Umgebung | null = null + +function lesen(): Umgebung { + const ergebnis = schema.safeParse(process.env) + if (!ergebnis.success) { + const felder = ergebnis.error.issues + .map((f) => `${f.path.join('.')}: ${f.message}`) + .join('\n ') + throw new Error(`Die Konfiguration ist unvollstaendig:\n ${felder}`) + } + const werte = ergebnis.data + return { + ...werte, + instanz: werte.INSTANZ, + oidcAktiv: Boolean( + werte.OIDC_ISSUER && werte.OIDC_CLIENT_ID && werte.OIDC_CLIENT_SECRET, + ), + } +} + +/** Die geprueften Einstellungen. Erst beim ersten Aufruf ausgewertet. */ +export function umgebung(): Umgebung { + zwischengespeichert ??= lesen() + return zwischengespeichert +} + +/** Nur fuer Tests: erzwingt beim naechsten Aufruf ein erneutes Einlesen. */ +export function umgebungVergessen(): void { + zwischengespeichert = null +} diff --git a/src/server/zeitgeber.ts b/src/server/zeitgeber.ts new file mode 100644 index 0000000..dbd0de7 --- /dev/null +++ b/src/server/zeitgeber.ts @@ -0,0 +1,20 @@ +import 'server-only' +import { umgebung } from '@/server/umgebung' + +/** + * Die aktuelle Zeit — für alles, was serverseitig gerendert wird. + * + * Reine Funktionen in src/lib bekommen den Zeitpunkt weiterhin als Parameter. + * Hier steht nur die eine Stelle, an der er entsteht. + * + * In der Screenshot-Instanz (DEMO_MODUS=true) steht die Uhr fest. Sonst würde + * sich in jedem Bild das Tagesdatum ändern und die Bilder wären bei jedem Lauf + * neu, ohne dass sich etwas Sichtbares geändert hätte. + */ + +/** Dienstag, 17. März 2026, 09:20 Uhr in Europe/Berlin. */ +export const DEMO_ZEITPUNKT = new Date('2026-03-17T08:20:00.000Z') + +export function jetzt(): Date { + return umgebung().DEMO_MODUS ? DEMO_ZEITPUNKT : new Date() +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..a72f305 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,20 @@ +import type { Rolle } from '@/generated/prisma/enums' + +/** + * Die Sitzung traegt zusaetzlich Kennung und Rolle. Beides wird im + * session-Callback bei jeder Anfrage frisch aus der Datenbank gelesen, damit + * eine Sperre oder ein Rollenwechsel sofort wirkt. + */ +declare module 'next-auth' { + interface Session { + user: { + id: string + rolle: Rolle + email: string + name?: string | null + image?: string | null + } + } +} + +export {} diff --git a/test/aufbau-oberflaeche.ts b/test/aufbau-oberflaeche.ts new file mode 100644 index 0000000..ed1966c --- /dev/null +++ b/test/aufbau-oberflaeche.ts @@ -0,0 +1,8 @@ +import { cleanup } from '@testing-library/react' +import { afterEach } from 'vitest' + +// Nach jedem Test das gerenderte DOM abraeumen, damit Tests sich nicht +// gegenseitig sehen. +afterEach(() => { + cleanup() +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..519efb5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "ES2022"], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "noEmit": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "vitest.config.ts", + ".next/dev/types/**/*.ts", + "playwright.config.ts", + "playwright.screenshots.config.ts" + ], + "exclude": ["node_modules", ".next", "out"] +} diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..35d577a --- /dev/null +++ b/update.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Aktualisiert den Planer auf eine neue Version. +# Für Linux mit Docker Compose. Läuft im Projektverzeichnis. +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$WURZEL" + +ZIELVERSION="" +OHNE_RUECKFRAGE=0 +OHNE_BACKUP=0 +NUR_PRUEFEN=0 +WARTEZEIT=180 + +LOGVERZEICHNIS="$WURZEL/logs" +LOGDATEI="$LOGVERZEICHNIS/update-$(date '+%Y-%m-%d_%H-%M-%S').log" + +hilfe() { + cat <<'ENDE' +Verwendung: update.sh [Optionen] + + --version vX.Y.Z Auf diese Version aktualisieren statt auf die neueste + --ja Ohne Rückfrage durchlaufen + --ohne-backup Keine Sicherung anlegen (nicht empfohlen) + --nur-pruefen Nur zeigen, was passieren würde + --wartezeit Sekunden, die auf einen gesunden Start gewartet wird + (Standard: 180) + --hilfe Diese Übersicht + +Ablauf: Voraussetzungen prüfen, Versionen ermitteln, Änderungen zeigen, +sichern, Ziel-Tag auschecken, bauen, starten, auf /api/health warten. +Schlägt etwas fehl, wird auf die vorherige Version zurückgerollt. +ENDE +} + +meldung() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI"; } +fehler() { + printf '%s Fehler: %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --version) + ZIELVERSION="${2:-}" + [ -n "$ZIELVERSION" ] || fehler '--version braucht eine Angabe wie v0.1.0.' + shift 2 + ;; + --ja) OHNE_RUECKFRAGE=1; shift ;; + --ohne-backup) OHNE_BACKUP=1; shift ;; + --nur-pruefen) NUR_PRUEFEN=1; shift ;; + --wartezeit) + WARTEZEIT="${2:-}" + case "$WARTEZEIT" in + ''|*[!0-9]*) fehler '--wartezeit braucht eine ganze Zahl.' ;; + esac + shift 2 + ;; + --hilfe|-h) hilfe; exit 0 ;; + *) fehler "Unbekannte Option '$1'. 'update.sh --hilfe' zeigt die Übersicht." ;; + esac +done + +mkdir -p "$LOGVERZEICHNIS" + +# --- 1. Voraussetzungen ---------------------------------------------------- +meldung 'Voraussetzungen prüfen.' +for werkzeug in git docker curl jq; do + command -v "$werkzeug" >/dev/null 2>&1 || fehler "$werkzeug fehlt." +done +docker compose version >/dev/null 2>&1 || fehler 'docker compose fehlt.' +[ -d "$WURZEL/.git" ] || fehler 'Kein Git-Arbeitsbaum – im Projektverzeichnis aufrufen.' +[ -f "$WURZEL/.env" ] || fehler '.env fehlt.' + +if [ -n "$(git status --porcelain)" ]; then + fehler 'Der Arbeitsbaum ist nicht sauber. Erst aufräumen oder committen.' +fi + +APP_PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- || true)" +APP_PORT="${APP_PORT:-3000}" +HEALTH_URL="http://127.0.0.1:${APP_PORT}/api/health" + +# --- 2. Versionen ermitteln ------------------------------------------------ +meldung 'Versionen ermitteln.' +INSTALLIERT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null | jq -r '.version // empty' || true)" +if [ -z "$INSTALLIERT" ]; then + INSTALLIERT="$(jq -r '.version' package.json)" + meldung "Die laufende Instanz war nicht erreichbar; nehme package.json: $INSTALLIERT" +else + meldung "Installiert: $INSTALLIERT" +fi + +VORHER_REF="$(git rev-parse --abbrev-ref HEAD)" +[ "$VORHER_REF" != 'HEAD' ] || VORHER_REF="$(git rev-parse HEAD)" + +git fetch --tags --quiet origin || fehler 'git fetch ist fehlgeschlagen.' + +if [ -z "$ZIELVERSION" ]; then + ZIELVERSION="$(git tag --list 'v*' --sort=-v:refname | head -1)" + [ -n "$ZIELVERSION" ] || fehler 'Es gibt keinen Versions-Tag auf origin.' +fi +git rev-parse -q --verify "refs/tags/${ZIELVERSION}" >/dev/null \ + || fehler "Den Tag $ZIELVERSION gibt es nicht." +meldung "Ziel: $ZIELVERSION" + +if [ "v$INSTALLIERT" = "$ZIELVERSION" ]; then + meldung 'Die Zielversion läuft bereits. Nichts zu tun.' + exit 0 +fi + +# --- 3. Änderungen zeigen -------------------------------------------------- +if [ -f CHANGELOG.md ]; then + meldung "Änderungen bis $ZIELVERSION laut CHANGELOG.md:" + # Vom Abschnitt der Zielversion bis zum Abschnitt der installierten Version. + awk -v ziel="${ZIELVERSION#v}" -v alt="$INSTALLIERT" ' + $0 ~ "^## \\[" ziel "\\]" { zeigen = 1 } + zeigen && $0 ~ "^## \\[" alt "\\]" { exit } + zeigen { print " " $0 } + ' CHANGELOG.md | tee -a "$LOGDATEI" +fi + +if [ "$NUR_PRUEFEN" -eq 1 ]; then + meldung "Nur geprüft: $INSTALLIERT würde auf ${ZIELVERSION#v} aktualisiert." + exit 0 +fi + +if [ "$OHNE_RUECKFRAGE" -eq 0 ]; then + printf 'Jetzt auf %s aktualisieren? [j/N] ' "$ZIELVERSION" + read -r antwort + case "$antwort" in + [jJ]|[jJ][aA]) ;; + *) meldung 'Abgebrochen.'; exit 0 ;; + esac +fi + +# --- 4. Sicherung ---------------------------------------------------------- +SICHERUNG='' +if [ "$OHNE_BACKUP" -eq 0 ]; then + meldung 'Sicherung anlegen.' + ./backup.sh --tage 30 | tee -a "$LOGDATEI" + SICHERUNG="$(find "$WURZEL/sicherungen" -maxdepth 1 -name 'planer-db-*.dump' -print0 2>/dev/null \ + | xargs -0 ls -t 2>/dev/null | head -1 || true)" +else + meldung 'Sicherung übersprungen (--ohne-backup).' +fi + +zurueckrollen() { + meldung "Zurückrollen auf $VORHER_REF." + git checkout --quiet "$VORHER_REF" || meldung 'Das Auschecken der alten Version schlug fehl.' + docker compose build >>"$LOGDATEI" 2>&1 || meldung 'Der Neubau der alten Version schlug fehl.' + docker compose up -d >>"$LOGDATEI" 2>&1 || meldung 'Der Start der alten Version schlug fehl.' + + if [ -n "$SICHERUNG" ]; then + meldung "Die Datenbank wurde NICHT zurückgespielt. Sicherung: $SICHERUNG" + meldung 'Wiederherstellen mit:' + meldung " docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists < \"$SICHERUNG\"" + fi + meldung "Vollständiges Protokoll: $LOGDATEI" +} + +# --- 5. Auschecken, bauen, starten ---------------------------------------- +meldung "Auf $ZIELVERSION wechseln." +if ! git checkout --quiet "$ZIELVERSION"; then + fehler "Der Wechsel auf $ZIELVERSION schlug fehl." +fi + +meldung 'Abbild bauen.' +if ! docker compose build >>"$LOGDATEI" 2>&1; then + meldung 'Der Bau ist fehlgeschlagen.' + zurueckrollen + exit 1 +fi + +meldung 'Dienste starten.' +if ! docker compose up -d >>"$LOGDATEI" 2>&1; then + meldung 'Der Start ist fehlgeschlagen.' + zurueckrollen + exit 1 +fi + +# --- 6. Auf Gesundheit warten --------------------------------------------- +meldung "Auf $HEALTH_URL warten (bis zu ${WARTEZEIT}s)." +ERWARTET="${ZIELVERSION#v}" +VERGANGEN=0 +GESUND=0 +while [ "$VERGANGEN" -lt "$WARTEZEIT" ]; do + ANTWORT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null || true)" + if [ -n "$ANTWORT" ]; then + STATUS="$(printf '%s' "$ANTWORT" | jq -r '.status // empty')" + LAEUFT="$(printf '%s' "$ANTWORT" | jq -r '.version // empty')" + if [ "$STATUS" = 'ok' ] && [ "$LAEUFT" = "$ERWARTET" ]; then + GESUND=1 + break + fi + fi + sleep 5 + VERGANGEN=$((VERGANGEN + 5)) +done + +# --- 7. Ergebnis ----------------------------------------------------------- +if [ "$GESUND" -eq 0 ]; then + meldung "Die neue Version meldet sich nicht gesund mit Version $ERWARTET." + docker compose logs --tail 50 app >>"$LOGDATEI" 2>&1 || true + zurueckrollen + exit 1 +fi + +meldung "Fertig. Der Planer läuft in Version $ERWARTET." +meldung "Protokoll: $LOGDATEI" diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..dfea4bc --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,41 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [react()], + resolve: { + // Loest den Alias @/ aus der tsconfig.json auf. + tsconfigPaths: true, + }, + test: { + // Die Zeitrechnung des Planers gilt in Europe/Berlin. Damit Tests nicht + // von der Zeitzone des Rechners abhaengen, laufen sie in UTC – so faellt + // sofort auf, wenn irgendwo die Ortszeit des Servers durchschlaegt. + env: { TZ: 'UTC' }, + coverage: { + provider: 'v8', + reportsDirectory: 'coverage', + include: ['src/lib/**/*.ts'], + exclude: ['**/*.test.ts'], + }, + projects: [ + { + extends: true, + test: { + name: 'logik', + environment: 'node', + include: ['src/lib/**/*.test.ts', 'src/server/**/*.test.ts'], + }, + }, + { + extends: true, + test: { + name: 'oberflaeche', + environment: 'jsdom', + include: ['src/components/**/*.test.tsx', 'src/app/**/*.test.tsx'], + setupFiles: ['./test/aufbau-oberflaeche.ts'], + }, + }, + ], + }, +})