diff --git a/.env.example b/.env.example index 6b80b28..18c19f7 100644 --- a/.env.example +++ b/.env.example @@ -15,9 +15,17 @@ DATABASE_URL="postgresql://cockpit:bitte-aendern@localhost:5432/betreuungs_cockp # 32 zufaellige Bytes, base64. Erzeugen: npm run schluessel AUTH_SECRET= # Oeffentliche Basis-URL der Anwendung (ohne abschliessenden Slash). +# Hinter einem Proxy die Adresse, die im Browser in der Adresszeile steht - +# nicht die interne. Passkeys pruefen dagegen. AUTH_URL=http://localhost:3000 -# Hinter einem Reverse Proxy auf true setzen. -AUTH_TRUST_HOST=true + +# Darf der Host-Kopf der Anfrage die Adresse bestimmen? +# +# Solange AUTH_URL gesetzt ist: nein, und die Zeile bleibt weg. Die Adresse +# ist dann bekannt und muss nicht aus einer Kopfzeile abgeleitet werden, die +# von aussen kommt. Ein "X-Forwarded-Host: fremde.example" schlaegt sonst in +# erzeugte Adressen durch. +# AUTH_TRUST_HOST=true # --- Erreichbarkeit: HIER MUSST DU DICH ENTSCHEIDEN ------------------------- # @@ -73,6 +81,25 @@ LOGIN_MAX_VERSUCHE=5 LOGIN_FENSTER_SEKUNDEN=900 LOGIN_SPERRE_SEKUNDEN=900 +# Geheimnis fuer die Ersteinrichtung. OHNE DIESEN WERT LAESST SICH DER ERSTE +# ZUGANG NICHT ANLEGEN - das ist Absicht. +# +# Zwischen dem ersten Start und dem Anlegen des Zugangs stand die +# Einrichtungsseite sonst offen: Wer sie in diesem Fenster findet, wird +# Administrator. Erzeugen: npm run schluessel +ERSTEINRICHTUNG_GEHEIMNIS= + +# Steht ein Reverse Proxy davor? +# +# Nur mit "true" wird "X-Forwarded-For" geglaubt - und dann von HINTEN +# gelesen, weil der vordere Teil der Liste vom Aufrufer selbst kommt. +# Ohne diese Zeile zaehlt die Anmeldesperre nur nach E-Mail-Adresse. +# +# Der dokumentierte Betrieb (Reverse Proxy davor) braucht sie also: +VERTRAUE_PROXY=true +# Bei mehreren gestaffelten Proxys deren Anzahl. Sonst weglassen. +# VERTRAUE_PROXY_STUFEN=1 + # --- Objektspeicher (S3-kompatibel, EU!) ------------------------------------ # Wird ab Etappe 3 verwendet. Beispiel: Hetzner Object Storage, Falkenstein. # ACHTUNG: Mit dem Anbieter muss ein AV-Vertrag nach Art. 28 DSGVO bestehen. diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c4020..0752505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,100 @@ Wiederherstellung*. _Noch nichts._ +## [1.11.0] – 2026-08-24 + +Diese Fassung geht aus einer Sicherheitsprüfung hervor. Sie ändert nichts +an der Bedienung, schließt aber mehrere Lücken — darunter drei, bei denen +im Code eine Zusage stand, die so nicht galt. + +### Vor dem Update: zwei neue Zeilen in der `.env` + +```bash +ERSTEINRICHTUNG_GEHEIMNIS= +VERTRAUE_PROXY=true +``` + +Beide sind nicht zwingend, aber beide haben Folgen, wenn sie fehlen: + +- Ohne `ERSTEINRICHTUNG_GEHEIMNIS` läuft alles weiter — nur der **erste** + Zugang ließe sich nicht mehr anlegen. Auf einer bestehenden Installation + ist das genau richtig; bei einer Neuinstallation brauchst du den Wert. +- Ohne `VERTRAUE_PROXY=true` zählt die Anmeldesperre nur noch nach + E-Mail-Adresse, nicht mehr zusätzlich nach Anschluss. Beim ersten Mal + steht eine erklärende Zeile im Log. + +### Nach dem Update, nur mit Objektspeicher + +Bereits abgelegte Dateien liegen weiter unverschlüsselt, bis sie einmal +umgeschrieben werden. Lesen funktioniert durchgehend, der Lauf lässt sich +jederzeit abbrechen und wiederholen: + +```bash +npm run speicher:verschluesseln # erst schauen +npm run speicher:verschluesseln -- --anwenden +``` + +Wer beim Dateitreiber bleibt, muss nichts tun — dort wurde immer schon +verschlüsselt. + +### Behoben + +- **Der zweite Faktor war eine Anzeige, keine Grenze.** „TOTP ist Pflicht" + galt nur für Seiten. Server Actions und Downloads liefen daran vorbei: + Wer sich mit Passwort angemeldet, die Einrichtung aber nie abgeschlossen + hatte, kam an die Daten. Die Prüfung sitzt jetzt an der Stelle, durch die + alles muss. + +- **Ein Dokumenten-Download fragte nur nach der Anmeldung.** Dieselbe + Lücke, an einer Stelle, die bei der ersten Durchsicht übersehen wurde. + +- **Die Anmeldesperre sperrte dauerhaft aus.** Jeder Versuch während einer + laufenden Sperre verlängerte sie um die volle Dauer. Wer alle zehn + Minuten ein falsches Passwort an eine fremde Adresse schickte, sperrte + deren Inhaber aus — ohne das Passwort zu kennen. Nebenbei wuchs die + Tabelle mit jedem Versuch. + +- **Ein Code aus der Authenticator-App galt mehrfach.** Bis zu 90 Sekunden + lang ließ sich derselbe Code erneut einlösen — abgelesen, abgefangen + oder aus dem Browserverlauf. Jetzt gilt jeder genau einmal. + +- **Passkeys ließen sich ohne Rückfrage anlegen und entfernen.** An einem + unbeaufsichtigten Rechner waren das zwei Klicks zu einem dauerhaften + eigenen Zugang, der auch einen Passwortwechsel überlebt hätte. Jetzt + fragt die Anwendung vorher nach Passwort und Code — wie beim Export. + +- **Die Ersteinrichtung stand offen.** Zwischen dem ersten Start und dem + Anlegen des Zugangs konnte jeder, der die Seite fand, Administrator + werden. Zwei gleichzeitige Anfragen legten außerdem beide einen Zugang + an. + +- **Dateien im Objektspeicher lagen mit dem Schlüssel des Anbieters da.** + Gegen eine verlorene Festplatte im Rechenzentrum half das, gegen den + Anbieter und gegen gestohlene Zugangsdaten zum Speicher nicht. Jetzt + liegt derselbe Umschlag darum wie bei der Ablage auf dem eigenen Server, + mit einem Schlüssel, der die Anwendung nie verlässt. + +- **Ein präpariertes PDF konnte den Server umbringen.** Beim Auslesen des + eingebetteten Texts gab es keine Obergrenze: Wenige Kilobyte Füllung + blähen sich beim Auspacken auf Gigabyte auf. Ebenso beim Umwandeln in + Bilder — die Seitengröße steht im Dokument, und bei fester Auflösung + ergab eine künstlich große Seite ein Bild von mehreren Gigabyte. + +- **32 Schwachstellen in mitgelieferten Paketen, davon 5 kritische.** + Jetzt keine. + +### Geändert + +- **Downloads laufen jetzt immer über die Anwendung**, auch mit + Objektspeicher. Das ist die Folge der Verschlüsselung — entschlüsseln + kann nur die Anwendung. Nebeneffekt: Jeder Abruf prüft die Berechtigung + und wird protokolliert. Ein weitergeleiteter Link allein öffnet nichts + mehr. + +- **`docs/SICHERHEIT.md`** ist neu: Bedrohungsmodell, der Durchgang entlang + der OWASP Top 10 — und ein Abschnitt darüber, was ausdrücklich **nicht** + geschützt ist. + ## [1.10.2] – 2026-08-24 ### Behoben @@ -1171,7 +1265,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). --- -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.2...HEAD +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.11.0...HEAD +[1.11.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.2...v1.11.0 [1.10.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.1...v1.10.2 [1.10.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...v1.10.1 [1.10.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.9.0...v1.10.0 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index d5c2955..f1e307a 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -156,6 +156,7 @@ const w = n => b(n).replace(/[+/=]/g,'x'); console.log('AUTH_SECRET=' + b(32)); console.log('ENC_KEY_V1=' + b(32)); console.log('INDEX_KEY=' + b(32)); +console.log('ERSTEINRICHTUNG_GEHEIMNIS=' + w(24)); console.log('POSTGRES_PASSWORD=' + w(24)); console.log('SICHERUNG_PASSWORT=' + w(24)); console.log('S3_ACCESS_KEY_ID=' + w(12)); @@ -192,7 +193,15 @@ POSTGRES_PASSWORD= # --- Anmeldung --- AUTH_SECRET= AUTH_URL=https://cockpit.deine-domain.de # exakt deine Adresse, mit https -AUTH_TRUST_HOST=true + +# Ohne diesen Wert laesst sich der erste Zugang nicht anlegen. Absicht: +# sonst steht die Einrichtungsseite bis zum ersten Anlegen fuer jeden offen. +ERSTEINRICHTUNG_GEHEIMNIS= + +# Ein Reverse Proxy steht davor (Schritt 7). Nur damit wird der +# weitergereichte Kopf geglaubt, aus dem die Anmeldesperre den Anschluss +# liest. Ohne die Zeile zaehlt sie nur nach E-Mail-Adresse. +VERTRAUE_PROXY=true # --- Verschlüsselung --- ENC_KEY_V1= @@ -258,12 +267,16 @@ chmod 600 .env ```bash # Sind alle Pflichtwerte gefüllt? Zeigt nur die Namen, nicht die Werte. -grep -E '^(POSTGRES_PASSWORD|AUTH_SECRET|AUTH_URL|ENC_KEY_V1|INDEX_KEY|S3_ACCESS_KEY_ID|S3_SECRET_ACCESS_KEY|SICHERUNG_PASSWORT)=.+' .env \ +grep -E '^(POSTGRES_PASSWORD|AUTH_SECRET|AUTH_URL|ERSTEINRICHTUNG_GEHEIMNIS|VERTRAUE_PROXY|ENC_KEY_V1|INDEX_KEY|S3_ACCESS_KEY_ID|S3_SECRET_ACCESS_KEY|SICHERUNG_PASSWORT)=.+' .env \ | sed 's/=.*/ = gesetzt/' ``` -Acht Zeilen müssen erscheinen. Fehlt eine, startet die Anwendung nicht — -und sagt dir beim Start auch, welche. +Zehn Zeilen müssen erscheinen. Fehlt eine, startet die Anwendung nicht — +und sagt dir beim Start auch, welche. Zwei Ausnahmen: Fehlt +`ERSTEINRICHTUNG_GEHEIMNIS`, startet die Anwendung, aber der erste Zugang +lässt sich nicht anlegen. Fehlt `VERTRAUE_PROXY`, läuft alles — die +Anmeldesperre zählt dann nur nach E-Mail-Adresse und schreibt beim ersten +Mal eine Zeile ins Log. --- diff --git a/docs/SICHERHEIT.md b/docs/SICHERHEIT.md new file mode 100644 index 0000000..76d6ab1 --- /dev/null +++ b/docs/SICHERHEIT.md @@ -0,0 +1,168 @@ +# Sicherheit + +Was in dieser Anwendung gegen welche Bedrohung steht — und was +ausdrücklich **nicht**. Der zweite Teil ist der wichtigere: Eine Zusage, +die nicht gilt, ist gefährlicher als eine fehlende, weil man sich auf sie +verlässt. + +Stand: v1.11.0. Die Fassung entstand beim Abarbeiten von Issue #38 +(Sicherheitsprüfung durch ein zweites Modell) und dem eigenen Durchgang +entlang der OWASP Top 10 (2021). + +--- + +## Das Bedrohungsmodell + +Eine ehrenamtliche rechtliche Betreuung. Ein Benutzer, eine betreute +Person, ein kleiner Server hinter einem Reverse Proxy. In der Datenbank +liegen Gesundheitsdaten (Art. 9 DSGVO), Kontobewegungen und +Vermögensverzeichnisse. + +Nach Wahrscheinlichkeit geordnet: + +| Was | Wie wahrscheinlich | Was dagegen steht | +|---|---|---| +| Offene Sitzung an einem unbeaufsichtigten Rechner | hoch | Abmeldung nach 30 min Leerlauf; erneute Bestätigung vor Export, Löschen, Benutzer- und Passkey-Verwaltung | +| Verlorenes oder gestohlenes Gerät | mittel | Zweiter Faktor ist Pflicht; Passkeys verlangen Nutzerverifikation | +| Kopiertes Backup, falsch gerechtetes Verzeichnis | mittel | Feldverschlüsselung (AES-256-GCM), Dateien im Umschlag, Schlüssel nur in der Umgebung | +| Zugangsdaten zum Objektspeicher abhandengekommen | mittel | Objekte liegen verschlüsselt, an ihren Ablageort gebunden | +| Raten von Passwörtern über das Formular | mittel | Anmeldesperre nach E-Mail **und** Anschluss, Argon2id | +| Einbruch in den Server selbst | gering | Kaum etwas — wer dort Wurzelrechte hat, hat die Schlüssel | + +Der letzte Punkt ist keine Nachlässigkeit, sondern eine Grenze: Die +Anwendung braucht die Schlüssel zur Laufzeit. Wer den Prozess kontrolliert, +kommt an sie heran. Dagegen hilft Betriebssicherheit, kein Anwendungscode. + +--- + +## Was ausdrücklich nicht geschützt ist + +- **Der Server selbst.** Siehe oben. +- **Metadaten in der Datenbank.** Verschlüsselt sind einzelne Felder — + Diagnosen, IBANs, Verwendungszwecke, Notizen. Dass es einen Vorgang + „Betreuungsgericht" gibt und wann er angelegt wurde, steht im Klartext. + Sonst ließe sich nichts mehr sortieren oder filtern. +- **Symlinks im Ablageverzeichnis.** `resolve()` folgt keinen Links. Wer + dort einen anlegen kann, hat bereits Schreibrechte im Datenverzeichnis — + dann ist der Umweg über einen Link die geringste Sorge. Bis v1.10.2 stand + im Code das Gegenteil; der Satz ist entfernt. +- **Die Middleware prüft keine Anmeldung.** Sie setzt Kopfzeilen, mehr + nicht. Der Zugriffsschutz sitzt in `src/lib/auth/sitzung.ts`, und nur + dort. Bis v1.10.2 behauptete ein Kommentar das Gegenteil. +- **Verfügbarkeit.** Es gibt keinen Schutz gegen jemanden, der den Server + mit Anfragen überzieht. Bei einem Nutzer hinter einem Proxy ist das die + Aufgabe des Proxys. + +--- + +## Der Durchgang entlang der OWASP Top 10 (2021) + +### A01 Broken Access Control + +Jede Server Action und jede Seite geht durch `erfordereZugang()`, das +zeilenweise gegen die `betreuteId` prüft — nie nur im UI ausgeblendet. +Fremdschlüssel aus Formularen werden über `src/lib/auth/verweise.ts` +geprüft; ein verändertes Auswahlfeld kann keine fremde Akte verknüpfen +(e2e-Test `fremdakte.spec.ts`). + +Weiterleitungsziele laufen durch `sicheresZiel()` — kein offener Redirect. + +**Behoben in dieser Runde:** Die MFA-Pflicht galt nur für Seiten, nicht +für Server Actions und Route Handler. Sie steckt jetzt in +`erfordereBenutzer()` selbst. `tests/mfa-grenze.test.ts` liest den +Quelltext und schlägt an, sobald eine weitere Umgehung dazukommt. + +### A02 Cryptographic Failures + +- Felder: AES-256-GCM, Umschlag `bc1....`, Feldname + als Zusatzdaten — ein Geheimtext lässt sich nicht von einem Feld in ein + anderes kopieren. +- Dateien: AES-256-GCM, Umschlag `BCD1`, seit v1.11.0 **beide** Treiber, + mit dem Ablageschlüssel als Zusatzdaten. +- Passwörter: Argon2id, Parameter im Hash, Nachziehen beim Anmelden. +- Schlüsselrotation über `ENC_KEY_V` / `ENC_KEY_AKTIV`; alte Schlüssel + bleiben zum Entschlüsseln stehen. +- Alle Geheimnisvergleiche laufen über `timingSafeEqual` — geprüft für + Reauth-Cookie, Zugriffstoken, ICS-Signatur, Wartungsschlüssel und das + Einrichtungsgeheimnis. + +**Behoben in dieser Runde:** Der Objektspeicher verließ sich auf +`ServerSideEncryption: 'AES256'` — den Schlüssel des Anbieters. + +### A03 Injection + +Prisma parametrisiert. Die fünf Rohabfragen benutzen ausnahmslos +`Prisma.sql`-Templates; `Prisma.join()` in `suche.ts` fügt nur bereits +parametrisierte Fragmente zusammen und ist damit unbedenklich. + +Zu XSS: `ts_headline` liefert ``-Marker im Suchausschnitt, und die +Quelle ist der OCR-Text hochgeladener Dokumente. Gerendert wird er in +`dokument-liste.tsx` aber nicht als HTML, sondern zerlegt und als React- +Kinder ausgegeben — also escaped. Das einzige +`dangerouslySetInnerHTML` der Anwendung setzt eine feste Konstante für die +Themenumschaltung. **Kein Fund.** + +### A04 Insecure Design + +Erneute Bestätigung vor den folgenschweren Handlungen. Anmeldesperre nach +zwei Dimensionen. Löschen ist ein Papierkorb mit 30 Tagen Frist. + +**Behoben in dieser Runde:** Passkeys ließen sich ohne erneute +Bestätigung anlegen und entfernen — ein Passkey ist ein vollwertiger +Zugang, der einen Passwortwechsel überlebt. + +### A05 Security Misconfiguration + +CSP mit Nonce pro Anfrage und `strict-dynamic` im Produktivbetrieb; dazu +`X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy: +no-referrer`, `Permissions-Policy`, HSTS mit zwei Jahren. +`poweredByHeader` aus. + +**Behoben in dieser Runde:** `trustHost` stand fest auf `true`, obwohl +`AUTH_URL` gesetzt ist. `X-Forwarded-For` wurde ohne Proxy geglaubt und +aus dem falschen Ende gelesen. + +### A06 Vulnerable and Outdated Components + +`npm audit`: 0 Schwachstellen (vorher 32, davon 5 kritisch). + +### A07 Identification and Authentication Failures + +Passwort + TOTP in einem Schritt, kein Zwischenzustand. Passkeys mit +erzwungener Nutzerverifikation zählen als zwei Faktoren. +`sitzungsGeneration` macht alle Sitzungen auf einen Schlag ungültig. +Antwortzeiten sind angeglichen, damit sich keine E-Mail-Adressen abfragen +lassen. + +**Behoben in dieser Runde:** TOTP-Codes galten mehrfach; die Anmeldesperre +verlängerte sich selbst; die Ersteinrichtung stand offen und war nicht +atomar. + +### A08 Software and Data Integrity Failures + +Zu jedem abgelegten Dokument steht eine SHA-256-Prüfsumme über den +Klartext in der Datenbank — der Nachweis der Unverändertheit gegenüber dem +Gericht. Der Umschlag ist authentifiziert (GCM), Veränderungen fallen beim +Lesen auf. + +### A09 Security Logging and Monitoring Failures + +`AuditLog` hält Anmeldungen, Fehlversuche, Exporte, Löschungen und +Passkey-Änderungen fest. Anmeldeversuche stehen zusätzlich in +`login_versuch` und werden nach 30 Tagen aufgeräumt. + +**Behoben in dieser Runde:** Während einer Sperre entstand pro Anfrage +eine Zeile — die Tabelle ließ sich vollschreiben. Jetzt eine je Sperre. + +### A10 Server-Side Request Forgery + +Die Anwendung ruft nichts von außen ab: kein `fetch`, keine +HTTP-Bibliothek, keine Webhooks, keine URL-Vorschauen. Ausgehend geht nur +SMTP an den konfigurierten Server. **Keine Angriffsfläche.** + +--- + +## Wenn etwas gefunden wird + +Kein öffentliches Issue. Der Weg führt über das Dashboard; dort wird +entschieden, ob und wie es hier landet. diff --git a/package-lock.json b/package-lock.json index 053c988..8e4c189 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "betreuungs-cockpit", - "version": "1.0.5", + "version": "1.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "betreuungs-cockpit", - "version": "1.0.5", + "version": "1.10.2", "hasInstallScript": true, "dependencies": { - "@aws-sdk/client-s3": "3.735.0", - "@aws-sdk/s3-request-presigner": "3.735.0", + "@aws-sdk/client-s3": "3.1116.0", + "@aws-sdk/s3-request-presigner": "3.1116.0", "@node-rs/argon2": "2.0.2", "@prisma/client": "6.3.1", "@radix-ui/react-dialog": "1.1.4", @@ -26,9 +26,9 @@ "date-fns": "4.1.0", "docx": "9.1.1", "lucide-react": "0.474.0", - "next": "15.1.6", - "next-auth": "5.0.0-beta.25", - "nodemailer": "6.10.0", + "next": "15.5.23", + "next-auth": "5.0.0-beta.32", + "nodemailer": "9.0.5", "otpauth": "9.3.6", "pdf-lib": "1.17.1", "qrcode": "1.5.4", @@ -41,22 +41,22 @@ "zod": "3.24.1" }, "devDependencies": { - "@playwright/test": "1.50.1", + "@playwright/test": "1.62.1", "@types/archiver": "6.0.3", "@types/node": "22.13.1", - "@types/nodemailer": "6.4.17", + "@types/nodemailer": "7.0.4", "@types/qrcode": "1.5.5", "@types/react": "19.0.8", "@types/react-dom": "19.0.3", "autoprefixer": "10.4.20", - "eslint": "9.19.0", - "eslint-config-next": "15.1.6", - "postcss": "8.5.1", + "eslint": "9.39.1", + "eslint-config-next": "15.5.23", + "postcss": "8.5.26", "prisma": "6.3.1", "tailwindcss": "3.4.17", - "tsx": "4.19.2", + "tsx": "4.20.6", "typescript": "5.7.3", - "vitest": "3.0.5" + "vitest": "3.2.6" }, "engines": { "node": ">=20.11" @@ -75,23 +75,21 @@ } }, "node_modules/@auth/core": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", - "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "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", - "@types/cookie": "0.6.0", - "cookie": "0.7.1", - "jose": "^5.9.3", - "oauth4webapi": "^3.0.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" + "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": "^6.8.0" + "nodemailer": "^7.0.7 || ^8.0.5" }, "peerDependenciesMeta": { "@simplewebauthn/browser": { @@ -105,811 +103,348 @@ } } }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.735.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.735.0.tgz", - "integrity": "sha512-6NcxX06c4tnnu6FTFiyS8shoYLy+8TvIDkYjJ5r9tvbaysOptUKQdolOuh7+Lz95QyaqiznpCsNTxsfywLXcqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.734.0", - "@aws-sdk/middleware-bucket-endpoint": "3.734.0", - "@aws-sdk/middleware-expect-continue": "3.734.0", - "@aws-sdk/middleware-flexible-checksums": "3.735.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-location-constraint": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-sdk-s3": "3.734.0", - "@aws-sdk/middleware-ssec": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/signature-v4-multi-region": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@aws-sdk/xml-builder": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/eventstream-serde-browser": "^4.0.1", - "@smithy/eventstream-serde-config-resolver": "^4.0.1", - "@smithy/eventstream-serde-node": "^4.0.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-blob-browser": "^4.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/hash-stream-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/md5-js": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-stream": "^4.0.2", - "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.734.0.tgz", - "integrity": "sha512-SxnDqf3vobdm50OLyAKfqZetv6zzwnSqwIwd3jrbopxxHKqNIM/I0xcYjD6Tn+mPig+u7iRKb9q3QnEooFTlmg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.734.0.tgz", - "integrity": "sha512-gtRkzYTGafnm1FPpiNO8VBmJrYMoxhDlGPYDVcijzx3DlF8dhWnowuSBCxLSi+MJMx5hvwrX2A+e/q0QAeHqmw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.734.0.tgz", - "integrity": "sha512-JFSL6xhONsq+hKM8xroIPhM5/FOhiQ1cov0lZxhzZWj6Ai3UAjucy3zyIFDr9MgP1KfCYNdvyaUq9/o+HWvEDg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.2", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.734.0.tgz", - "integrity": "sha512-HEyaM/hWI7dNmb4NhdlcDLcgJvrilk8G4DQX6qz0i4pBZGC2l4iffuqP8K6ZQjUfz5/6894PzeFuhTORAMd+cg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-env": "3.734.0", - "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-process": "3.734.0", - "@aws-sdk/credential-provider-sso": "3.734.0", - "@aws-sdk/credential-provider-web-identity": "3.734.0", - "@aws-sdk/nested-clients": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.734.0.tgz", - "integrity": "sha512-9NOSNbkPVb91JwaXOhyfahkzAwWdMsbWHL6fh5/PHlXYpsDjfIfT23I++toepNF2nODAJNLnOEHGYIxgNgf6jQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.734.0", - "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-ini": "3.734.0", - "@aws-sdk/credential-provider-process": "3.734.0", - "@aws-sdk/credential-provider-sso": "3.734.0", - "@aws-sdk/credential-provider-web-identity": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.734.0.tgz", - "integrity": "sha512-zvjsUo+bkYn2vjT+EtLWu3eD6me+uun+Hws1IyWej/fKFAqiBPwyeyCgU7qjkiPQSXqk1U9+/HG9IQ6Iiz+eBw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.734.0.tgz", - "integrity": "sha512-cCwwcgUBJOsV/ddyh1OGb4gKYWEaTeTsqaAK19hiNINfYV/DO9r4RMlnWAo84sSBfJuj9shUNsxzyoe6K7R92Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.734.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/token-providers": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.734.0.tgz", - "integrity": "sha512-t4OSOerc+ppK541/Iyn1AS40+2vT/qE+MFMotFkhCgCJbApeRF2ozEdnDN6tGmnl4ybcUuxnp9JWLjwDVlR/4g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/nested-clients": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.734.0.tgz", - "integrity": "sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.734.0.tgz", - "integrity": "sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.735.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.735.0.tgz", - "integrity": "sha512-Tx7lYTPwQFRe/wQEHMR6Drh/S+X0ToAEq1Ava9QyxV1riwtepzRLojpNDELFb3YQVVYbX7FEiBMCJLMkmIIY+A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.0.2", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", - "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.734.0.tgz", - "integrity": "sha512-EJEIXwCQhto/cBfHdm3ZOeLxd2NlJD+X2F+ZTOxzokuhBtY0IONfC/91hOo5tWQweerojwshSMHRCKzRv1tlwg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", - "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", - "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.734.0.tgz", - "integrity": "sha512-zeZPenDhkP/RXYMFG3exhNOe2Qukg2l2KpIjxq9o66meELiTULoIXjCmgPoWcM8zzrue06SBdTsaJDHfDl2vdA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/core": "^3.1.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.0.2", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.734.0.tgz", - "integrity": "sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.734.0.tgz", - "integrity": "sha512-MFVzLWRkfFz02GqGPjqSOteLe5kPfElUrXZft1eElnqulqs6RJfVSpOV7mO90gu293tNAeggMWAVSGRPKIYVMg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@smithy/core": "^3.1.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.734.0.tgz", - "integrity": "sha512-iph2XUy8UzIfdJFWo1r0Zng9uWj3253yvW9gljhtu+y/LNmNvSnJxQk1f3D2BC5WmcoPZqTS3UsycT3mLPSzWA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", - "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.735.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.735.0.tgz", - "integrity": "sha512-PzfS4rWDLlp22NORWmezA8ZH6uwz7fAmYfdIbWsPKoy1Rpm+/6Kqn7Nx+Taz6UKNhGPtexutCoJqsMxCy0ZmxQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-format-url": "3.734.0", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.734.0.tgz", - "integrity": "sha512-GSRP8UH30RIYkcpPILV4pWrKFjRmmNjtUd41HTKWde5GbjJvNYpxqFXw2aIJHjKTw/js3XEtGSNeTaQMVVt3CQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.734.0.tgz", - "integrity": "sha512-2U6yWKrjWjZO8Y5SHQxkFvMVWHQWbS0ufqfAIBROqmIZNubOL7jXCiVdEFekz6MZ9LF2tvYGnOW4jX8OKDGfIw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/nested-clients": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.723.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.723.0.tgz", - "integrity": "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.734.0.tgz", - "integrity": "sha512-w2+/E88NUbqql6uCVAsmMxDQKu7vsKV0KqhlQb0lL+RCq4zy07yXYptVNs13qrnuTfyX7uPXkXrlugvK9R1Ucg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.734.0.tgz", - "integrity": "sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.10.tgz", - "integrity": "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==", + "node_modules/@aws-sdk/checksums": { + "version": "3.1000.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.29.tgz", + "integrity": "sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", - "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", + "node_modules/@aws-sdk/client-s3": { + "version": "3.1116.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1116.0.tgz", + "integrity": "sha512-UKRl9qSVW0rZpvSOauQNpYAy8+ONBAVYnpfKVtCyOF+FZVT1tl6MunYuHvuarCrroD2/YJs+tHTALYNgAlec3Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.734.0.tgz", - "integrity": "sha512-c6Iinh+RVQKs6jYUFQ64htOU2HUXFQ3TVx+8Tu3EDF19+9vzWi9UukhIMH9rqyyEXIAkk9XL7avt8y2Uyw2dGA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/checksums": "^3.1000.29", + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/credential-provider-node": "^3.972.81", + "@aws-sdk/middleware-sdk-s3": "^3.972.75", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.1116.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.1116.0.tgz", + "integrity": "sha512-5j2Zu5e6I0GbvrpSe86+z4I760TWGiypkBTNVGF+B46h8CkcWACG8EMwi0xVpXbjAQm1sBRvIFoinIJaEP0qcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/credential-provider-node": "^3.972.81", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.977.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.9.tgz", + "integrity": "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@aws-sdk/xml-builder": "^3.972.40", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.33.3", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.70", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.70.tgz", + "integrity": "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.72.tgz", + "integrity": "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.15.tgz", + "integrity": "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/credential-provider-env": "^3.972.70", + "@aws-sdk/credential-provider-http": "^3.972.72", + "@aws-sdk/credential-provider-login": "^3.972.77", + "@aws-sdk/credential-provider-process": "^3.972.70", + "@aws-sdk/credential-provider-sso": "^3.973.14", + "@aws-sdk/credential-provider-web-identity": "^3.972.76", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.77.tgz", + "integrity": "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.81", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.81.tgz", + "integrity": "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.70", + "@aws-sdk/credential-provider-http": "^3.972.72", + "@aws-sdk/credential-provider-ini": "^3.973.15", + "@aws-sdk/credential-provider-process": "^3.972.70", + "@aws-sdk/credential-provider-sso": "^3.973.14", + "@aws-sdk/credential-provider-web-identity": "^3.972.76", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.70", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.70.tgz", + "integrity": "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.14.tgz", + "integrity": "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/token-providers": "3.1116.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.76.tgz", + "integrity": "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.75", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.75.tgz", + "integrity": "sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.44.tgz", + "integrity": "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1116.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1116.0.tgz", + "integrity": "sha512-WwaaVpvrZyML5L8SNY7sUGsYlMeCHzQ/8A/ms7dz/7sfYRvPn+qf0OasUWk+zuT8qGwjsYhILD0WVtlqUU08pQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz", + "integrity": "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1116.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1116.0.tgz", + "integrity": "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.5.tgz", + "integrity": "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.734.0.tgz", - "integrity": "sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz", + "integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } @@ -955,9 +490,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -972,9 +507,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -989,9 +524,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -1006,9 +541,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -1023,9 +558,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -1040,9 +575,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -1057,9 +592,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -1074,9 +609,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -1091,9 +626,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -1108,9 +643,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -1125,9 +660,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -1142,9 +677,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -1159,9 +694,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -1176,9 +711,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -1193,9 +728,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -1210,9 +745,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -1227,9 +762,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -1261,9 +796,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -1278,9 +813,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -1295,9 +830,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -1329,9 +864,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -1346,9 +881,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -1363,9 +898,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -1380,9 +915,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -1439,24 +974,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "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.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "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": { @@ -1491,13 +1039,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "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": { @@ -1511,32 +1062,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "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.13.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "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/@floating-ui/core": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", @@ -1647,10 +1185,20 @@ "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.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -1660,19 +1208,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -1682,19 +1230,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -1708,9 +1275,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -1724,9 +1291,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -1743,9 +1310,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -1761,10 +1328,48 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "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.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -1781,9 +1386,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -1800,9 +1405,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -1819,9 +1424,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -1838,9 +1443,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -1853,19 +1458,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -1878,19 +1483,69 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "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.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "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.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -1903,19 +1558,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -1928,19 +1583,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -1953,19 +1608,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -1978,38 +1633,73 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.11.1" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "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.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -2019,16 +1709,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -2038,7 +1728,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -2061,79 +1751,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2208,15 +1825,15 @@ } }, "node_modules/@next/env": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", - "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.23.tgz", + "integrity": "sha512-Mv3Z9hVbFcPnoLevsZ6rnX1TBtyHb5E17yN7HTPDXSXxeNsGBjUFrdbjRXKKXIOhfth7/cg6Ay7PZ2UFawaWsQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.6.tgz", - "integrity": "sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.23.tgz", + "integrity": "sha512-0KnCFpiWVIsbwBhByZ0uIcjYM5xqGrFzN2eOPbwru/wuy5Z1dmA+3gP+PRbi4gl1Ny7an66BqAM/NHkX/50rbw==", "dev": true, "license": "MIT", "dependencies": { @@ -2224,9 +1841,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", - "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.23.tgz", + "integrity": "sha512-SrEwOROH/rhA03F59hHtdhgtfZMWGzr5duDBWgRQt2rS3mJhqMKOcnNx6txOd0/i3E3D3uFKYFvyHsEiwQxzag==", "cpu": [ "arm64" ], @@ -2240,9 +1857,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", - "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.23.tgz", + "integrity": "sha512-f0FpFbG2EhDCuptBGcfrLcYMDuQAhe6m1QA4VVfXFrIBoFXvXt/olGbBkYkloKlXQtmhuzvtdYyuu/6zf07GIg==", "cpu": [ "x64" ], @@ -2256,9 +1873,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", - "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.23.tgz", + "integrity": "sha512-WlNtfepUXKX2u2ZsJZ8c3c8+tJSRZqsYzoMwLOY72A8ucKCCgxgNhiePA3qzFYahVWrwcQd8jOeJmBinc+VFVQ==", "cpu": [ "arm64" ], @@ -2275,9 +1892,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", - "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.23.tgz", + "integrity": "sha512-W/6qKk7UG93mg14PmQC+2urt69MIdwTBLNQ6MJyeC4wOCIHCjz+VfgssvS1pK7mgYBtLC1g6VKNoHD9xB0WWGg==", "cpu": [ "arm64" ], @@ -2294,9 +1911,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", - "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.23.tgz", + "integrity": "sha512-vzefI32mi6VMk96RaTAyxApgfGbiFzQBXVsekEjsDv1fr48mlABTWx0sUYhaYCBHWqCalxmz3DxbxFcbFvzNtw==", "cpu": [ "x64" ], @@ -2313,9 +1930,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", - "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.23.tgz", + "integrity": "sha512-qppK/3dTGOTI+aoWWBZc3DshFIhrzgL8guATlaN9V6M1QJxbkP/rhEZ22tdICsQ/2WWXopMZ2Jokzj2u3uKY3Q==", "cpu": [ "x64" ], @@ -2332,9 +1949,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", - "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.23.tgz", + "integrity": "sha512-Wc29KFOdT7XBcII3Vtmw7aoU8Uk3Mes/FNJfhFeSHdYBFJWMcR/DsI8U9BCPUhq/uycsUVuqSKGthW15tLsigA==", "cpu": [ "arm64" ], @@ -2348,9 +1965,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", - "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.23.tgz", + "integrity": "sha512-/C7wRW4fa9s/PKA18zGPPpVmx8ycgVpP8yOxro4gzGTzjPJdscbAP3ODeFvgiIovxD176Z2J/SXO9t8PJKHLeQ==", "cpu": [ "x64" ], @@ -2932,19 +2549,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", - "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.50.1" + "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@prisma/client": { @@ -3609,13 +3226,22 @@ "is-url": "^1.2.4" } }, - "node_modules/@react-pdf/image": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.1.1.tgz", - "integrity": "sha512-pbeb2qUs+lDZsvNpWSjoVkAm10WjXsoYd5jIB3utqkfWwwMkh3qo2Iz4By08AptAcRINPXaG6ICT3zsrXBMb6A==", + "node_modules/@react-pdf/hyphenate": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/hyphenate/-/hyphenate-0.1.0.tgz", + "integrity": "sha512-CWulbuusHh2Lnos9ZffT3ZYfjCt332Yl8wjSyCKWbwhbTpe/VdFt53fM5elPp3HU3R6tzH6j3oYlLXDwC2WEHQ==", "license": "MIT", "dependencies": { - "@react-pdf/svg": "^1.1.0", + "hyphen": "~1.6.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.1.2.tgz", + "integrity": "sha512-89vlvZCCv1hPunFtyeS2rJTRL8h2yYmTI97AM4ppibS79zGsPFbqz/YrAunndcHB9uwGdn5S3+5k18mj0L2vkg==", + "license": "MIT", + "dependencies": { + "@react-pdf/svg": "^1.1.1", "jay-peg": "^1.1.1", "png-js": "^2.0.0" } @@ -3663,9 +3289,9 @@ } }, "node_modules/@react-pdf/primitives": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.3.0.tgz", - "integrity": "sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.4.0.tgz", + "integrity": "sha512-BFpuhNH6ffSFjTTMnpdUoZxWoXdhPmFDdrSVBl0i/zMR4yDTEfYOW3AcfjjmNIOpm9+LnIXbgELLklQJ+nD3oA==", "license": "MIT" }, "node_modules/@react-pdf/reconciler": { @@ -3681,23 +3307,17 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-pdf/reconciler/node_modules/scheduler": { - "version": "0.25.0-rc-603e6108-20241029", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", - "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", - "license": "MIT" - }, "node_modules/@react-pdf/render": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.6.1.tgz", - "integrity": "sha512-jDPxvtrdETDd+pujEPvCWCiBasZxqf9IDEQUYyD9SiFEaP/FoTLh0sCGTJMG3tlzSPi/jM5Hqduop0RMVlMisg==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.6.3.tgz", + "integrity": "sha512-/UHjsNKCBZu10SSTwYft6u2i2/xO7baoY7Mhel7n2K6fpeEHAqd5OI54YeQX8m7fDaoe+CsAv5bZZgEUb0IMqA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.3", - "@react-pdf/primitives": "^4.3.0", - "@react-pdf/textkit": "^6.4.1", - "@react-pdf/types": "^2.11.3", + "@react-pdf/primitives": "^4.4.0", + "@react-pdf/textkit": "^7.0.0", + "@react-pdf/types": "^2.13.0", "abs-svg-path": "^0.1.1", "color-string": "^2.1.4", "normalize-svg-path": "^1.1.0", @@ -3705,25 +3325,16 @@ "svg-arc-to-cubic-bezier": "^3.2.0" } }, - "node_modules/@react-pdf/render/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/@react-pdf/render/node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "node_modules/@react-pdf/render/node_modules/@react-pdf/textkit": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-7.0.0.tgz", + "integrity": "sha512-HvLdU0+n4eN4tqzSCTezAzsCy2Osj416lfYbPszNRHZJ7jsQfSP5K1rIEtIC9sDaURQifBs8rdhfKZ3IB055sw==", "license": "MIT", "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" + "@react-pdf/fns": "3.1.3", + "@react-pdf/hyphenate": "^0.1.0", + "bidi-js": "^1.0.2", + "unicode-properties": "^1.4.1" } }, "node_modules/@react-pdf/renderer": { @@ -3750,99 +3361,61 @@ } }, "node_modules/@react-pdf/stylesheet": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.2.3.tgz", - "integrity": "sha512-To5bbdM5KyOwkY+GqWdZSq6TlgBe+tQc/r00H1gF6RnqucMfkriRRgqXbYNlH2+RBOg5jltq7IhYARyyuge+1g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.3.0.tgz", + "integrity": "sha512-BJu8NO4fnCxaQdIjTFQ1V1K58tLrqe2V1rs89s1AQgbU/EFz3NWL2iSJSOLOyMqQSQW++8EuBQ14v0OWmnXoAQ==", "license": "MIT", "dependencies": { "@react-pdf/fns": "3.1.3", - "@react-pdf/types": "^2.11.3", + "@react-pdf/types": "^2.13.0", "color-string": "^2.1.4", "hsl-to-hex": "^1.0.0", - "media-engine": "^1.0.3", + "media-engine": "^2.0.0", "postcss-value-parser": "^4.1.0" } }, - "node_modules/@react-pdf/stylesheet/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/@react-pdf/stylesheet/node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@react-pdf/svg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@react-pdf/svg/-/svg-1.1.0.tgz", - "integrity": "sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/svg/-/svg-1.1.1.tgz", + "integrity": "sha512-m1GmGxV2wg/3VpoQ0aCJ598fBedCCfN+HtxKx1lq5kdwUWEaTbfXI1DGFAUedprFDd/i24okKFaUhV/KVZIqIQ==", "license": "MIT", "dependencies": { - "@react-pdf/primitives": "^4.3.0" + "@react-pdf/primitives": "^4.4.0" } }, "node_modules/@react-pdf/textkit": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.4.1.tgz", - "integrity": "sha512-wjQu/f1pKxyC2iRPJrnIm4Sc0tEmofsWtHdiyHP4NUlfN9uX4l7yVihPDWUWfoVPWB1QnTOTAIKv1vqRlGgoSg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.4.2.tgz", + "integrity": "sha512-NcyM2/HKud/JSNxnq7gLrp0Y9gm7+WKKv3hAZsXPDMvywERLEUk41QPphhSGj4nEdjfCPlMr3v8LDSqe06vUow==", "license": "MIT", "dependencies": { "@react-pdf/fns": "3.1.3", + "@react-pdf/hyphenate": "^0.1.0", "bidi-js": "^1.0.2", - "hyphen": "^1.6.4", "unicode-properties": "^1.4.1" } }, "node_modules/@react-pdf/types": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.11.3.tgz", - "integrity": "sha512-/3jG6KUOsSERDDmz2zq+pLI2NcHqupzYtLP+hEnO39FgzWUpAnD44eJdoDtARkoqX/Yhahu4Luu8N6XK9u0/pw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.13.0.tgz", + "integrity": "sha512-0e2309nzrdLvIFid3tl6n/FMm+qqngXKZlHeUkN8gtugXEDAMJFsxA9g3vyLYiSDPrLSyKDdh04W5u4LP6bnpA==", "license": "MIT", "dependencies": { - "@react-pdf/font": "^4.0.10", - "@react-pdf/primitives": "^4.3.0", - "@react-pdf/stylesheet": "^6.2.3" + "@react-pdf/font": "^4.1.0", + "@react-pdf/primitives": "^4.4.0", + "@react-pdf/stylesheet": "^6.3.0" } }, "node_modules/@react-pdf/types/node_modules/@react-pdf/font": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.10.tgz", - "integrity": "sha512-8Bg7PvtjFumVY8RnIpkbgLIwX3ZlMjrQemGl+sVz1Mx/hi8o8Bj+M9jVCWFaNXLbKAA06YxOlAMhfLEPT1+GLg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.1.0.tgz", + "integrity": "sha512-BaZzvH3xT++IYbAbRJHP6GyTrnk6vFmYEemGdieiJ2Ckp4r638oJa6x8oQqX8XWi22xQkaN4WMRddhnmsjM1Ig==", "license": "MIT", "dependencies": { - "@react-pdf/pdfkit": "^6.0.1", - "@react-pdf/types": "^2.11.3", + "@react-pdf/types": "^2.13.0", "fontkit": "^2.0.2", - "is-url": "^1.2.4" - } - }, - "node_modules/@react-pdf/types/node_modules/@react-pdf/pdfkit": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-6.0.1.tgz", - "integrity": "sha512-emZdaG6L/T+QP9rO4n7LAe3lbVXOy4X/C1UpgbCLxDJXsHiYBuZ8unPBv4/Tdte5hPm/Xc2KT1JEL74RSHE/Fw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@noble/ciphers": "^1.0.0", - "@noble/hashes": "^1.6.0", - "fflate": "^0.8.2", - "fontkit": "^2.0.2", - "js-md5": "^0.8.3", - "linebreak": "^1.1.0", - "png-js": "^2.0.0", - "vite-compatible-readable-stream": "^3.6.1" + "is-url": "^1.2.4", + "pdfkit": "0.20.1" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -4273,19 +3846,6 @@ "node": ">=20.0.0" } }, - "node_modules/@smithy/config-resolver": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.7.2.tgz", - "integrity": "sha512-Y1XfSefHIOub9762qm3ShafdlEE/Va8h3kLUeMq765fNeWeNLcOP2YUPr86H1SlyGwZTOqQ67RlBZPZ3k9Djgg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/core": { "version": "3.33.3", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.3.tgz", @@ -4313,45 +3873,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.5.2.tgz", - "integrity": "sha512-HQ4l596buGUPnypSGfoYKp17GvR0sMMKyc6PQ/NHxLmh8sQjP7h3TRvy+lW/et12UN9BRPAbHJKa4d3XMMVszQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.6.2.tgz", - "integrity": "sha512-GVWpmVKiVAvZDr3E0qRPu+A7TzeeMUctuP9SoFpzGx31lenW3Wk8zq7N97p8sJMwLCyVhXcbv5D0UTURCHdH0w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.5.2.tgz", - "integrity": "sha512-WkWkseIHXv+hr6RnrkpEOv5TOPHWLzzlOsocnPCCKsdfCOkBN7qhBn1/Pij9QZynkloJXYsQp9A5ykdIPVL2tg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/fetch-http-handler": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz", @@ -4366,162 +3887,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/hash-blob-browser": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.5.2.tgz", - "integrity": "sha512-+fx5GrWB/EElSRl/z+MQXa5RMaGH4ZH/MyZbBgH7CDE053KLD9o4kLGdrBGe8KicU/t22cHzdZs2cVwGL1yt9g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.5.2.tgz", - "integrity": "sha512-OcD8fGClTkP0BWHVEAgUp1RZyCw8cKfqTPQ+DgrSF5jvR8zKkw2Aud79L4G/1Fu3QKLcsHExxRIPQCcKx7+xkg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-stream-node": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.5.2.tgz", - "integrity": "sha512-0sx3A9RyCV8taQ1UM9P7K9JFH2bsLXhdIr85vNNXkhq7Q9TqUdm+XxGiBuInq76aA7vBdvUQFvvyBoB95DJf3Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.5.2.tgz", - "integrity": "sha512-VONOgtCxIXtwXrLVZPUdxOELYpkFzNizkpbQE5CrJ/OEh12Osx+LVXsLEAF+JcvVqPODmUoOUaovlOjCQHTOow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.5.2.tgz", - "integrity": "sha512-ErISzsrBz6I4rREFd9l49An0nIuatPWDegdu446Hf4QU3/UTygVaBYrcGbWNOC6sbANf2eyvPun3DyI2aHV+jg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/md5-js": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.5.2.tgz", - "integrity": "sha512-EC8CXhy/Y3DHniNBPK63ECkYir91IpFhIbVOo/Z7y75hO3RMFY0v+b6MUPLVp0TBtTRo+A5uU73yqyrTecTJmw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.5.2.tgz", - "integrity": "sha512-43Ixsc2OE5rhruhc9a1vETUCmK3Yh1AIbg2+wwQUgloOIti2FgPHmPLLBkjIqx8vdmpABtsVlhazNr5TMZKJmA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.7.2.tgz", - "integrity": "sha512-7HgK3/pQQHcD5w9lOPtK53/eDMKDp324Nd4KZ8XvxlcKHiWytuM9VwVOB2iy3rutsz/N1WNEWBkaRBayrGnuog==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.8.2.tgz", - "integrity": "sha512-wj0BTZ6SC7YI8hSLMbZCO1BP7k8JOuSUHjRYrZJApqNEiHL6jxaLs5Sr92EeXu5CuVzZOUCFUuewXusWxps0bQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.5.2.tgz", - "integrity": "sha512-5Qeqkw4IdmUQUGR/FRImBruFcnXiwGC8+EOVH377bonkQF66temzxM4HbCnOOQb8U/SClP1IdlFbC7CTmKux4Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.5.2.tgz", - "integrity": "sha512-db1TDSBA03WtomPMLWPy69L85vEaQbbnDHlmCYQ7hIqOdD22K+k8GlsVYdafYKaFkJaJW2OvcgcXOELGfhtptA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.6.2.tgz", - "integrity": "sha512-zMrXu/O5tPa7GLtra8L4wFG6DACcXT9QV4Ay+WEAjUhXm1dVq7c/q9Qv9gkJZNLY8hmQKg08778kDcxpKNMqOA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/node-http-handler": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.11.3.tgz", @@ -4536,58 +3901,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/property-provider": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.5.2.tgz", - "integrity": "sha512-SPJCSCCGpHf5g5b8244ig3WVKIdv2DS+X6cfmy4bpKEYo4VlLYM+bGHpHbfbmp76vegMKQBvs7DEpwQ5YuhKLA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.6.2.tgz", - "integrity": "sha512-Asd04MaxODN6FNY8EPTeCAM4kPNi3jDUAjZU0Y4F9rHvpLUrrUo7KLcxFgSthywFr6dZfIyDLIJda6jxmVTk5w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.5.2.tgz", - "integrity": "sha512-TLuVW4cGFCj+LirJylCVcFwxcUCpZ5Av0I7K9k/xhm6v6zAUeXSNuZ9e2QKuxcwkgrejL8LZvQTJy42pmq5bOg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.7.2.tgz", - "integrity": "sha512-XsIDj5gVG4YRxGS4n4TiBDAogPWRHXKZyor6JW/sEuaa/7BvKADe9j45jI2dPYCnYbKkLBmbZ4qN9ns0sH+kMQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/signature-v4": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.3.tgz", @@ -4602,20 +3915,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/smithy-client": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.15.2.tgz", - "integrity": "sha512-qweA7dHwcWmq8IN2mIGo7nAfeji1nwWBra+P3nz4R8W8yoQYE6Iy2bN33I0W3d50MN/V5EiDY/nElr4EHITsrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "@smithy/types": "^4.17.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/types": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", @@ -4628,210 +3927,10 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/url-parser": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.5.2.tgz", - "integrity": "sha512-xqZcyU/YjOt9ofe8WM1y9aCaAmxel2iRiHpkT0x2BN++mRXw0M4VdxPAOtuux279JndY6wWMiOt5BJ2xmAuMYA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.6.2.tgz", - "integrity": "sha512-wTQX1hPfElIqY6AzM/s6c4UgpMxCL4MwJ5u6340ksLPq78lur6Y+keheIDk5gMX4G3KFh4MERcDt6xhRq+ie1Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.5.2.tgz", - "integrity": "sha512-QyeUSin/R2nz0hXZ5ObTvhmCQomsr6+ZyHygab5AH5+fTY5xVDQeX1DFKp9uPEwOs6RW26jEn9yqq+4yqIZDrA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.5.2.tgz", - "integrity": "sha512-SErsW8pBKTCkFEA1RoChwW6/8JYzdcWqWDOsRP0xOtxMYsHOUMcldkcQjtujpKSy2FtlywqygCOxSKarc4TAZw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-buffer-from/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.5.2.tgz", - "integrity": "sha512-bAtInttsiHw9HX/vaNUU6Nj8pWqxMNKD0okEsjr8StgKwRdzN4mpl4/P7/en2JNCWlCyterSDOoveptct/jOGg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.6.2.tgz", - "integrity": "sha512-wENSdyLXVzupl1nyPK7JP/wt6Lkph/psRgocKli5NLh7O936L66FrZQ/I9HBHwZ5N0Z4MJUoj8qNdw+/6cTRgA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.5.2.tgz", - "integrity": "sha512-3915rhdCibo7Do927AFV6pXxsIiaKOZGGexJ3LnT2l5CgJZi3DPqZ2ZMhYTIPjiRA3asHhPe0gaFBtxuqXC8aA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.7.2.tgz", - "integrity": "sha512-16wQS3r4gXctr9/uYB0OUAUOm1LZLUKAsBeG364l64OalFwsRRvgO+AjL3OYxmxt060z1ExchX2ayrNVfM0r9Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.5.2.tgz", - "integrity": "sha512-wmfA04AZqTwu10EWGKUu4dm/3BZfSedqlqAiQPWVr8OkwFh0MovRDBwc48Pzd2YEEjqpI0JwXVk65TBarff3wQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.6.2.tgz", - "integrity": "sha512-q6MXFNu+W4ZCNdNKutzDLP/Hzumd1FU9CQX++P/7ylanYIYiGhgZwSzwZWAw+G1RNcfY+RBYv61XcGlSYhj+BA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.8.2.tgz", - "integrity": "sha512-R+S7jmu8VdbtOygMUSIGfkmOBp0SowT8+8LZdk21oXdmmKfN4G/bwN5IGIi0UH3RwWc9KJWDH1Dzm45DClyUvw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.5.2.tgz", - "integrity": "sha512-7wNWV7SugHpcMA7uzEawJNpE0GrasXM7a9E+1+Wm6NxVuDClESac/AKt+G7jMZNUz5vBLWqKlqVV7Sv7AtUr6Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-waiter": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.6.2.tgz", - "integrity": "sha512-TNNK01i1HiSAR6hBF94J7q338JgEaHPrP8QKRSeQBsbSbw5O4+YGdZYitmsTeEWvHGX+2R/oQ7BzxyAjeZxuWg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.33.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "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" @@ -4857,10 +3956,22 @@ "@types/readdir-glob": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -4894,12 +4005,13 @@ } }, "node_modules/@types/nodemailer": { - "version": "6.4.17", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", - "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==", "dev": true, "license": "MIT", "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, @@ -5625,15 +4737,16 @@ ] }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -5641,13 +4754,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5656,7 +4769,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -5681,38 +4794,39 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, "license": "MIT", "dependencies": { @@ -5723,27 +4837,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -5751,9 +4865,9 @@ } }, "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, "license": "MIT", "dependencies": { @@ -5822,12 +4936,15 @@ } }, "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==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -6337,9 +5454,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz", - "integrity": "sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==", + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6484,17 +5601,6 @@ "node": ">=8.0.0" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6712,6 +5818,61 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/cliui/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -6730,20 +5891,6 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6756,21 +5903,31 @@ "node": ">=7.0.0" } }, - "node_modules/color-name": { + "node_modules/color-convert/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==", "license": "MIT" }, + "node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "license": "MIT", - "optional": true, "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/commander": { @@ -6805,15 +5962,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -7106,24 +6254,6 @@ "node": ">=10" } }, - "node_modules/docx/node_modules/nanoid": { - "version": "5.1.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", - "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7370,9 +6500,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7383,30 +6513,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -7433,33 +6565,33 @@ } }, "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.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", @@ -7494,13 +6626,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.6.tgz", - "integrity": "sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.23.tgz", + "integrity": "sha512-z4WcTXNqFHwMG4V8WHb2xrlEPJvwarZa+H/6CR28vxr53icRnQzGXviO11p748BwrMZGl52itdPRzZfzYo0SKw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.1.6", + "@next/eslint-plugin-next": "15.5.23", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -7943,28 +7075,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -8107,9 +7217,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8486,9 +7596,9 @@ "license": "ISC" }, "node_modules/hyphen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", - "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.6.6.tgz", + "integrity": "sha512-XtqmnT+b9n5MX+MsqluFAVTIenbtC25iskW0Z+jLd+awfhA+ZbWKWQMIvLJccGoa2bM1R6juWJ27cZxIFOmkWw==", "license": "ISC" }, "node_modules/ieee754": { @@ -8593,13 +7703,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT", - "optional": true - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -9100,20 +8203,14 @@ } }, "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-md5": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", - "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9463,9 +8560,9 @@ } }, "node_modules/media-engine": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", - "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-2.0.0.tgz", + "integrity": "sha512-FqNmlXKYrp5d3g7xEOMJb+6gXZE0fTssdyIQqYR4LbkifbNbS0hDylhNDOh8r6tCgLboHd8+mwgH2njgSYDpnQ==", "license": "MIT" }, "node_modules/merge2": { @@ -9547,9 +8644,9 @@ } }, "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==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -9558,10 +8655,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/napi-postinstall": { @@ -9588,16 +8685,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", - "integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.23.tgz", + "integrity": "sha512-Gvd2WKgvxIXCGotxcI1im/Uf3rS3J3oZGw0g/uskg6AVBZhyE3aAbujkYWzS3xLmEPEtTLfkaVQUKK0KMTSIkA==", "license": "MIT", "dependencies": { - "@next/env": "15.1.6", - "@swc/counter": "0.1.3", + "@next/env": "15.5.23", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -9609,19 +8703,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.6", - "@next/swc-darwin-x64": "15.1.6", - "@next/swc-linux-arm64-gnu": "15.1.6", - "@next/swc-linux-arm64-musl": "15.1.6", - "@next/swc-linux-x64-gnu": "15.1.6", - "@next/swc-linux-x64-musl": "15.1.6", - "@next/swc-win32-arm64-msvc": "15.1.6", - "@next/swc-win32-x64-msvc": "15.1.6", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.23", + "@next/swc-darwin-x64": "15.5.23", + "@next/swc-linux-arm64-gnu": "15.5.23", + "@next/swc-linux-arm64-musl": "15.5.23", + "@next/swc-linux-x64-gnu": "15.5.23", + "@next/swc-linux-x64-musl": "15.5.23", + "@next/swc-win32-arm64-msvc": "15.5.23", + "@next/swc-win32-x64-msvc": "15.5.23", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@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", @@ -9643,19 +8737,19 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.25", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", - "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", + "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.37.2" + "@auth/core": "0.41.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", - "next": "^14.0.0-0 || ^15.0.0-0", - "nodemailer": "^6.6.5", - "react": "^18.2.0 || ^19.0.0-0" + "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": { @@ -9669,32 +8763,13 @@ } } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "tslib": "^2.8.0" } }, "node_modules/node-exports-info": { @@ -9737,9 +8812,9 @@ } }, "node_modules/nodemailer": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", - "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz", + "integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -10109,6 +9184,32 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/pdfkit": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.20.1.tgz", + "integrity": "sha512-1rRXK6x5o8I/3dBrBzXfxibpHpkfCnIA7EBAES7pEpGFc/65inMLlA8SalGWpJfal7BGekxeLf6A30IOpQpc5Q==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.3.0", + "@noble/hashes": "^1.8.0", + "fflate": "^0.8.3", + "fontkit": "^2.0.4", + "linebreak": "^1.1.0", + "png-js": "^2.0.0" + } + }, + "node_modules/pdfkit/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10137,49 +9238,35 @@ } }, "node_modules/playwright": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", - "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.1" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", - "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=20" } }, "node_modules/png-js": { @@ -10210,9 +9297,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -10229,7 +9316,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10300,6 +9387,41 @@ "postcss": "^8.4.21" } }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -10344,10 +9466,28 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postcss/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/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "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", @@ -10355,13 +9495,10 @@ } }, "node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "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", - "dependencies": { - "pretty-format": "^3.8.0" - }, "peerDependencies": { "preact": ">=10" } @@ -10376,12 +9513,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", - "license": "MIT" - }, "node_modules/prisma": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.3.1.tgz", @@ -10410,6 +9541,20 @@ } } }, + "node_modules/prisma/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -10531,6 +9676,12 @@ "react": "^19.0.0" } }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10967,9 +10118,9 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", "license": "MIT" }, "node_modules/semver": { @@ -11053,43 +10204,53 @@ "license": "MIT" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -11208,16 +10369,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -11262,14 +10413,6 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/streamx": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", @@ -11291,17 +10434,20 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -11319,17 +10465,32 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/string.prototype.includes": { "version": "2.0.1", @@ -11446,15 +10607,18 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi-cjs": { @@ -11470,6 +10634,15 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -11493,16 +10666,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, "license": "MIT" }, "node_modules/styled-jsx": { @@ -11665,41 +10846,6 @@ "node": ">= 6" } }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, "node_modules/tailwindcss/node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -11886,9 +11032,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -11946,13 +11092,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -11965,6 +11111,21 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -12268,24 +11429,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -12294,14 +11455,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "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" @@ -12357,17 +11518,17 @@ } }, "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", - "vite": "^5.0.0 || ^6.0.0" + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -12380,9 +11541,9 @@ } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "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" ], @@ -12397,9 +11558,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -12414,9 +11575,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "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" ], @@ -12431,9 +11592,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -12448,9 +11609,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -12465,9 +11626,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "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" ], @@ -12482,9 +11643,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "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" ], @@ -12499,9 +11660,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "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" ], @@ -12516,9 +11677,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "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" ], @@ -12533,9 +11694,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -12550,9 +11711,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "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" ], @@ -12567,9 +11728,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "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" ], @@ -12584,9 +11745,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "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" ], @@ -12601,9 +11762,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "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" ], @@ -12618,9 +11779,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "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" ], @@ -12635,9 +11796,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -12652,9 +11813,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "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" ], @@ -12668,10 +11829,27 @@ "node": ">=18" } }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "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" ], @@ -12686,9 +11864,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "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" ], @@ -12703,9 +11881,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -12719,10 +11897,27 @@ "node": ">=18" } }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -12737,9 +11932,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "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" ], @@ -12754,9 +11949,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "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" ], @@ -12771,9 +11966,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "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" ], @@ -12788,9 +11983,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "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", @@ -12801,32 +11996,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/vite/node_modules/fdir": { @@ -12847,6 +12042,21 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", @@ -12860,61 +12070,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", - "debug": "^4.4.0", - "expect-type": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", "magic-string": "^0.30.17", - "pathe": "^2.0.2", - "std-env": "^3.8.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -12930,8 +12114,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, @@ -12959,6 +12143,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13097,17 +12294,20 @@ } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { @@ -13128,6 +12328,59 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -13202,6 +12455,21 @@ "node": ">=6" } }, + "node_modules/yargs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/yargs/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -13254,6 +12522,32 @@ "node": ">=8" } }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 41daa25..2164108 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betreuungs-cockpit", - "version": "1.10.2", + "version": "1.11.0", "private": true, "description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung", "engines": { @@ -18,6 +18,7 @@ "db:migrate": "prisma migrate dev", "db:deploy": "prisma migrate deploy", "db:migration": "node scripts/migration-erzeugen.mjs", + "speicher:verschluesseln": "tsx --env-file=.env scripts/speicher-verschluesseln.mts", "db:pruefen": "node -e \"import('./scripts/migration-erzeugen.mjs').then(m => m.indizesPruefen())\"", "db:volltext": "prisma db execute --schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql", "db:seed": "prisma db seed", @@ -37,8 +38,8 @@ "seed": "tsx prisma/seed.ts" }, "dependencies": { - "@aws-sdk/client-s3": "3.735.0", - "@aws-sdk/s3-request-presigner": "3.735.0", + "@aws-sdk/client-s3": "3.1116.0", + "@aws-sdk/s3-request-presigner": "3.1116.0", "@node-rs/argon2": "2.0.2", "@prisma/client": "6.3.1", "@radix-ui/react-dialog": "1.1.4", @@ -54,9 +55,9 @@ "date-fns": "4.1.0", "docx": "9.1.1", "lucide-react": "0.474.0", - "next": "15.1.6", - "next-auth": "5.0.0-beta.25", - "nodemailer": "6.10.0", + "next": "15.5.23", + "next-auth": "5.0.0-beta.32", + "nodemailer": "9.0.5", "otpauth": "9.3.6", "pdf-lib": "1.17.1", "qrcode": "1.5.4", @@ -69,22 +70,22 @@ "zod": "3.24.1" }, "devDependencies": { - "@playwright/test": "1.50.1", + "@playwright/test": "1.62.1", "@types/archiver": "6.0.3", "@types/node": "22.13.1", - "@types/nodemailer": "6.4.17", + "@types/nodemailer": "7.0.4", "@types/qrcode": "1.5.5", "@types/react": "19.0.8", "@types/react-dom": "19.0.3", "autoprefixer": "10.4.20", - "eslint": "9.19.0", - "eslint-config-next": "15.1.6", - "postcss": "8.5.1", + "eslint": "9.39.1", + "eslint-config-next": "15.5.23", + "postcss": "8.5.26", "prisma": "6.3.1", "tailwindcss": "3.4.17", - "tsx": "4.19.2", + "tsx": "4.20.6", "typescript": "5.7.3", - "vitest": "3.0.5" + "vitest": "3.2.6" }, "allowScripts": { "@prisma/client@6.3.1": true, @@ -99,6 +100,9 @@ "next-auth": { "@simplewebauthn/server": "$@simplewebauthn/server", "@simplewebauthn/browser": "$@simplewebauthn/browser" - } + }, + "nodemailer": "9.0.5", + "postcss": "8.5.26", + "sharp": "0.35.3" } } diff --git a/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql b/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql new file mode 100644 index 0000000..88826c6 --- /dev/null +++ b/prisma/migrations/20260824074643_sicherheitsaudit/migration.sql @@ -0,0 +1,31 @@ +-- sicherheitsaudit +-- +-- Erzeugt mit "npm run db:migration". Entfernt wurden die +-- DROP-INDEX-Anweisungen zu diesen Indizes: +-- dokument_absender_trgm +-- vorgang_titel_trgm +-- +-- Sie stammen aus prisma/sql/volltextsuche.sql und sind Prisma unbekannt; +-- ohne sie waere die Volltextsuche still langsam geworden. + +-- AlterTable +ALTER TABLE "benutzer" ADD COLUMN "totpLetzterSchritt" INTEGER; + +-- CreateTable +CREATE TABLE "ersteinrichtung" ( + "id" TEXT NOT NULL, + "abgeschlossenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + + CONSTRAINT "ersteinrichtung_pkey" PRIMARY KEY ("id") +); + +-- Bestehende Installationen nachziehen. +-- +-- Die Tabelle oben ist die Sperre fuer die Ersteinrichtung. Auf einem +-- System, das schon laeuft, ist die Einrichtung laengst erledigt - ohne +-- diese Zeile stuende die Sperre nach dem Update offen, und der Weg zum +-- Anlegen eines Administrators waere wieder frei. +INSERT INTO "ersteinrichtung" ("id", "userId") +SELECT 'einmalig', (SELECT "id" FROM "benutzer" ORDER BY "erstelltAm" ASC LIMIT 1) +WHERE EXISTS (SELECT 1 FROM "benutzer"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d8dfb5c..13577ae 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -70,6 +70,13 @@ model User { // TOTP ist Pflicht. Das Secret liegt verschluesselt, nicht im Klartext. totpSecretVerschluesselt String? totpAktiviertAm DateTime? + /// Der zuletzt akzeptierte TOTP-Zeitschritt (Unixzeit / 30). + /// + /// Ein TOTP-Code gilt 30 Sekunden, mit Toleranz bis zu 90. In dieser Zeit + /// liesse sich derselbe Code ein zweites Mal einloesen - abgelesen ueber + /// die Schulter oder abgefangen. Deshalb wird der Schritt festgehalten + /// und nur noch ein groesserer akzeptiert. + totpLetzterSchritt Int? // Einmal-Wiederherstellungscodes, einzeln Argon2id-gehasht. wiederherstellungscodes String[] @default([]) @@ -128,6 +135,25 @@ model BetreuungsZugang { @@map("betreuungs_zugang") } +/// Haelt fest, dass die Ersteinrichtung stattgefunden hat. +/// +/// Die Tabelle hat genau eine Zeile mit fester Id. Das ist kein Protokoll, +/// sondern eine Sperre: Vorher stand im Code "wenn es noch keinen Benutzer +/// gibt, lege einen an" - zwei Anfragen gleichzeitig kamen beide durch die +/// Pruefung und legten beide einen Administrator an. Ueber den +/// Primaerschluessel entscheidet das jetzt die Datenbank, und zwar fuer +/// genau einen Gewinner. +model Ersteinrichtung { + /// Immer "einmalig". Der feste Wert ist die Sperre. + id String @id + + abgeschlossenAm DateTime @default(now()) + /// Wer den Zugang angelegt hat - nur zum Nachvollziehen. + userId String? + + @@map("ersteinrichtung") +} + /// Fehlgeschlagene und erfolgreiche Anmeldeversuche - Grundlage fuer das /// Rate-Limiting am Login. Wird nach 30 Tagen aufgeraeumt. model LoginVersuch { diff --git a/prisma/seed.ts b/prisma/seed.ts index b013b46..438fbbf 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -112,6 +112,14 @@ async function ersterZugang() { }, }) + // Die Ersteinrichtung ist damit erledigt. Ohne diese Zeile stuende die + // Einrichtungsseite nach dem Seed weiter offen. + await db.ersteinrichtung.upsert({ + where: { id: 'einmalig' }, + update: {}, + create: { id: 'einmalig', userId: benutzer.id }, + }) + console.log(`Zugang ${email} angelegt.`) if (devSecret) { console.log(' Zweiter Faktor mit dem Entwicklungs-Geheimnis vorbelegt.') diff --git a/scripts/schluessel-erzeugen.mjs b/scripts/schluessel-erzeugen.mjs index 043168b..9551708 100644 --- a/scripts/schluessel-erzeugen.mjs +++ b/scripts/schluessel-erzeugen.mjs @@ -18,6 +18,7 @@ AUTH_SECRET=${b64(32)} ENC_KEY_V1=${b64(32)} ENC_KEY_AKTIV=1 INDEX_KEY=${b64(32)} +ERSTEINRICHTUNG_GEHEIMNIS=${b64(24)} POSTGRES_PASSWORD=${b64(24).replace(/[+/=]/g, 'x')} # WICHTIG: ENC_KEY_V1 geht NICHT verloren gehen duerfen. diff --git a/scripts/speicher-verschluesseln.mts b/scripts/speicher-verschluesseln.mts new file mode 100644 index 0000000..0577a22 --- /dev/null +++ b/scripts/speicher-verschluesseln.mts @@ -0,0 +1,184 @@ +/** + * Bestehende Dateien nachtraeglich in den Umschlag stecken. + * + * npm run speicher:verschluesseln # nur schauen + * npm run speicher:verschluesseln -- --anwenden + * + * Warum es diesen Lauf gibt: Der Objektspeicher-Treiber hat bis v1.10.2 + * unverschluesselt abgelegt - es stand nur ServerSideEncryption: 'AES256' + * darauf, also Verschluesselung mit dem Schluessel des Anbieters. Seit + * v1.11.0 liegt derselbe Umschlag darum wie beim Dateitreiber. Neue Dateien + * bekommen ihn beim Ablegen; die alten liegen weiter im Klartext, bis sie + * hier einmal umgeschrieben werden. + * + * Lesen funktioniert waehrenddessen durchgehend: Der Treiber erkennt am + * Kopf, ob ein Umschlag da ist, und reicht alles andere unveraendert durch. + * Der Lauf laesst sich also jederzeit starten, abbrechen und wiederholen. + * + * Er ist absichtlich langsam und einfach: ein Objekt nach dem anderen, + * lesen, pruefen, zurueckschreiben. Bei ein paar hundert Dokumenten dauert + * das Sekunden, und ein Fehler betrifft immer nur eine Datei. + * + * OHNE --anwenden wird nichts geschrieben. + */ +import { createRequire } from 'node:module' +import { readFile, writeFile } from 'node:fs/promises' + +/** + * "server-only" fuer diesen Lauf neutralisieren. + * + * Das Paket wirft beim Import ausserhalb einer Server-Umgebung - genau + * dafuer ist es da. Hier laufen wir aber bewusst ausserhalb von Next, und + * die Alternative waere, den Umschlag im Skript noch einmal zu schreiben. + * Zwei Fassungen derselben Krypto sind die schlechtere Wahl: Wenn eine + * davon abweicht, merkt es niemand, bis eine Datei nicht mehr aufgeht. + * + * Deshalb wird der Modullader vorher umgebogen und danach dynamisch + * importiert - statische Importe wuerden vorgezogen und liefen in den + * Fehler. + */ +const verlangen = createRequire(import.meta.url) +const modulLader = verlangen('node:module') as { + _load: (anfrage: string, ...rest: unknown[]) => unknown +} +const echterLader = modulLader._load +modulLader._load = function (anfrage: string, ...rest: unknown[]) { + if (anfrage === 'server-only') return {} + return echterLader.call(this, anfrage, ...rest) +} + +const { env } = await import('../src/lib/env') +const { istVerschluesselt, verschluesselnBinaer } = await import('../src/lib/speicher/umschlag') + +const anwenden = process.argv.includes('--anwenden') + +type Bilanz = { geprueft: number; umgeschrieben: number; schonGut: number; fehler: number } + +function zeile(text = ''): void { + console.log(text) +} + +/** Objektspeicher: auflisten, lesen, bei Bedarf zurueckschreiben. */ +async function s3Durchgehen(): Promise { + const s3 = await import('../src/lib/speicher/s3') + const bilanz: Bilanz = { geprueft: 0, umgeschrieben: 0, schonGut: 0, fehler: 0 } + + const schluessel = await s3.objekteAuflisten() + zeile(` ${schluessel.length} Objekte im Eimer "${env.s3.bucket}".`) + zeile() + + for (const eintrag of schluessel) { + bilanz.geprueft += 1 + try { + const roh = await s3.rohLesen(eintrag) + + if (istVerschluesselt(roh)) { + bilanz.schonGut += 1 + continue + } + + if (anwenden) { + // schreiben() legt den Umschlag selbst an - der Inhalt geht also + // als Klartext hinein und kommt verschluesselt im Eimer an. + await s3.schreiben(eintrag, roh, 'application/octet-stream') + } + bilanz.umgeschrieben += 1 + zeile(` ${anwenden ? 'umgeschrieben' : 'waere umzuschreiben'}: ${eintrag}`) + } catch (fehler) { + bilanz.fehler += 1 + zeile(` FEHLER bei ${eintrag}: ${fehler instanceof Error ? fehler.message : fehler}`) + } + } + + return bilanz +} + +/** Dateitreiber: dasselbe fuer das Ablageverzeichnis. */ +async function dateienDurchgehen(): Promise { + const { readdir, stat } = await import('node:fs/promises') + const { join, relative, sep } = await import('node:path') + const treiber = await import('../src/lib/speicher/datei') + const bilanz: Bilanz = { geprueft: 0, umgeschrieben: 0, schonGut: 0, fehler: 0 } + + const wurzel = treiber.ablageverzeichnis() + zeile(` Verzeichnis ${wurzel}`) + zeile() + + async function absteigen(verzeichnis: string): Promise { + let eintraege: string[] + try { + eintraege = await readdir(verzeichnis) + } catch { + return // gibt es noch nicht + } + + for (const name of eintraege) { + const pfad = join(verzeichnis, name) + if ((await stat(pfad)).isDirectory()) { + await absteigen(pfad) + continue + } + + bilanz.geprueft += 1 + try { + const roh = await readFile(pfad) + if (istVerschluesselt(roh)) { + bilanz.schonGut += 1 + continue + } + + // Der Ablageschluessel ist der Pfad unterhalb der Wurzel, mit + // Schraegstrichen - genau das, was der Treiber beim Schreiben + // benutzt. Er gehoert in die Zusatzdaten, sonst waere die Datei + // hinterher zwar verschluesselt, aber nicht an ihren Platz + // gebunden, und liesse sich unter fremdem Namen wieder einspielen. + const ablageschluessel = relative(wurzel, pfad).split(sep).join('/') + + if (anwenden) { + await writeFile(pfad, verschluesselnBinaer(roh, ablageschluessel), { mode: 0o600 }) + } + bilanz.umgeschrieben += 1 + zeile(` ${anwenden ? 'umgeschrieben' : 'waere umzuschreiben'}: ${pfad}`) + } catch (fehler) { + bilanz.fehler += 1 + zeile(` FEHLER bei ${pfad}: ${fehler instanceof Error ? fehler.message : fehler}`) + } + } + } + + await absteigen(wurzel) + return bilanz +} + +async function main(): Promise { + zeile() + zeile(' Bestehende Dateien in den Umschlag stecken') + zeile(' ' + '-'.repeat(50)) + zeile() + + if (!anwenden) { + zeile(' TROCKENLAUF - es wird nichts geschrieben.') + zeile(' Zum Anwenden: npm run speicher:verschluesseln -- --anwenden') + zeile() + } + + const treiber = env.speicherTreiber + zeile(` Treiber: ${treiber}`) + zeile() + + const bilanz = treiber === 's3' ? await s3Durchgehen() : await dateienDurchgehen() + + zeile() + zeile(` Geprueft: ${bilanz.geprueft}`) + zeile(` Schon im Umschlag: ${bilanz.schonGut}`) + zeile(` ${anwenden ? 'Umgeschrieben' : 'Umzuschreiben'}: ${bilanz.umgeschrieben}`) + if (bilanz.fehler > 0) zeile(` Fehler: ${bilanz.fehler}`) + zeile() + + if (bilanz.fehler > 0) process.exitCode = 1 +} + +main().catch((fehler) => { + console.error(fehler) + process.exitCode = 1 +}) diff --git a/src/app/(app)/berichte/export/datei/route.ts b/src/app/(app)/berichte/export/datei/route.ts index 9ad88f7..64e03e0 100644 --- a/src/app/(app)/berichte/export/datei/route.ts +++ b/src/app/(app)/berichte/export/datei/route.ts @@ -1,6 +1,6 @@ import { Readable } from 'node:stream' import type { NextRequest } from 'next/server' -import { aktiveBetreute, erfordereBenutzer, reauthGueltig } from '@/lib/auth/sitzung' +import { aktiveBetreute, benutzerFuerRoute, reauthGueltig } from '@/lib/auth/sitzung' import { darf } from '@/lib/auth/rollen' import { db, nichtGeloescht } from '@/lib/db' import { protokollieren } from '@/lib/audit' @@ -33,7 +33,11 @@ function abgelehnt(nachricht: string, status: number): Response { } export async function GET(anfrage: NextRequest): Promise { - const benutzer = await erfordereBenutzer('/berichte/export') + // Eine Route antwortet mit einem Statuscode, nicht mit einer + // Anmeldeseite als Dateiinhalt. + const angemeldet = await benutzerFuerRoute() + if (!angemeldet.ok) return angemeldet.antwort + const benutzer = angemeldet.benutzer const gewuenscht = anfrage.nextUrl.searchParams.get('betreute') const aktive = await aktiveBetreute(benutzer) diff --git a/src/app/(auth)/ersteinrichtung/page.tsx b/src/app/(auth)/ersteinrichtung/page.tsx index b3ab902..181b1bd 100644 --- a/src/app/(auth)/ersteinrichtung/page.tsx +++ b/src/app/(auth)/ersteinrichtung/page.tsx @@ -21,8 +21,16 @@ export const dynamic = 'force-dynamic' export default async function ErsteinrichtungSeite() { const anzahlBenutzer = await db.user.count({ where: { geloeschtAm: null } }) + // Die Sperrzeile ist die eigentliche Auskunft darueber, ob eingerichtet + // wurde. Der Benutzerzaehler allein genuegt nicht: Liegt der erste Zugang + // im Papierkorb, stuende Schritt 1 sonst wieder offen. + const sperre = await db.ersteinrichtung.findUnique({ + where: { id: 'einmalig' }, + select: { id: true }, + }) + // --- Schritt 1: Es gibt noch keinen Zugang ------------------------------- - if (anzahlBenutzer === 0) { + if (anzahlBenutzer === 0 && !sperre) { return ( <> diff --git a/src/app/(auth)/ersteinrichtung/zugang-formular.tsx b/src/app/(auth)/ersteinrichtung/zugang-formular.tsx index fca2619..ad935a5 100644 --- a/src/app/(auth)/ersteinrichtung/zugang-formular.tsx +++ b/src/app/(auth)/ersteinrichtung/zugang-formular.tsx @@ -61,6 +61,23 @@ export function ZugangFormular() { /> + + + + }, -): Promise { + // Response statt NextResponse: Die Abweisung ohne zweiten Faktor kommt + // aus benutzerFuerRoute() und ist eine schlichte Response. NextResponse + // erbt davon, die uebrigen Rueckgaben passen also weiterhin. +): Promise { const { signatur } = await params // 1. Signatur und Ablauf @@ -70,9 +73,14 @@ export async function GET( } const schluessel = geprueft.schluessel - // 2. Angemeldet? - const benutzer = await aktuellerBenutzer() - if (!benutzer) return nurText(401, 'Bitte melde dich an.') + // 2. Angemeldet - und mit abgeschlossenem zweitem Faktor. + // + // Frueher stand hier nur die Frage nach der Anmeldung. Damit kam jemand, + // der die TOTP-Einrichtung nie abgeschlossen hatte, an jedes Dokument - + // die MFA-Pflicht galt nur fuer Seiten, nicht fuer diesen Abruf. + const angemeldet = await benutzerFuerRoute() + if (!angemeldet.ok) return angemeldet.antwort + const benutzer = angemeldet.benutzer // 3. Zugriff auf genau diese betreute Person const betreuteId = betreuteIdAusSchluessel(schluessel) diff --git a/src/auth.ts b/src/auth.ts index d70aecd..8d72fa5 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,15 +3,17 @@ import Credentials from 'next-auth/providers/credentials' import type { Rolle } from '@prisma/client' import { db } from '@/lib/db' import { env } from '@/lib/env' -import { hashVeraltet, passwortHashen, passwortPruefen, wiederherstellungscodeFinden } from '@/lib/crypto/hash' -import { totpPruefen, totpSecretEntschluesseln } from '@/lib/crypto/totp' +import { hashVeraltet, passwortHashen, passwortPruefen } from '@/lib/crypto/hash' import { anmeldungErlaubt, aufMindestdauerWarten, fehlversucheZuruecksetzen, + sperreVermerken, versuchFesthalten, } from '@/lib/auth/ratelimit' import { protokollieren } from '@/lib/audit' +import { zweitenFaktorEinloesen } from '@/lib/auth/zweiter-faktor' +import { anschlussAus } from '@/lib/auth/anschluss' import { PasskeyFehler, anmeldungPruefen } from '@/lib/auth/passkey' import type { AuthenticationResponseJSON } from '@simplewebauthn/server' @@ -64,12 +66,13 @@ export class PasskeyAnmeldeFehler extends CredentialsSignin { function ipAusAnfrage(anfrage: Request | undefined): string | null { if (!anfrage) return null - const weitergeleitet = anfrage.headers.get('x-forwarded-for')?.split(',')[0]?.trim() - return weitergeleitet || anfrage.headers.get('x-real-ip') || null + return anschlussAus(anfrage.headers) } export const { handlers, signIn, signOut, auth } = NextAuth({ - trustHost: true, + // Siehe env.vertraueHost: Ist AUTH_URL gesetzt, wird dem Host-Kopf nicht + // geglaubt. Vorher stand hier fest "true". + trustHost: env.vertraueHost, secret: process.env.AUTH_SECRET, session: { @@ -110,7 +113,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ // 1. Sperre pruefen, bevor teuer gerechnet wird. const sperre = await anmeldungErlaubt(email, ip) if (sperre.gesperrt) { - await versuchFesthalten(email, ip, false, 'gesperrt') + await sperreVermerken(email, ip) throw new AnmeldeFehler('gesperrt', sperre.nochSekunden) } @@ -141,33 +144,13 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ throw new AnmeldeFehler('konto-inaktiv') } - // 3. Zweiter Faktor. Ohne eingerichtetes TOTP kommt der Benutzer - // zwar herein, das App-Layout schickt ihn aber sofort zur - // Einrichtung - er kann nichts anderes tun. + // 3. Zweiter Faktor. Ist er noch nicht eingerichtet, kommt der + // Benutzer nur bis zur Einrichtungsseite - erzwungen in + // erfordereBenutzer(), nicht erst im Layout. if (benutzer.totpSecretVerschluesselt && benutzer.totpAktiviertAm) { - const codeOhneTrenner = code.replace(/[\s-]/g, '') - let zweiterFaktorOk = false + const eingeloest = await zweitenFaktorEinloesen(benutzer, code) - if (/^\d{6}$/.test(codeOhneTrenner)) { - const secret = totpSecretEntschluesseln(benutzer.totpSecretVerschluesselt) - zweiterFaktorOk = totpPruefen(secret, benutzer.email, codeOhneTrenner).gueltig - } else if (codeOhneTrenner.length >= 12) { - // Wiederherstellungscode - wird nach Gebrauch entfernt. - const treffer = await wiederherstellungscodeFinden( - benutzer.wiederherstellungscodes, - code, - ) - if (treffer >= 0) { - zweiterFaktorOk = true - const rest = benutzer.wiederherstellungscodes.filter((_, i) => i !== treffer) - await db.user.update({ - where: { id: benutzer.id }, - data: { wiederherstellungscodes: rest }, - }) - } - } - - if (!zweiterFaktorOk) { + if (!eingeloest.ok) { await versuchFesthalten(email, ip, false, 'code-falsch') await protokollieren({ aktion: 'ANMELDUNG_FEHLGESCHLAGEN', @@ -175,7 +158,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ entitaetId: benutzer.id, userEmail: email, userId: benutzer.id, - bezeichnung: 'Zweiter Faktor falsch', + bezeichnung: + eingeloest.grund === 'wiederverwendet' + ? 'Zweiter Faktor bereits verbraucht' + : 'Zweiter Faktor falsch', }) await aufMindestdauerWarten(begonnen) throw new AnmeldeFehler('code-falsch') @@ -274,7 +260,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ // anmeldet - erraten laesst sich ein Passkey ohnehin nicht. const sperre = await anmeldungErlaubt(benutzer.email, ip) if (sperre.gesperrt) { - await versuchFesthalten(benutzer.email, ip, false, 'gesperrt') + await sperreVermerken(benutzer.email, ip) throw new AnmeldeFehler('gesperrt', sperre.nochSekunden) } diff --git a/src/lib/auth/anschluss.ts b/src/lib/auth/anschluss.ts new file mode 100644 index 0000000..3e64a73 --- /dev/null +++ b/src/lib/auth/anschluss.ts @@ -0,0 +1,92 @@ +import 'server-only' +import { env } from '@/lib/env' + +/** + * Von welchem Anschluss kommt diese Anfrage? + * + * Gebraucht wird das nur fuer eines: die zweite Dimension der Anmeldesperre. + * Gezaehlt wird nach E-Mail UND nach Anschluss, damit es weder hilft, + * dieselbe Adresse von vielen Anschluessen zu probieren, noch viele + * Adressen von einem. + * + * ## Warum das vorher nichts geschuetzt hat + * + * Der Wert kam aus "x-forwarded-for", und zwar aus dem ERSTEN Eintrag: + * + * kopf.get('x-forwarded-for')?.split(',')[0]?.trim() + * + * Dieser Kopf ist eine Liste, an die jeder Zwischenknoten hinten anhaengt. + * Der erste Eintrag ist deshalb der, den der Aufrufer selbst geschickt hat - + * frei erfunden. Wer bei jeder Anfrage eine andere Zahl hineinschrieb, sah + * fuer die Sperre wie tausend verschiedene Anschluesse aus. Die Zaehlung + * nach Anschluss war damit nicht nur wirkungslos, sondern irrefuehrend: Im + * Protokoll standen tausend Anschluesse, wo einer war. + * + * Richtig ist das andere Ende: Was der vertraute Proxy anhaengt, hat er + * selbst gesehen und nicht der Aufrufer geschrieben. + * + * ## Und wenn kein Proxy davor steht + * + * Dann ist der Kopf vollstaendig fremdbestimmt und es gibt hier nichts zu + * holen - also null. Keine Zaehlung nach Anschluss ist besser als eine, die + * sich mit einer Kopfzeile aushebeln laesst. Deshalb muss der Betrieb + * ausdruecklich sagen, dass ein Proxy davorsteht (VERTRAUE_PROXY=true). + */ + +let gewarnt = false + +/** + * Warnt einmal, wenn weitergereichte Koepfe ankommen, ihnen aber nicht + * geglaubt wird. Genau dann fehlt die Einstellung im Betrieb. + */ +function einmalWarnen(): void { + if (gewarnt) return + gewarnt = true + console.warn( + '[anschluss] Es kommen "x-forwarded-for"-Koepfe an, aber VERTRAUE_PROXY ist nicht gesetzt. ' + + 'Die Anmeldesperre zaehlt deshalb nur nach E-Mail-Adresse, nicht nach Anschluss. ' + + 'Steht ein Reverse Proxy davor, setze VERTRAUE_PROXY=true (und, bei mehreren, ' + + 'VERTRAUE_PROXY_STUFEN=). Steht keiner davor, ist das hier richtig so.', + ) +} + +/** + * Liest die Adresse des Aufrufers aus den Koepfen. + * + * @param kopf Die Kopfzeilen der Anfrage. + * @returns Die Adresse, oder null, wenn sie sich nicht vertrauenswuerdig + * bestimmen laesst. + */ +export function anschlussAus(kopf: Headers): string | null { + const weitergereicht = kopf.get('x-forwarded-for') + const echterIp = kopf.get('x-real-ip') + + if (!env.vertraueProxy) { + if (weitergereicht || echterIp) einmalWarnen() + return null + } + + if (weitergereicht) { + const kette = weitergereicht + .split(',') + .map((teil) => teil.trim()) + .filter((teil) => teil.length > 0) + + // Von hinten zaehlen: Der letzte Eintrag stammt vom naechsten + // Zwischenknoten, der zweitletzte von dem davor. Bei einer Stufe ist + // der letzte Eintrag der Aufrufer, wie der Proxy ihn gesehen hat. + if (kette.length > 0) { + const stelle = kette.length - env.vertraueProxyStufen + return kette[Math.max(0, stelle)] ?? null + } + } + + // Manche Proxys setzen nur diesen Kopf. Er enthaelt genau eine Adresse, + // also gibt es hier nichts abzuzaehlen. + return echterIp?.trim() || null +} + +/** Nur fuer Tests: setzt die einmalige Warnung zurueck. */ +export function warnungZuruecksetzen(): void { + gewarnt = false +} diff --git a/src/lib/auth/ratelimit.ts b/src/lib/auth/ratelimit.ts index a3723ce..ed34b08 100644 --- a/src/lib/auth/ratelimit.ts +++ b/src/lib/auth/ratelimit.ts @@ -14,8 +14,32 @@ import { ipHashen } from '@/lib/crypto/hash' * Gezaehlt wird nach E-Mail UND nach Anschluss (IP-Hash). Damit hilft es * weder, dieselbe Adresse von vielen Anschluessen zu probieren, noch viele * Adressen von einem Anschluss. + * + * ## Warum abgewiesene Versuche nicht mitzaehlen + * + * Frueher wurde jeder Aufruf festgehalten, auch der, der wegen einer + * laufenden Sperre gar nicht geprueft wurde. Zusammen mit einer Frist, die + * vom juengsten Fehlversuch aus gerechnet wurde, ergab das eine + * Rueckkopplung: Jeder abgewiesene Aufruf schob die Frist um die volle + * Sperrdauer nach hinten. Wer alle zehn Minuten ein falsches Passwort an + * eine fremde Adresse schickte, sperrte deren Inhaber dauerhaft aus - ohne + * das Passwort zu kennen. Und jeder dieser Aufrufe hinterliess eine Zeile, + * die erst der Wartungslauf nach 30 Tagen wieder los wurde. + * + * Deshalb jetzt zwei getrennte Dinge: + * + * - Ein *geprueft* fehlgeschlagener Versuch (falsches Passwort, falscher + * Code, unbekannte Adresse) zaehlt und wird einzeln festgehalten. + * - Ein *abgewiesener* Aufruf waehrend der Sperre zaehlt nicht und wird + * hoechstens einmal je Sperre vermerkt. + * + * Damit steht die Frist fest, sobald sie einmal gesetzt ist, und die + * Tabelle waechst nicht mit der Zahl der Angriffsversuche. */ +/** Grund fuer eine Zeile, die nur den abgewiesenen Aufruf dokumentiert. */ +const GRUND_ABGEWIESEN = 'gesperrt' + export type SperrErgebnis = | { gesperrt: false; verbleibendeVersuche: number } | { gesperrt: true; nochSekunden: number } @@ -29,33 +53,49 @@ function fensterStart(): Date { * Passwortvergleich aufgerufen - ein gesperrter Zugang soll gar nicht erst * Rechenzeit fuer Argon2 kosten. */ -export async function anmeldungErlaubt( - email: string, - ip: string | null, -): Promise { - const seit = fensterStart() +export async function anmeldungErlaubt(email: string, ip: string | null): Promise { + const jetzt = Date.now() const kennung = email.trim().toLowerCase() const ipH = ipHashen(ip) const fehlversuche = await db.loginVersuch.findMany({ where: { erfolg: false, - zeitpunkt: { gte: seit }, + // Abgewiesene Aufrufe zaehlen nicht mit - sonst haelt sich die Sperre + // selbst am Leben, siehe Kopf dieser Datei. + grund: { not: GRUND_ABGEWIESEN }, + zeitpunkt: { gte: new Date(jetzt - env.loginFensterSekunden * 1000) }, OR: [{ email: kennung }, ...(ipH ? [{ ipHash: ipH }] : [])], }, - orderBy: { zeitpunkt: 'desc' }, + orderBy: { zeitpunkt: 'asc' }, select: { zeitpunkt: true }, - take: env.loginMaxVersuche + 1, + // Deckel gegen eine ausufernde Abfrage. Mehr als eine Handvoll Runden + // passt ohnehin nicht in ein Zeitfenster, in dem jede Runde gesperrt + // wird. + take: 200, }) - if (fehlversuche.length >= env.loginMaxVersuche) { - const juengster = fehlversuche[0]!.zeitpunkt.getTime() - const frei = juengster + env.loginSperreSekunden * 1000 - const nochSekunden = Math.max(1, Math.ceil((frei - Date.now()) / 1000)) - return { gesperrt: true, nochSekunden } + // Von alt nach neu durchgehen und die Runden nachvollziehen: Sobald das + // Mass voll ist, beginnt an genau diesem Versuch eine Sperre, und der + // Zaehler faengt von vorn an. Ist ihre Frist inzwischen abgelaufen, sind + // die Versuche davor abgegolten - sonst bliebe der Zugang bis zum Ende + // des Zeitfensters dicht, obwohl die Sperre laengst um ist. + let zaehler = 0 + let sperreBis = 0 + + for (const versuch of fehlversuche) { + zaehler += 1 + if (zaehler >= env.loginMaxVersuche) { + sperreBis = versuch.zeitpunkt.getTime() + env.loginSperreSekunden * 1000 + zaehler = 0 + } } - return { gesperrt: false, verbleibendeVersuche: env.loginMaxVersuche - fehlversuche.length } + if (sperreBis > jetzt) { + return { gesperrt: true, nochSekunden: Math.max(1, Math.ceil((sperreBis - jetzt) / 1000)) } + } + + return { gesperrt: false, verbleibendeVersuche: env.loginMaxVersuche - zaehler } } /** Haelt einen Versuch fest. Der Grund hilft spaeter beim Nachvollziehen. */ @@ -75,6 +115,34 @@ export async function versuchFesthalten( }) } +/** + * Vermerkt einen abgewiesenen Aufruf - aber nur den ersten je Sperre. + * + * Dass eine Sperre gegriffen hat, will man im Protokoll sehen. Wie oft + * dagegen geklopft wurde, ist die Information nicht wert: Sie kostet eine + * Zeile pro Anfrage, und genau daraus laesst sich die Tabelle vollschreiben. + * Eine Zeile je Sperre reicht, um den Vorgang zu erkennen. + * + * @returns true, wenn tatsaechlich vermerkt wurde. + */ +export async function sperreVermerken(email: string, ip: string | null): Promise { + const kennung = email.trim().toLowerCase() + const vorhanden = await db.loginVersuch.findFirst({ + where: { + email: kennung, + erfolg: false, + grund: GRUND_ABGEWIESEN, + zeitpunkt: { gte: fensterStart() }, + }, + select: { id: true }, + }) + + if (vorhanden) return false + + await versuchFesthalten(kennung, ip, false, GRUND_ABGEWIESEN) + return true +} + /** * Nach einer erfolgreichen Anmeldung werden die Fehlversuche dieser * Adresse geloescht - sonst bliebe der Zaehler bis zum Fensterende stehen. diff --git a/src/lib/auth/sitzung.ts b/src/lib/auth/sitzung.ts index ad0f624..c4f0c58 100644 --- a/src/lib/auth/sitzung.ts +++ b/src/lib/auth/sitzung.ts @@ -46,8 +46,46 @@ export async function aktuellerBenutzer(): Promise { return benutzer } -/** Wie aktuellerBenutzer, leitet aber zum Anmeldeformular um. */ +/** Hat dieser Benutzer den zweiten Faktor abgeschlossen? */ +export function zweiterFaktorFertig(benutzer: User): boolean { + return Boolean(benutzer.totpAktiviertAm && benutzer.totpSecretVerschluesselt) +} + +/** + * Der angemeldete Benutzer - mit abgeschlossenem zweitem Faktor. + * + * Die MFA-Pflicht steckt bewusst HIER und nicht im App-Layout. Ein Layout + * schuetzt nur, was durch das Layout gerendert wird; Server Actions und + * Route Handler laufen daran vorbei. Wer eine Passwort-Sitzung hat, aber + * die TOTP-Einrichtung nie abgeschlossen hat, konnte sie frueher direkt + * aufrufen - "MFA ist Pflicht" war damit eine Aussage ueber die + * Oberflaeche, keine Sicherheitsgrenze. + * + * Massgeblich ist der Datenbankstand aus aktuellerBenutzer(), nicht das + * Feld im Sitzungstoken: Das laesst sich ueber session.update() vom + * Client beeinflussen. + * + * Die Ausnahmen sind klein und aufgezaehlt - Einrichtung des zweiten + * Faktors und Abmelden. Wer eine weitere braucht, nimmt bewusst + * erfordereBenutzerOhneZweitenFaktor() und begruendet es dort. + */ export async function erfordereBenutzer(zielNachAnmeldung?: string): Promise { + const benutzer = await erfordereBenutzerOhneZweitenFaktor(zielNachAnmeldung) + if (!zweiterFaktorFertig(benutzer)) { + redirect('/zwei-faktor/einrichten') + } + return benutzer +} + +/** + * Nur fuer die Handvoll Stellen, die VOR abgeschlossener MFA erreichbar + * sein muessen: die Einrichtung selbst und das Abmelden. Jede weitere + * Verwendung ist ein Loch in der MFA-Pflicht und braucht eine + * Begruendung im Code. + */ +export async function erfordereBenutzerOhneZweitenFaktor( + zielNachAnmeldung?: string, +): Promise { const benutzer = await aktuellerBenutzer() if (!benutzer) { const ziel = zielNachAnmeldung ? `?weiter=${encodeURIComponent(zielNachAnmeldung)}` : '' @@ -57,11 +95,37 @@ export async function erfordereBenutzer(zielNachAnmeldung?: string): Promise { + const benutzer = await aktuellerBenutzer() + if (!benutzer) { + return { ok: false, antwort: new Response('Nicht angemeldet.', { status: 401 }) } + } + if (!zweiterFaktorFertig(benutzer)) { + return { + ok: false, + antwort: new Response('Zweiter Faktor ist nicht eingerichtet.', { status: 403 }), + } + } + return { ok: true, benutzer } +} + +/** + * Erzwingt den eingerichteten zweiten Faktor. + * + * Bleibt als eigener Aufruf im App-Layout bestehen, obwohl + * erfordereBenutzer() das inzwischen selbst tut: Zwei Sperren an + * derselben Stelle kosten nichts und die aeussere ist die, die man beim + * Lesen des Layouts sieht. */ export async function erfordereZweitenFaktor(benutzer: User): Promise { - if (!benutzer.totpAktiviertAm || !benutzer.totpSecretVerschluesselt) { + if (!zweiterFaktorFertig(benutzer)) { redirect('/zwei-faktor/einrichten') } } diff --git a/src/lib/auth/zweiter-faktor.ts b/src/lib/auth/zweiter-faktor.ts new file mode 100644 index 0000000..cd8ed18 --- /dev/null +++ b/src/lib/auth/zweiter-faktor.ts @@ -0,0 +1,93 @@ +import 'server-only' +import type { User } from '@prisma/client' +import { db } from '@/lib/db' +import { wiederherstellungscodeFinden } from '@/lib/crypto/hash' +import { totpPruefen, totpSchritt, totpSecretEntschluesseln } from '@/lib/crypto/totp' + +/** + * Einloesen des zweiten Faktors - einmal und nur einmal. + * + * Ein TOTP-Code gilt 30 Sekunden, mit der Toleranz von einem Fenster bis zu + * 90. Wer ihn in dieser Zeit noch einmal einreicht, kommt ohne die + * Toleranz ein zweites Mal herein: abgelesen ueber die Schulter, + * mitgeschrieben von einem Zwischenglied, oder aus einem Formular, das im + * Browserverlauf steht. Der Kommentar in totp.ts versprach seit dem Anfang, + * der Aufrufer speichere das getroffene Fenster - getan hat es niemand. + * + * Deshalb hier: Der Schritt wird festgehalten, und angenommen wird nur ein + * groesserer. Damit ist jeder Code genau einmal gut. + * + * ## Warum updateMany und nicht update + * + * Die Bedingung steht im WHERE, nicht in einem if davor. Zwei Anfragen mit + * demselben Code, gleichzeitig abgeschickt, wuerden sonst beide den + * Vergleich bestehen, bevor eine von ihnen schreibt. So entscheidet die + * Datenbank: Genau eine trifft eine Zeile, die andere keine. + */ + +export type ZweiterFaktorErgebnis = + | { ok: true; art: 'totp' | 'wiederherstellungscode' } + | { ok: false; grund: 'falsch' | 'wiederverwendet' } + +/** Ab dieser Laenge wird die Eingabe als Wiederherstellungscode gelesen. */ +const CODE_MINDESTLAENGE = 12 + +/** + * Prueft die Eingabe und verbraucht sie. + * + * Nimmt sowohl einen sechsstelligen TOTP-Code als auch einen + * Wiederherstellungscode. Letzterer wird nach Gebrauch aus der Liste + * entfernt - auch das eine Einmalverwendung, nur mit anderem Mechanismus. + * + * @param benutzer Der Datensatz aus der Datenbank, nicht aus der Sitzung. + * @param eingabe Was im Formular stand, ungereinigt. + */ +export async function zweitenFaktorEinloesen( + benutzer: User, + eingabe: string, +): Promise { + const ohneTrenner = eingabe.replace(/[\s-]/g, '') + + if (/^\d{6}$/.test(ohneTrenner)) { + if (!benutzer.totpSecretVerschluesselt) return { ok: false, grund: 'falsch' } + + const secret = totpSecretEntschluesseln(benutzer.totpSecretVerschluesselt) + const { gueltig, delta } = totpPruefen(secret, benutzer.email, ohneTrenner) + if (!gueltig || delta === null) return { ok: false, grund: 'falsch' } + + const schritt = totpSchritt(delta) + + const { count } = await db.user.updateMany({ + where: { + id: benutzer.id, + OR: [{ totpLetzterSchritt: null }, { totpLetzterSchritt: { lt: schritt } }], + }, + data: { totpLetzterSchritt: schritt }, + }) + + // Keine Zeile getroffen: Dieser Schritt war schon einmal dran. Der Code + // ist rechnerisch richtig und trotzdem verbraucht. + if (count === 0) return { ok: false, grund: 'wiederverwendet' } + + return { ok: true, art: 'totp' } + } + + if (ohneTrenner.length >= CODE_MINDESTLAENGE) { + const treffer = await wiederherstellungscodeFinden(benutzer.wiederherstellungscodes, eingabe) + if (treffer < 0) return { ok: false, grund: 'falsch' } + + // Auch hier die Bedingung in die Abfrage: Sonst koennten zwei + // gleichzeitige Anmeldungen denselben Code einloesen, weil beide noch + // die alte Liste sehen. + const rest = benutzer.wiederherstellungscodes.filter((_, i) => i !== treffer) + const { count } = await db.user.updateMany({ + where: { id: benutzer.id, wiederherstellungscodes: { equals: benutzer.wiederherstellungscodes } }, + data: { wiederherstellungscodes: rest }, + }) + if (count === 0) return { ok: false, grund: 'wiederverwendet' } + + return { ok: true, art: 'wiederherstellungscode' } + } + + return { ok: false, grund: 'falsch' } +} diff --git a/src/lib/crypto/totp.ts b/src/lib/crypto/totp.ts index 8871769..37e95d2 100644 --- a/src/lib/crypto/totp.ts +++ b/src/lib/crypto/totp.ts @@ -20,6 +20,9 @@ const ZIFFERN = 6 const PERIODE = 30 const TOLERANZ_FENSTER = 1 +/** Laenge eines TOTP-Fensters in Sekunden - fuer den Wiederverwendungsschutz. */ +export const TOTP_PERIODE = PERIODE + function totpBauen(secret: string, kennung: string): TOTP { return new TOTP({ issuer: AUSSTELLER, @@ -54,10 +57,24 @@ export async function totpQrCode(secret: string, kennung: string): Promise { await ausfuehren( 'pdftoppm', - ['-r', '300', '-gray', '-png', '-l', String(MAX_SEITEN), pdfPfad, join(verzeichnis, 'seite')], - { timeout: ZEITGRENZE_JE_SEITE_MS * 4, windowsHide: true }, + [ + // Statt fester dpi: Die laengste Kante wird auf eine feste Pixelzahl + // gebracht. Siehe MAX_KANTE_PIXEL - eine im PDF angegebene Riesenseite + // ergibt sonst ein Riesenbild. + '-scale-to', + String(MAX_KANTE_PIXEL), + '-gray', + '-png', + '-l', + String(MAX_SEITEN), + pdfPfad, + join(verzeichnis, 'seite'), + ], + { timeout: ZEITGRENZE_JE_SEITE_MS * 4, maxBuffer: 1024 * 1024, windowsHide: true }, ) - const dateien = await readdir(verzeichnis) - return dateien + + const dateien = (await readdir(verzeichnis)) .filter((name) => name.startsWith('seite') && name.endsWith('.png')) .sort() .map((name) => join(verzeichnis, name)) + + // Zweiter Deckel, hinterher: Was pdftoppm trotz -scale-to abgelegt hat, + // wird nur bis zu einer Gesamtgroesse weiterverarbeitet. Der Rest bleibt + // liegen und wird mit dem Arbeitsverzeichnis geloescht. + const genommen: string[] = [] + let gesamt = 0 + for (const pfad of dateien) { + const { size } = await stat(pfad) + if (gesamt + size > MAX_BILDER_BYTES) break + gesamt += size + genommen.push(pfad) + } + return genommen } /** diff --git a/src/lib/ocr/pdf-text.ts b/src/lib/ocr/pdf-text.ts index f6c765a..2981cc2 100644 --- a/src/lib/ocr/pdf-text.ts +++ b/src/lib/ocr/pdf-text.ts @@ -15,8 +15,33 @@ import { inflateSync, inflateRawSync, unzipSync } from 'node:zlib' * "nicht_verfuegbar", und der Nutzer traegt die Metadaten von Hand ein. * * Rein, ohne Seiteneffekte, ohne Netz. + * + * ## Grenzen, und warum es sie braucht + * + * Ein Flate-Strom sagt nicht, wie gross er ausgepackt wird. Ein paar + * Kilobyte Nullen komprimieren auf fast nichts und blaehen sich beim + * Auspacken auf Gigabyte auf - die klassische Dekompressionsbombe. Vorher + * lief das Auspacken ohne Obergrenze, und ein Dokument, das jemand in die + * Akte legt, haette den Server damit aus dem Speicher werfen koennen. + * + * Deshalb drei Deckel: einer je Strom, einer fuer die Summe, und die + * bestehende Obergrenze von 2000 Stroemen. Wird einer erreicht, bricht das + * Auspacken ab und der Text ist eben unvollstaendig - besser ein + * "nicht_verfuegbar" als ein Serverneustart. */ +/** + * Wie gross ein einzelner ausgepackter Strom werden darf. + * + * Acht Megabyte sind fuer eine Seite Text absurd viel - ein + * Behoerdenbescheid liegt bei einigen Kilobyte. Die Grenze ist bewusst + * grosszuegig: Sie soll Bomben abfangen, nicht ungewoehnliche PDFs. + */ +const MAX_STROM_BYTES = 8 * 1024 * 1024 + +/** Und wie viel es ueber alle Stroeme zusammen sein darf. */ +const MAX_GESAMT_BYTES = 64 * 1024 * 1024 + /** Oktal-Escapes und Standard-Escapes in PDF-Literalstrings. */ function literalEntpacken(roh: string): string { let ergebnis = '' @@ -108,8 +133,9 @@ function stroemeAuspacken(pdf: Buffer): string[] { const roh = pdf.toString('latin1') const stroeme: string[] = [] let suchAb = 0 + let gesamt = 0 - while (stroeme.length < 2000) { + while (stroeme.length < 2000 && gesamt < MAX_GESAMT_BYTES) { const beginn = roh.indexOf('stream', suchAb) if (beginn < 0) break @@ -131,28 +157,50 @@ function stroemeAuspacken(pdf: Buffer): string[] { } if (/\/Filter\s*(\/FlateDecode|\[\s*\/FlateDecode)/.test(kopf)) { - const entpackt = flateVersuchen(daten) - if (entpackt) stroeme.push(entpackt.toString('latin1')) + const entpackt = flateVersuchen(daten, MAX_GESAMT_BYTES - gesamt) + if (entpackt) { + gesamt += entpackt.length + stroeme.push(entpackt.toString('latin1')) + } continue } if (!/\/Filter/.test(kopf)) { + // Unkomprimiert: Hier gibt es nichts, was sich aufblaehen koennte - + // die Bytes stehen so in der Datei. Sie zaehlen trotzdem mit, damit + // die Summe stimmt. + gesamt += daten.length stroeme.push(daten.toString('latin1')) continue } // Unbekannter Filter: trotzdem einen Flate-Versuch wagen. - const entpackt = flateVersuchen(daten) - if (entpackt) stroeme.push(entpackt.toString('latin1')) + const entpackt = flateVersuchen(daten, MAX_GESAMT_BYTES - gesamt) + if (entpackt) { + gesamt += entpackt.length + stroeme.push(entpackt.toString('latin1')) + } } return stroeme } -function flateVersuchen(daten: Buffer): Buffer | null { +/** + * Packt einen Strom aus - mit Deckel. + * + * maxOutputLength laesst zlib abbrechen, sobald die Ausgabe die Grenze + * ueberschreitet; der Fehler landet im catch wie jeder andere. Ohne diese + * Angabe entpackt Node so lange, wie Speicher da ist. + * + * @param rest Wie viele Bytes ueber alle Stroeme noch uebrig sind. + */ +function flateVersuchen(daten: Buffer, rest: number): Buffer | null { + const grenze = Math.min(MAX_STROM_BYTES, rest) + if (grenze <= 0) return null + for (const fn of [inflateSync, unzipSync, inflateRawSync]) { try { - return fn(daten) + return fn(daten, { maxOutputLength: grenze }) } catch { // naechster Versuch } diff --git a/src/lib/speicher/datei.ts b/src/lib/speicher/datei.ts index bc4264b..415cdd3 100644 --- a/src/lib/speicher/datei.ts +++ b/src/lib/speicher/datei.ts @@ -1,8 +1,8 @@ import 'server-only' -import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { dirname, join, resolve, sep } from 'node:path' import { env } from '@/lib/env' +import { entschluesselnBinaer, verschluesselnBinaer } from '@/lib/speicher/umschlag' import { dateiRoutePfad, schluesselErfordern, @@ -23,15 +23,10 @@ import { * gibt sonst Bescheide, Arztbriefe und Kontoauszuege im Klartext preis. * Der Schluessel liegt in der Umgebung, nicht neben den Daten. * - * Umschlag (binaer): - * "BCD1" | Schluesselversion (uint32 BE) | IV (12) | AuthTag (16) | Geheimtext + * Der Umschlag selbst steht in src/lib/speicher/umschlag.ts - der + * Objektspeicher benutzt denselben. */ -const MAGIE = Buffer.from('BCD1', 'ascii') -const IV_LAENGE = 12 -const TAG_LAENGE = 16 -const KOPF_LAENGE = MAGIE.length + 4 + IV_LAENGE + TAG_LAENGE - function wurzel(): string { return resolve(process.cwd(), env.dateiSpeicherPfad) } @@ -39,8 +34,18 @@ function wurzel(): string { /** * Baut den Dateipfad - und prueft danach noch einmal, dass er wirklich * unterhalb des Ablageverzeichnisses liegt. Der Schluessel ist zwar bereits - * streng geprueft, aber diese zweite Sperre kostet nichts und faengt auch - * Symlink-Tricks und Windows-Eigenheiten ab. + * streng geprueft, aber diese zweite Sperre kostet nichts. + * + * Was sie NICHT abfaengt, obwohl hier lange das Gegenteil stand: Symlinks. + * resolve() rechnet nur mit Zeichenketten und folgt keinem Link - dafuer + * braeuchte es realpath(). Ein untergeschobener Symlink im Ablageverzeichnis + * wuerde also nicht auffallen. + * + * Nachgeruestet wird das trotzdem nicht, und zwar aus einem Grund und nicht + * aus Bequemlichkeit: Wer dort einen Symlink anlegen kann, hat bereits + * Schreibrechte im Datenverzeichnis des Servers - dann ist der Umweg ueber + * einen Link die Sorge, die am wenigsten draengt. Der Satz musste + * verschwinden, weil er eine Sicherheit versprach, die es nie gab. */ function pfadFuer(schluessel: string): string { const geprueft = schluesselErfordern(schluessel) @@ -52,57 +57,15 @@ function pfadFuer(schluessel: string): string { return ziel } -function verschluesselnBinaer(inhalt: Buffer): Buffer { - const version = env.aktiveSchluesselversion - const schluessel = env.verschluesselungsschluessel.get(version) - if (!schluessel) throw new Error(`Verschlüsselungsschlüssel Version ${version} fehlt.`) - - const iv = randomBytes(IV_LAENGE) - const cipher = createCipheriv('aes-256-gcm', schluessel, iv, { authTagLength: TAG_LAENGE }) - cipher.setAAD(Buffer.from(`bcd:${version}`, 'utf8')) - const geheim = Buffer.concat([cipher.update(inhalt), cipher.final()]) - - const versionBytes = Buffer.alloc(4) - versionBytes.writeUInt32BE(version, 0) - - return Buffer.concat([MAGIE, versionBytes, iv, cipher.getAuthTag(), geheim]) -} - -function entschluesselnBinaer(roh: Buffer): Buffer { - // Dateien ohne Umschlag (von Hand hineingelegt, aus einer aelteren - // Installation uebernommen) werden unveraendert durchgereicht. - if (roh.length < KOPF_LAENGE || !roh.subarray(0, MAGIE.length).equals(MAGIE)) return roh - - const version = roh.readUInt32BE(MAGIE.length) - const schluessel = env.verschluesselungsschluessel.get(version) - if (!schluessel) { - throw new Error( - `Zum Entschlüsseln dieser Datei fehlt ENC_KEY_V${version}. Alte Schlüssel dürfen beim ` + - `Rotieren nicht aus der Umgebung entfernt werden.`, - ) - } - - const ivVon = MAGIE.length + 4 - const iv = roh.subarray(ivVon, ivVon + IV_LAENGE) - const tag = roh.subarray(ivVon + IV_LAENGE, ivVon + IV_LAENGE + TAG_LAENGE) - const geheim = roh.subarray(KOPF_LAENGE) - - const decipher = createDecipheriv('aes-256-gcm', schluessel, iv, { authTagLength: TAG_LAENGE }) - decipher.setAAD(Buffer.from(`bcd:${version}`, 'utf8')) - decipher.setAuthTag(tag) - - return Buffer.concat([decipher.update(geheim), decipher.final()]) -} - export async function schreiben(schluessel: string, inhalt: Buffer): Promise { const pfad = pfadFuer(schluessel) await mkdir(dirname(pfad), { recursive: true }) - await writeFile(pfad, verschluesselnBinaer(inhalt), { mode: 0o600 }) + await writeFile(pfad, verschluesselnBinaer(inhalt, schluessel), { mode: 0o600 }) } export async function lesen(schluessel: string): Promise { const roh = await readFile(pfadFuer(schluessel)) - return entschluesselnBinaer(roh) + return entschluesselnBinaer(roh, schluessel) } export async function entfernen(schluessel: string): Promise { diff --git a/src/lib/speicher/s3.ts b/src/lib/speicher/s3.ts index 35c092f..e0dcde8 100644 --- a/src/lib/speicher/s3.ts +++ b/src/lib/speicher/s3.ts @@ -3,12 +3,18 @@ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, + ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3' -import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { env } from '@/lib/env' -import { schluesselErfordern, TOKEN_STANDARD_SEKUNDEN } from '@/lib/speicher/schluessel' +import { + dateiRoutePfad, + schluesselErfordern, + zugriffTokenErzeugen, + TOKEN_STANDARD_SEKUNDEN, +} from '@/lib/speicher/schluessel' +import { entschluesselnBinaer, verschluesselnBinaer } from '@/lib/speicher/umschlag' /** * Treiber "s3": S3-kompatibler Objektspeicher, betrieben in der EU. @@ -17,10 +23,28 @@ import { schluesselErfordern, TOKEN_STANDARD_SEKUNDEN } from '@/lib/speicher/sch * Art. 28 DSGVO mit dem Anbieter - hier liegen Gesundheits- und * Finanzdaten. Ohne diesen Vertrag bleibt es beim Dateitreiber. * - * Ausgeliefert wird nur ueber kurzlebige presigned URLs. Sie tragen - * Content-Disposition: attachment und einen festen Content-Type, damit der - * Objektspeicher eine hochgeladene Datei niemals als HTML im Browser - * rendern kann. + * ## Anwendungsseitige Verschluesselung + * + * Frueher stand hier nur ServerSideEncryption: 'AES256'. Das ist + * Verschluesselung mit dem Schluessel des Anbieters: Er entschluesselt beim + * Ausliefern selbst. Gegen eine verlorene Festplatte im Rechenzentrum hilft + * das, gegen den Anbieter und gegen gestohlene Zugangsdaten zum Eimer + * nicht - und in diesem Eimer liegen Bescheide, Arztbriefe und + * Kontoauszuege. + * + * Deshalb liegt jetzt derselbe Umschlag darum wie beim Dateitreiber, mit + * einem Schluessel aus der Umgebung. Die serverseitige Verschluesselung + * bleibt zusaetzlich stehen; sie kostet nichts. + * + * ## Und deshalb keine presigned URLs mehr + * + * Eine presigned URL laesst den Browser direkt beim Objektspeicher laden. + * Was dort liegt, ist jetzt aber Geheimtext - der Browser bekaeme + * unlesbare Bytes. Die Auslieferung geht darum ueber dieselbe eigene Route + * wie beim Dateitreiber: Sie prueft zusaetzlich die Berechtigung des + * angemeldeten Benutzers und schreibt einen Audit-Eintrag. Dass jeder + * Abruf durch die Anwendung laeuft, ist bei einer Handvoll Dokumenten am + * Tag kein Thema. */ let klient: S3Client | null = null @@ -42,18 +66,29 @@ function eimer(): string { return env.s3.bucket } +/** + * Legt ein Objekt ab - verschluesselt. + * + * Der gemeldete MIME-Typ wird nicht mehr gebraucht: Was hier landet, ist + * Geheimtext und wird als application/octet-stream ausgezeichnet. Der + * Parameter bleibt in der Signatur, damit die Fassade beide Treiber gleich + * aufrufen kann. + */ export async function schreiben( schluessel: string, inhalt: Buffer, - mimeTyp: string, + _mimeTyp: string, ): Promise { const geprueft = schluesselErfordern(schluessel) await s3().send( new PutObjectCommand({ Bucket: eimer(), Key: geprueft, - Body: inhalt, - ContentType: mimeTyp, + Body: verschluesselnBinaer(inhalt, geprueft), + // Bewusst nicht der echte Typ: Was hier liegt, ist Geheimtext. Ein + // "application/pdf" darauf waere eine Falschauskunft, und der einzige + // Weg zum Inhalt fuehrt ohnehin durch die Anwendung. + ContentType: 'application/octet-stream', // Auch beim direkten Abruf niemals im Browser darstellen. ContentDisposition: 'attachment', // Serverseitige Verschluesselung, wo der Anbieter sie unterstuetzt. @@ -67,7 +102,10 @@ export async function lesen(schluessel: string): Promise { const antwort = await s3().send(new GetObjectCommand({ Bucket: eimer(), Key: geprueft })) if (!antwort.Body) throw new Error(`Objekt ${geprueft} ist leer.`) const bytes = await antwort.Body.transformToByteArray() - return Buffer.from(bytes) + // Objekte, die vor dieser Aenderung abgelegt wurden, tragen keinen + // Umschlag und kommen unveraendert zurueck. Der Migrationslauf + // (npm run speicher:verschluesseln) schreibt sie nach und nach um. + return entschluesselnBinaer(Buffer.from(bytes), geprueft) } export async function entfernen(schluessel: string): Promise { @@ -85,20 +123,54 @@ export async function existiert(schluessel: string): Promise { } } +/** + * Zeigt auf die eigene, signierte Route - nicht mehr auf den + * Objektspeicher. + * + * Der direkte Weg zum Anbieter lieferte den Rohinhalt aus. Solange der + * Klartext war, ging das; jetzt ist es Geheimtext, und entschluesseln kann + * nur die Anwendung. Der Nebeneffekt ist erwuenscht: Die Route prueft die + * Berechtigung des angemeldeten Benutzers und schreibt einen Audit-Eintrag. + * Ein weitergeleiteter Link allein oeffnet also nichts mehr. + */ export async function signierteUrl( schluessel: string, sekunden: number = TOKEN_STANDARD_SEKUNDEN, ): Promise { const geprueft = schluesselErfordern(schluessel) - return getSignedUrl( - s3(), - new GetObjectCommand({ - Bucket: eimer(), - Key: geprueft, - // Die Antwort des Objektspeichers wird hier festgenagelt: Anhang, - // niemals HTML, egal was beim Hochladen gemeldet wurde. - ResponseContentDisposition: 'attachment', - }), - { expiresIn: Math.min(Math.max(Math.floor(sekunden), 1), 60 * 60) }, - ) + return dateiRoutePfad(zugriffTokenErzeugen(geprueft, sekunden)) +} + +// ----------------------------------------------------------------------------- +// Wartung +// ----------------------------------------------------------------------------- +// +// Nur fuer den Migrationslauf, der bestehende Objekte nachtraeglich in den +// Umschlag steckt. Bewusst hier und nicht im Skript: Der Zugang zum Eimer +// wird an genau einer Stelle aufgebaut. + +/** Alle Ablageschluessel im Eimer, seitenweise geholt. */ +export async function objekteAuflisten(): Promise { + const gefunden: string[] = [] + let weiter: string | undefined + + do { + const antwort = await s3().send( + new ListObjectsV2Command({ Bucket: eimer(), ContinuationToken: weiter }), + ) + for (const eintrag of antwort.Contents ?? []) { + if (eintrag.Key) gefunden.push(eintrag.Key) + } + weiter = antwort.IsTruncated ? antwort.NextContinuationToken : undefined + } while (weiter) + + return gefunden +} + +/** Der Inhalt, wie er im Eimer liegt - ohne den Umschlag abzunehmen. */ +export async function rohLesen(schluessel: string): Promise { + const geprueft = schluesselErfordern(schluessel) + const antwort = await s3().send(new GetObjectCommand({ Bucket: eimer(), Key: geprueft })) + if (!antwort.Body) throw new Error(`Objekt ${geprueft} ist leer.`) + return Buffer.from(await antwort.Body.transformToByteArray()) } diff --git a/src/lib/speicher/umschlag.ts b/src/lib/speicher/umschlag.ts new file mode 100644 index 0000000..4ce1d99 --- /dev/null +++ b/src/lib/speicher/umschlag.ts @@ -0,0 +1,119 @@ +import 'server-only' +import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' +import { env } from '@/lib/env' + +/** + * Anwendungsseitiger Umschlag fuer abgelegte Dateien (AES-256-GCM). + * + * Der Dateitreiber hat das von Anfang an getan, der Objektspeicher nicht: + * Dort stand nur ServerSideEncryption: 'AES256'. Das ist Verschluesselung + * mit dem Schluessel des Anbieters - er entschluesselt beim Ausliefern + * selbst. Gegen ein kopiertes Backup hilft es, gegen den Anbieter und gegen + * gestohlene Zugangsdaten zum Eimer nicht. Bei Bescheiden, Arztbriefen und + * Kontoauszuegen ist das der falsche Zuschnitt. + * + * Jetzt liegt derselbe Umschlag um beides. Der Schluessel kommt aus der + * Umgebung und nie aus der Datenbank oder vom Anbieter. + * + * ## Format + * + * "BCD1" | Schluesselversion (uint32 BE) | IV (12) | AuthTag (16) | Geheimtext + * + * Bewusst dasselbe Format wie bisher im Dateitreiber - bestehende Dateien + * bleiben ohne Umschreiben lesbar. + * + * ## Zusatzdaten + * + * Mitauthentifiziert wird die Schluesselversion und, wo der Aufrufer ihn + * angibt, der Ablageschluessel des Objekts. Letzteres bindet den Geheimtext + * an seinen Platz: Wer Schreibrechte auf den Eimer hat, kann die Bytes von + * Dokument A nicht unter dem Namen von Dokument B ablegen - beim Lesen + * schlaegt die Pruefung fehl, statt den falschen Bescheid auszuliefern. + * + * Ohne Angabe bleibt es bei "bcd:", damit die Dateien, die der + * Dateitreiber frueher geschrieben hat, weiter aufgehen. + */ + +const MAGIE = Buffer.from('BCD1', 'ascii') +const IV_LAENGE = 12 +const TAG_LAENGE = 16 + +export const KOPF_LAENGE = MAGIE.length + 4 + IV_LAENGE + TAG_LAENGE + +function zusatzdaten(version: number, ablageschluessel?: string): Buffer { + return Buffer.from( + ablageschluessel ? `bcd:${version}:${ablageschluessel}` : `bcd:${version}`, + 'utf8', + ) +} + +/** Traegt dieser Inhalt bereits einen Umschlag? */ +export function istVerschluesselt(roh: Buffer): boolean { + return roh.length >= KOPF_LAENGE && roh.subarray(0, MAGIE.length).equals(MAGIE) +} + +export function verschluesselnBinaer(inhalt: Buffer, ablageschluessel?: string): Buffer { + const version = env.aktiveSchluesselversion + const schluessel = env.verschluesselungsschluessel.get(version) + if (!schluessel) throw new Error(`Verschlüsselungsschlüssel Version ${version} fehlt.`) + + const iv = randomBytes(IV_LAENGE) + const cipher = createCipheriv('aes-256-gcm', schluessel, iv, { authTagLength: TAG_LAENGE }) + cipher.setAAD(zusatzdaten(version, ablageschluessel)) + const geheim = Buffer.concat([cipher.update(inhalt), cipher.final()]) + + const versionBytes = Buffer.alloc(4) + versionBytes.writeUInt32BE(version, 0) + + return Buffer.concat([MAGIE, versionBytes, iv, cipher.getAuthTag(), geheim]) +} + +/** + * Nimmt den Umschlag ab. + * + * Inhalte ohne Umschlag werden unveraendert durchgereicht: Dateien aus + * einer aelteren Installation, von Hand hineingelegte, und - bis der + * Migrationslauf durch ist - die Objekte, die vor dieser Aenderung im + * Objektspeicher lagen. + */ +export function entschluesselnBinaer(roh: Buffer, ablageschluessel?: string): Buffer { + if (!istVerschluesselt(roh)) return roh + + const version = roh.readUInt32BE(MAGIE.length) + const schluessel = env.verschluesselungsschluessel.get(version) + if (!schluessel) { + throw new Error( + `Zum Entschlüsseln dieser Datei fehlt ENC_KEY_V${version}. Alte Schlüssel dürfen beim ` + + `Rotieren nicht aus der Umgebung entfernt werden.`, + ) + } + + const ivVon = MAGIE.length + 4 + const iv = roh.subarray(ivVon, ivVon + IV_LAENGE) + const tag = roh.subarray(ivVon + IV_LAENGE, ivVon + IV_LAENGE + TAG_LAENGE) + const geheim = roh.subarray(KOPF_LAENGE) + + // Zwei Anlaeufe: erst mit dem Ablageschluessel als Zusatzdaten, dann ohne. + // Der zweite Anlauf ist fuer alles, was vor der Bindung an den Platz + // geschrieben wurde - ohne ihn waeren die alten Dateien nicht mehr + // lesbar, und das waere ein Datenverlust fuer eine Verbesserung. + const varianten = ablageschluessel ? [ablageschluessel, undefined] : [undefined] + + let letzterFehler: unknown = null + for (const variante of varianten) { + try { + const decipher = createDecipheriv('aes-256-gcm', schluessel, iv, { + authTagLength: TAG_LAENGE, + }) + decipher.setAAD(zusatzdaten(version, variante)) + decipher.setAuthTag(tag) + return Buffer.concat([decipher.update(geheim), decipher.final()]) + } catch (fehler) { + letzterFehler = fehler + } + } + + throw letzterFehler instanceof Error + ? letzterFehler + : new Error('Die Datei ließ sich nicht entschlüsseln.') +} diff --git a/src/lib/version.ts b/src/lib/version.ts index d393081..710fae4 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -17,7 +17,7 @@ */ /** Muss zur "version" in der package.json passen. */ -export const VERSION = '1.10.2' +export const VERSION = '1.11.0' /** * Kurzer Git-Stand, beim Bauen hineingereicht: diff --git a/src/middleware.ts b/src/middleware.ts index 1d2c941..03661cb 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -30,8 +30,21 @@ import { NextResponse, type NextRequest } from 'next/server' * Data-URI eingebettet, statt sie ueber einen Fremddienst zu holen. */ -/** Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen. */ -const OEFFENTLICHE_PFADE = [ +/** + * Seiten, die auch ohne Anmeldung sinnvoll im Verlauf stehen duerfen. + * + * ACHTUNG - was diese Liste NICHT ist: eine Zugriffskontrolle. Frueher + * stand hier "Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen", + * und das war schlicht falsch: Die Middleware prueft keine Anmeldung, sie + * setzt Kopfzeilen. Ausgewertet wird die Liste einzig fuer die Frage, ob + * "Cache-Control: no-store" gesetzt wird. + * + * Der Irrtum waere teuer geworden: Wer den Satz glaubt, verzichtet an einer + * neuen Seite auf erfordereBenutzer() - "die Middleware faengt das ja ab". + * Sie faengt nichts ab. Der Zugriffsschutz sitzt in + * src/lib/auth/sitzung.ts, und nur dort. + */ +const OHNE_CACHE_SPERRE = [ '/anmelden', '/ersteinrichtung', '/datenschutz', @@ -79,7 +92,7 @@ export function middleware(anfrage: NextRequest) { // Verhindert, dass ein Bericht oder Beleg im Verlauf des Browsers // zwischengespeichert wird. - if (!OEFFENTLICHE_PFADE.some((pfad) => anfrage.nextUrl.pathname.startsWith(pfad))) { + if (!OHNE_CACHE_SPERRE.some((pfad) => anfrage.nextUrl.pathname.startsWith(pfad))) { antwort.headers.set('Cache-Control', 'no-store, must-revalidate') } diff --git a/src/server/actions/anmelden.ts b/src/server/actions/anmelden.ts index af48882..1f1a66f 100644 --- a/src/server/actions/anmelden.ts +++ b/src/server/actions/anmelden.ts @@ -5,6 +5,7 @@ import { headers } from 'next/headers' import { z } from 'zod' import { signIn } from '@/auth' import { anmeldungErlaubt } from '@/lib/auth/ratelimit' +import { anschlussAus } from '@/lib/auth/anschluss' /** * Server Action fuer das Anmeldeformular. @@ -43,11 +44,10 @@ function istWeiterleitung(fehler: unknown): boolean { ) } -/** Hinter dem Reverse Proxy steht die echte Adresse im weitergereichten Kopf. */ +/** Siehe src/lib/auth/anschluss.ts - der Kopf wird nur mit Proxy geglaubt. */ async function ipDerAnfrage(): Promise { try { - const kopf = await headers() - return kopf.get('x-forwarded-for')?.split(',')[0]?.trim() || kopf.get('x-real-ip') || null + return anschlussAus(await headers()) } catch { return null } diff --git a/src/server/actions/bestaetigen.ts b/src/server/actions/bestaetigen.ts index f3d0f99..6283303 100644 --- a/src/server/actions/bestaetigen.ts +++ b/src/server/actions/bestaetigen.ts @@ -5,7 +5,7 @@ import { z } from 'zod' import { db } from '@/lib/db' import { erfordereBenutzer, reauthBestaetigen } from '@/lib/auth/sitzung' import { passwortPruefen } from '@/lib/crypto/hash' -import { totpPruefen, totpSecretEntschluesseln } from '@/lib/crypto/totp' +import { zweitenFaktorEinloesen } from '@/lib/auth/zweiter-faktor' import { aufMindestdauerWarten } from '@/lib/auth/ratelimit' /** @@ -56,10 +56,14 @@ export async function anmeldungBestaetigen( const passwortStimmt = await passwortPruefen(frisch.passwortHash, gelesen.data.passwort) + // Der Code wird nur eingeloest, wenn das Passwort stimmt. Sonst + // verbrauchte ein falsches Passwort mit richtigem Code das Zeitfenster, + // und der Benutzer muesste dreissig Sekunden auf den naechsten warten - + // ohne zu verstehen, warum. let codeStimmt = true - if (frisch.totpSecretVerschluesselt && frisch.totpAktiviertAm) { - const secret = totpSecretEntschluesseln(frisch.totpSecretVerschluesselt) - codeStimmt = totpPruefen(secret, frisch.email, gelesen.data.code).gueltig + if (passwortStimmt && frisch.totpSecretVerschluesselt && frisch.totpAktiviertAm) { + const eingeloest = await zweitenFaktorEinloesen(frisch, gelesen.data.code) + codeStimmt = eingeloest.ok } if (!passwortStimmt || !codeStimmt) { diff --git a/src/server/actions/ersteinrichtung.ts b/src/server/actions/ersteinrichtung.ts index 94867e5..d1d702b 100644 --- a/src/server/actions/ersteinrichtung.ts +++ b/src/server/actions/ersteinrichtung.ts @@ -1,25 +1,49 @@ 'use server' import { redirect } from 'next/navigation' +import { createHash, timingSafeEqual } from 'node:crypto' import { z } from 'zod' import { db } from '@/lib/db' +import { env } from '@/lib/env' import { passwortHashen } from '@/lib/crypto/hash' import { protokollieren } from '@/lib/audit' +import { aufMindestdauerWarten } from '@/lib/auth/ratelimit' /** * Anlegen des allerersten Zugangs. * - * Diese Aktion ist nur so lange erreichbar, wie es noch keinen Benutzer - * gibt. Danach sperrt sie sich selbst - sonst koennte sich jemand von - * aussen einen Administratorzugang anlegen. + * Diese Aktion ist die einzige Stelle der Anwendung, an der jemand ohne + * Anmeldung etwas schreiben darf. Entsprechend eng ist sie gebaut - zwei + * Dinge sichern sie ab: + * + * ## 1. Ein Geheimnis aus der Umgebung + * + * Vorher galt: "Wenn es noch keinen Benutzer gibt, darf jeder einen + * anlegen." Zwischen dem ersten Start und dem Anlegen des Zugangs stand die + * Seite damit offen - wer sie in diesem Fenster fand, wurde Administrator + * einer Anwendung mit Gesundheits- und Finanzdaten. Das Fenster ist kurz, + * aber Zufall ist keine Zugriffskontrolle. Jetzt muss ERSTEINRICHTUNG_ + * GEHEIMNIS aus der .env mit eingegeben werden. + * + * ## 2. Eine Sperre in der Datenbank + * + * Die Pruefung "gibt es schon einen Benutzer?" und das Anlegen waren zwei + * getrennte Schritte. Zwei Anfragen gleichzeitig kamen beide durch die + * Pruefung, bevor eine von ihnen schrieb - und legten beide einen + * Administrator an. Jetzt entscheidet der Primaerschluessel der Tabelle + * ersteinrichtung, und der laesst genau einen Gewinner zu. */ const PASSWORT_MINDESTLAENGE = 12 +/** Fester Schluessel der einen Zeile, die die Sperre bildet. */ +const SPERRE = 'einmalig' + const ZugangSchema = z .object({ name: z.string().trim().min(2, 'Bitte deinen Namen angeben.').max(120), email: z.string().trim().toLowerCase().email('Bitte eine gültige E-Mail-Adresse angeben.'), + geheimnis: z.string().min(1, 'Bitte das Einrichtungsgeheimnis aus der .env eingeben.'), passwort: z .string() .min( @@ -37,22 +61,54 @@ const ZugangSchema = z export type ZugangZustand = { fehler?: string - felder?: Partial> + felder?: Partial< + Record<'name' | 'email' | 'passwort' | 'passwortWiederholung' | 'geheimnis', string> + > werte?: { name?: string; email?: string } } +/** + * Vergleicht zwei Zeichenketten in gleichbleibender Zeit. + * + * Ueber die Laenge wird gehasht, nicht abgeschnitten: timingSafeEqual will + * gleich lange Puffer, und ein vorgeschalteter Laengenvergleich verriete + * die Laenge des Geheimnisses. + */ +function gleich(eingabe: string, erwartet: string): boolean { + const a = createHash('sha256').update(eingabe).digest() + const b = createHash('sha256').update(erwartet).digest() + return timingSafeEqual(a, b) +} + export async function erstenZugangAnlegen( _vorher: ZugangZustand, formular: FormData, ): Promise { - const vorhandene = await db.user.count() - if (vorhandene > 0) { + const begonnen = Date.now() + + // Ist die Einrichtung schon gelaufen, ist hier Schluss - unabhaengig + // davon, ob der Benutzer inzwischen im Papierkorb liegt. + const bereitsErledigt = await db.ersteinrichtung.findUnique({ + where: { id: SPERRE }, + select: { id: true }, + }) + if (bereitsErledigt || (await db.user.count()) > 0) { return { fehler: 'Es gibt bereits einen Zugang. Bitte melde dich an.' } } + if (env.ersteinrichtungGeheimnis === '') { + return { + fehler: + 'Die Ersteinrichtung ist gesperrt, weil ERSTEINRICHTUNG_GEHEIMNIS in der .env fehlt. ' + + 'Trage dort einen zufälligen Wert ein („npm run schluessel“ erzeugt passende), starte ' + + 'die Anwendung neu und gib ihn hier ein.', + } + } + const gelesen = ZugangSchema.safeParse({ name: formular.get('name'), email: formular.get('email'), + geheimnis: formular.get('geheimnis'), passwort: formular.get('passwort'), passwortWiederholung: formular.get('passwortWiederholung'), }) @@ -64,6 +120,7 @@ export async function erstenZugangAnlegen( felder: { name: felder.name?.[0], email: felder.email?.[0], + geheimnis: felder.geheimnis?.[0], passwort: felder.passwort?.[0], passwortWiederholung: felder.passwortWiederholung?.[0], }, @@ -74,24 +131,50 @@ export async function erstenZugangAnlegen( } } - const { name, email, passwort } = gelesen.data + const { name, email, passwort, geheimnis } = gelesen.data - const benutzer = await db.user.create({ - data: { - name, - email, - passwortHash: await passwortHashen(passwort), - // Der erste Zugang verwaltet auch die Anwendung. - rolle: 'ADMIN', - }, - }) + if (!gleich(geheimnis, env.ersteinrichtungGeheimnis)) { + // Dieselbe Mindestdauer wie am Anmeldeformular: Sonst verraet die + // Antwortzeit, ob das Geheimnis stimmte und erst das Anlegen scheiterte. + await aufMindestdauerWarten(begonnen) + return { + fehler: 'Das Einrichtungsgeheimnis stimmt nicht.', + felder: { geheimnis: 'Stimmt nicht mit ERSTEINRICHTUNG_GEHEIMNIS überein.' }, + werte: { name, email }, + } + } + + // Anlegen und Sperren in einem Zug. Kommt eine zweite Anfrage gleichzeitig + // hier an, scheitert sie am Primaerschluessel der Sperrzeile - und zwar + // bevor ein zweiter Administrator entstanden ist. + let benutzerId: string + try { + const ergebnis = await db.$transaction(async (tx) => { + const benutzer = await tx.user.create({ + data: { + name, + email, + passwortHash: await passwortHashen(passwort), + // Der erste Zugang verwaltet auch die Anwendung. + rolle: 'ADMIN', + }, + }) + await tx.ersteinrichtung.create({ data: { id: SPERRE, userId: benutzer.id } }) + return benutzer + }) + benutzerId = ergebnis.id + } catch { + // Der haeufigste Fall ist die verlorene Wettlauf-Anfrage. Für den, der + // davorsitzt, ist die Auskunft dieselbe wie oben. + return { fehler: 'Es gibt bereits einen Zugang. Bitte melde dich an.' } + } await protokollieren({ aktion: 'ERSTELLT', entitaet: 'User', - entitaetId: benutzer.id, + entitaetId: benutzerId, bezeichnung: `${name} (erster Zugang)`, - userId: benutzer.id, + userId: benutzerId, userEmail: email, userName: name, }) diff --git a/src/server/actions/passkey.ts b/src/server/actions/passkey.ts index 41f3919..014a84f 100644 --- a/src/server/actions/passkey.ts +++ b/src/server/actions/passkey.ts @@ -20,7 +20,11 @@ import { registrierungPruefen, registrierungVorbereiten, } from '@/lib/auth/passkey' -import { erfordereBenutzer, reauthBestaetigen } from '@/lib/auth/sitzung' +import { + erfordereBenutzer, + erfordereFrischeAnmeldung, + reauthBestaetigen, +} from '@/lib/auth/sitzung' /** * Server Actions rund um Passkeys. @@ -42,6 +46,21 @@ export type PasskeyStart = { ok: true; optionen: T } | { ok: false; fehler: s /** Ergebnis eines abschliessenden Schritts. */ export type PasskeyErgebnis = { ok: true; meldung?: string } | { ok: false; fehler: string } +/** + * Erkennt die Weiterleitung, die Next.js intern als Ausnahme wirft. + * Wie in anmelden.ts ueber das digest-Feld statt ueber einen Import aus + * next/dist - interne Pfade aendern sich zwischen Nebenversionen. + */ +function istWeiterleitung(fehler: unknown): boolean { + return ( + typeof fehler === 'object' && + fehler !== null && + 'digest' in fehler && + typeof (fehler as { digest?: unknown }).digest === 'string' && + (fehler as { digest: string }).digest.startsWith('NEXT_REDIRECT') + ) +} + function alsFehler(fehler: unknown): { ok: false; fehler: string } { if (fehler instanceof PasskeyFehler) { return { ok: false, fehler: PASSKEY_MELDUNG[fehler.grund] } @@ -50,6 +69,18 @@ function alsFehler(fehler: unknown): { ok: false; fehler: string } { return { ok: false, fehler: PASSKEY_MELDUNG.ungueltig } } +/** + * Wie alsFehler, wirft aber die Weiterleitung von Next durch. + * + * Ohne das verschwindet der Ruf nach einer erneuten Bestaetigung in der + * allgemeinen Meldung "Der Passkey konnte nicht geprueft werden" - der + * Benutzer sieht einen Fehler, wo eine Zwischenfrage gemeint war. + */ +function alsFehlerOderWeiter(fehler: unknown): { ok: false; fehler: string } { + if (istWeiterleitung(fehler)) throw fehler + return alsFehler(fehler) +} + /** * Parst die Antwort des Browsers. * @@ -74,14 +105,26 @@ function antwortLesen(roh: string): T { // Einrichten (angemeldet, unter "Mein Konto") // --------------------------------------------------------------------------- +/** + * Ein Passkey ist ein vollwertiger Zugang - wer einen anlegt, kommt danach + * ohne Passwort und ohne Code herein. Deshalb dasselbe Vorzimmer wie beim + * Vollexport: Die Anmeldung muss frisch sein. + * + * Das ist genau der Fall, gegen den die erneute Bestaetigung da ist: Eine + * offene Sitzung an einem unbeaufsichtigten Rechner ist in diesem Umfeld + * der wahrscheinlichste Angriffsweg. Wer daran vorbeikaeme, richtete sich + * in zwei Klicks einen dauerhaften eigenen Zugang ein - und der ueberlebte + * auch einen Passwortwechsel. + */ export async function passkeyEinrichtungStarten(): Promise< PasskeyStart > { try { const benutzer = await erfordereBenutzer() + await erfordereFrischeAnmeldung(benutzer, '/konto') return { ok: true, optionen: await registrierungVorbereiten(benutzer) } } catch (fehler) { - return alsFehler(fehler) + return alsFehlerOderWeiter(fehler) } } @@ -97,6 +140,11 @@ export async function passkeyEinrichtungAbschliessen( ): Promise { try { const benutzer = await erfordereBenutzer() + // Auch im zweiten Schritt: Zwischen Optionen holen und Antwort + // einreichen liegt der Griff zum Sensor, und die Frist kann in dieser + // Zeit ablaufen. Ohne die Pruefung waere der erste Schritt nur eine + // Empfehlung. + await erfordereFrischeAnmeldung(benutzer, '/konto') const geprueft = NameSchema.safeParse(name) if (!geprueft.success) { @@ -122,7 +170,7 @@ export async function passkeyEinrichtungAbschliessen( revalidatePath('/konto') return { ok: true, meldung: `„${passkey.name}“ ist eingerichtet.` } } catch (fehler) { - return alsFehler(fehler) + return alsFehlerOderWeiter(fehler) } } @@ -130,9 +178,15 @@ export async function passkeyEinrichtungAbschliessen( // Verwalten // --------------------------------------------------------------------------- +/** + * Entfernen braucht die frische Anmeldung genauso wie das Anlegen: Wer + * einem Betreuer alle Passkeys wegnimmt, sperrt ihn von dem Weg aus, auf + * dem er hereinkommt. + */ export async function passkeyEntfernen(id: string): Promise { try { const benutzer = await erfordereBenutzer() + await erfordereFrischeAnmeldung(benutzer, '/konto') // Zeilenweise Pruefung: Es genuegt nicht, dass die ID existiert - sie // muss zu DIESEM Benutzer gehoeren. deleteMany mit userId im WHERE @@ -163,7 +217,7 @@ export async function passkeyEntfernen(id: string): Promise { revalidatePath('/konto') return { ok: true, meldung: `„${passkey.name}“ wurde entfernt.` } } catch (fehler) { - return alsFehler(fehler) + return alsFehlerOderWeiter(fehler) } } @@ -187,21 +241,6 @@ function sicheresZiel(ziel: string): string { return ziel } -/** - * Erkennt die Weiterleitung, die Next.js intern als Ausnahme wirft. - * Wie in anmelden.ts ueber das digest-Feld statt ueber einen Import aus - * next/dist - interne Pfade aendern sich zwischen Nebenversionen. - */ -function istWeiterleitung(fehler: unknown): boolean { - return ( - typeof fehler === 'object' && - fehler !== null && - 'digest' in fehler && - typeof (fehler as { digest?: unknown }).digest === 'string' && - (fehler as { digest: string }).digest.startsWith('NEXT_REDIRECT') - ) -} - /** * Uebersetzt den Fehlercode aus authorize() in einen lesbaren Satz. * "passkey:" kommt aus PasskeyAnmeldeFehler, alles andere aus dem diff --git a/src/server/actions/zwei-faktor.ts b/src/server/actions/zwei-faktor.ts index 7a5fae3..7cf0856 100644 --- a/src/server/actions/zwei-faktor.ts +++ b/src/server/actions/zwei-faktor.ts @@ -3,10 +3,15 @@ import { revalidatePath } from 'next/cache' import { z } from 'zod' import { db } from '@/lib/db' -import { erfordereBenutzer, erfordereFrischeAnmeldung } from '@/lib/auth/sitzung' +import { + erfordereBenutzer, + erfordereBenutzerOhneZweitenFaktor, + erfordereFrischeAnmeldung, +} from '@/lib/auth/sitzung' import { protokollieren } from '@/lib/audit' import { totpPruefen, + totpSchritt, totpQrCode, totpSecretEntschluesseln, totpSecretErzeugen, @@ -40,7 +45,9 @@ export type EinrichtungsDaten = { } export async function zweitenFaktorVorbereiten(): Promise { - const benutzer = await erfordereBenutzer() + // Ausnahme von der MFA-Pflicht, und zwar die offensichtliche: Wer den + // zweiten Faktor gerade einrichtet, hat ihn noch nicht. + const benutzer = await erfordereBenutzerOhneZweitenFaktor() if (benutzer.totpAktiviertAm && benutzer.totpSecretVerschluesselt) { // Schon eingerichtet - kein neues Geheimnis, sonst waere die alte App @@ -78,7 +85,8 @@ export async function zweitenFaktorBestaetigen( _vorher: BestaetigungsZustand, formular: FormData, ): Promise { - const benutzer = await erfordereBenutzer() + // Zweite und letzte Ausnahme: der Abschluss der Einrichtung selbst. + const benutzer = await erfordereBenutzerOhneZweitenFaktor() if (benutzer.totpAktiviertAm) { return { fehler: 'Der zweite Faktor ist bereits eingerichtet.' } @@ -93,9 +101,9 @@ export async function zweitenFaktorBestaetigen( } const secret = totpSecretEntschluesseln(benutzer.totpSecretVerschluesselt) - const { gueltig } = totpPruefen(secret, benutzer.email, gelesen.data) + const { gueltig, delta } = totpPruefen(secret, benutzer.email, gelesen.data) - if (!gueltig) { + if (!gueltig || delta === null) { return { fehler: 'Der Code stimmt nicht. Prüfe, ob die Uhrzeit deines Geräts automatisch gestellt wird — ' + @@ -108,7 +116,14 @@ export async function zweitenFaktorBestaetigen( await db.user.update({ where: { id: benutzer.id }, - data: { totpAktiviertAm: new Date(), wiederherstellungscodes: hashes }, + data: { + totpAktiviertAm: new Date(), + wiederherstellungscodes: hashes, + // Der Code, mit dem gerade bestaetigt wurde, ist verbraucht. Ohne + // diese Zeile liesse er sich unmittelbar danach fuer die erste + // Anmeldung noch einmal verwenden. + totpLetzterSchritt: totpSchritt(delta), + }, }) await protokollieren({ diff --git a/tests/anmeldesperre.test.ts b/tests/anmeldesperre.test.ts new file mode 100644 index 0000000..f8dbbf6 --- /dev/null +++ b/tests/anmeldesperre.test.ts @@ -0,0 +1,250 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +/** + * Die Anmeldesperre - und warum sie sich frueher selbst am Leben hielt. + * + * Bis v1.10.2 wurde jeder Aufruf festgehalten, auch der, der wegen einer + * laufenden Sperre gar nicht geprueft wurde. Gleichzeitig lief die Frist ab + * dem juengsten Fehlversuch. Beides zusammen ergab eine Rueckkopplung: Wer + * regelmaessig gegen eine gesperrte Adresse klopfte, verlaengerte die Sperre + * bei jedem Klopfen um die volle Dauer - und sperrte den Inhaber damit + * dauerhaft aus, ohne dessen Passwort zu kennen. Nebenbei wuchs die Tabelle + * mit jedem Versuch. + * + * Diese Tests laufen gegen eine Attrappe der Datenbank. Das reicht: Was hier + * geprueft wird, ist die Rechnung, nicht die Abfrage. + */ + +// Kuerzere Sperre als Zeitfenster - nur so wird sichtbar, ob die Frist +// wirklich ablaeuft oder bis zum Fensterende stehen bleibt. +process.env.LOGIN_MAX_VERSUCHE = '5' +process.env.LOGIN_FENSTER_SEKUNDEN = '3600' +process.env.LOGIN_SPERRE_SEKUNDEN = '900' + +type Zeile = { + id: string + email: string + ipHash: string | null + erfolg: boolean + grund: string | null + zeitpunkt: Date +} + +let tabelle: Zeile[] = [] +let laufendeNummer = 0 + +/** Nur die Abfrageformen, die in ratelimit.ts wirklich vorkommen. */ +function passt(zeile: Zeile, wo: Record): boolean { + if ('erfolg' in wo && zeile.erfolg !== wo.erfolg) return false + if ('email' in wo && zeile.email !== wo.email) return false + + if ('grund' in wo) { + const bedingung = wo.grund + if (typeof bedingung === 'object' && bedingung !== null && 'not' in bedingung) { + if (zeile.grund === (bedingung as { not: string }).not) return false + } else if (zeile.grund !== bedingung) { + return false + } + } + + if ('zeitpunkt' in wo) { + const { gte, lt } = wo.zeitpunkt as { gte?: Date; lt?: Date } + if (gte && zeile.zeitpunkt < gte) return false + if (lt && zeile.zeitpunkt >= lt) return false + } + + if ('OR' in wo) { + const zweige = wo.OR as Record[] + if (!zweige.some((zweig) => passt(zeile, zweig))) return false + } + + if ('ipHash' in wo && zeile.ipHash !== wo.ipHash) return false + + return true +} + +vi.mock('@/lib/db', () => ({ + db: { + loginVersuch: { + findMany: async ({ + where, + orderBy, + take, + }: { + where: Record + orderBy?: { zeitpunkt?: 'asc' | 'desc' } + take?: number + }) => { + const treffer = tabelle.filter((zeile) => passt(zeile, where)) + treffer.sort((a, b) => + orderBy?.zeitpunkt === 'desc' + ? b.zeitpunkt.getTime() - a.zeitpunkt.getTime() + : a.zeitpunkt.getTime() - b.zeitpunkt.getTime(), + ) + return take ? treffer.slice(0, take) : treffer + }, + + findFirst: async ({ where }: { where: Record }) => + tabelle.find((zeile) => passt(zeile, where)) ?? null, + + create: async ({ data }: { data: Omit }) => { + laufendeNummer += 1 + const zeile: Zeile = { + id: `z${laufendeNummer}`, + email: data.email, + ipHash: data.ipHash ?? null, + erfolg: data.erfolg, + grund: data.grund ?? null, + zeitpunkt: new Date(), + } + tabelle.push(zeile) + return zeile + }, + + deleteMany: async ({ where }: { where: Record }) => { + const vorher = tabelle.length + tabelle = tabelle.filter((zeile) => !passt(zeile, where)) + return { count: vorher - tabelle.length } + }, + }, + }, +})) + +const { anmeldungErlaubt, sperreVermerken, versuchFesthalten, fehlversucheZuruecksetzen } = + await import('@/lib/auth/ratelimit') + +const OPFER = 'opfer@example.de' +const ANSCHLUSS = '198.51.100.7' +const FREMD = '203.0.113.9' +const START = new Date('2026-08-24T12:00:00Z').getTime() + +/** Setzt die Uhr auf START plus so viele Sekunden. */ +function beiSekunde(sekunden: number): void { + vi.setSystemTime(new Date(START + sekunden * 1000)) +} + +async function fehlversuch(sekunde: number): Promise { + beiSekunde(sekunde) + await versuchFesthalten(OPFER, ANSCHLUSS, false, 'passwort-falsch') +} + +describe('Anmeldesperre', () => { + beforeEach(() => { + tabelle = [] + laufendeNummer = 0 + vi.useFakeTimers() + beiSekunde(0) + }) + + it('sperrt erst, wenn das Mass voll ist', async () => { + for (let i = 0; i < 4; i += 1) await fehlversuch(i) + + beiSekunde(10) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false) + + await fehlversuch(11) + beiSekunde(12) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + }) + + it('laesst die Frist ablaufen, obwohl die Fehlversuche noch im Zeitfenster stehen', async () => { + for (let i = 0; i < 5; i += 1) await fehlversuch(i) + + beiSekunde(890) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + + // Kurz danach offen - und mit vollem Kontingent, nicht mit null + // Restversuchen. Das Zeitfenster laeuft noch (3600 s), die fuenf + // Fehlversuche stehen also weiterhin in der Tabelle. + beiSekunde(905) + const frei = await anmeldungErlaubt(OPFER, ANSCHLUSS) + expect(frei.gesperrt).toBe(false) + if (!frei.gesperrt) expect(frei.verbleibendeVersuche).toBe(5) + }) + + it('laesst sich durch Klopfen waehrend der Sperre nicht verlaengern', async () => { + for (let i = 0; i < 5; i += 1) await fehlversuch(i) + + // Der Angreifer klopft mitten in der Sperre weiter. Genau das hat die + // Frist frueher jedes Mal um volle 900 s nach hinten geschoben. + for (const sekunde of [100, 300, 600, 880]) { + beiSekunde(sekunde) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + await sperreVermerken(OPFER, FREMD) + } + + // Die Frist haengt am fuenften echten Fehlversuch (Sekunde 4), nicht am + // letzten Klopfen (Sekunde 880). + beiSekunde(905) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false) + }) + + it('rechnet einen abgewiesenen Aufruf nicht als Fehlversuch', async () => { + // Vier echte Fehlversuche - das Mass ist noch nicht voll. + for (let i = 0; i < 4; i += 1) await fehlversuch(i) + + // Dazu ein Vermerk ueber einen abgewiesenen Aufruf. Er steht als + // fuenfte Zeile in der Tabelle, darf aber nicht mitzaehlen: Geprueft + // wurde bei ihm nichts, also sagt er ueber die Zugangsdaten nichts aus. + beiSekunde(5) + await sperreVermerken(OPFER, FREMD) + expect(tabelle.length).toBe(5) + + beiSekunde(6) + const stand = await anmeldungErlaubt(OPFER, ANSCHLUSS) + expect(stand.gesperrt).toBe(false) + if (!stand.gesperrt) expect(stand.verbleibendeVersuche).toBe(1) + }) + + it('rechnet die Frist ab dem Versuch, der das Mass vollgemacht hat', async () => { + // Vier Fehlversuche ganz frueh, der fuenfte deutlich spaeter. Die + // Sperre beginnt beim fuenften - nicht beim ersten und nicht beim + // naechsten, der noch kommt. + for (let i = 0; i < 4; i += 1) await fehlversuch(i) + await fehlversuch(600) + + beiSekunde(1400) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + + beiSekunde(1510) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false) + }) + + it('schreibt je Sperre hoechstens eine Zeile, egal wie oft geklopft wird', async () => { + for (let i = 0; i < 5; i += 1) await fehlversuch(i) + const nachSperre = tabelle.length + + for (let i = 0; i < 200; i += 1) { + beiSekunde(10 + i) + await sperreVermerken(OPFER, FREMD) + } + + expect(tabelle.length).toBe(nachSperre + 1) + }) + + it('zaehlt die naechste Runde erst nach Ablauf der Sperre wieder hoch', async () => { + for (let i = 0; i < 5; i += 1) await fehlversuch(i) + + await fehlversuch(906) + const stand = await anmeldungErlaubt(OPFER, ANSCHLUSS) + expect(stand.gesperrt).toBe(false) + if (!stand.gesperrt) expect(stand.verbleibendeVersuche).toBe(4) + + // Vier weitere - dann greift die zweite Sperre. + for (let i = 1; i < 5; i += 1) await fehlversuch(906 + i) + beiSekunde(915) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + }) + + it('raeumt die Fehlversuche nach erfolgreicher Anmeldung ab', async () => { + for (let i = 0; i < 5; i += 1) await fehlversuch(i) + beiSekunde(10) + expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true) + + await fehlversucheZuruecksetzen(OPFER) + + const frei = await anmeldungErlaubt(OPFER, ANSCHLUSS) + expect(frei.gesperrt).toBe(false) + if (!frei.gesperrt) expect(frei.verbleibendeVersuche).toBe(5) + }) +}) diff --git a/tests/anschluss.test.ts b/tests/anschluss.test.ts new file mode 100644 index 0000000..41822cb --- /dev/null +++ b/tests/anschluss.test.ts @@ -0,0 +1,136 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { anschlussAus, warnungZuruecksetzen } from '@/lib/auth/anschluss' + +/** + * Aus welchem Kopf die Adresse des Aufrufers gelesen wird. + * + * Vorher stand dort der ERSTE Eintrag von "x-forwarded-for". Das ist die + * klassische Verwechslung: Der Kopf ist eine Liste, an die jeder + * Zwischenknoten hinten anhaengt - der erste Eintrag ist also der, den der + * Aufrufer selbst mitgeschickt hat. Wer bei jeder Anfrage eine andere Zahl + * hineinschrieb, sah fuer die Anmeldesperre wie tausend Anschluesse aus. + */ + +function koepfe(werte: Record): Headers { + return new Headers(werte) +} + +describe('Anschluss des Aufrufers', () => { + beforeEach(() => { + warnungZuruecksetzen() + delete process.env.VERTRAUE_PROXY + delete process.env.VERTRAUE_PROXY_STUFEN + }) + + afterEach(() => { + delete process.env.VERTRAUE_PROXY + delete process.env.VERTRAUE_PROXY_STUFEN + vi.restoreAllMocks() + }) + + describe('ohne vertrauten Proxy', () => { + it('glaubt dem weitergereichten Kopf nicht', () => { + const warnung = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + expect(anschlussAus(koepfe({ 'x-forwarded-for': '203.0.113.9' }))).toBeNull() + expect(warnung).toHaveBeenCalledOnce() + }) + + it('warnt nur einmal, nicht bei jeder Anfrage', () => { + const warnung = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + for (let i = 0; i < 20; i += 1) { + anschlussAus(koepfe({ 'x-forwarded-for': `203.0.113.${i}` })) + } + + expect(warnung).toHaveBeenCalledOnce() + }) + + it('schweigt, wenn gar kein solcher Kopf ankommt', () => { + const warnung = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + expect(anschlussAus(koepfe({}))).toBeNull() + expect(warnung).not.toHaveBeenCalled() + }) + }) + + describe('mit einem vertrauten Proxy', () => { + beforeEach(() => { + process.env.VERTRAUE_PROXY = 'true' + }) + + it('nimmt den letzten Eintrag, nicht den ersten', () => { + // Links das, was der Aufrufer selbst behauptet hat; rechts das, was + // der Proxy gesehen hat. Nur letzteres ist etwas wert. + const kopf = koepfe({ 'x-forwarded-for': '10.0.0.1, 198.51.100.7' }) + + expect(anschlussAus(kopf)).toBe('198.51.100.7') + }) + + it('laesst sich durch eine erfundene Kette nicht taeuschen', () => { + const kopf = koepfe({ + 'x-forwarded-for': '1.1.1.1, 2.2.2.2, 3.3.3.3, 198.51.100.7', + }) + + expect(anschlussAus(kopf)).toBe('198.51.100.7') + }) + + it('nimmt den einzigen Eintrag, wenn der Aufrufer keinen mitschickte', () => { + expect(anschlussAus(koepfe({ 'x-forwarded-for': '198.51.100.7' }))).toBe('198.51.100.7') + }) + + it('faellt auf x-real-ip zurueck', () => { + expect(anschlussAus(koepfe({ 'x-real-ip': '198.51.100.7' }))).toBe('198.51.100.7') + }) + + it('gibt null, wenn nichts da ist', () => { + expect(anschlussAus(koepfe({}))).toBeNull() + }) + + it('kommt mit Leerraum und leeren Gliedern zurecht', () => { + const kopf = koepfe({ 'x-forwarded-for': ' 10.0.0.1 , , 198.51.100.7 ' }) + + expect(anschlussAus(kopf)).toBe('198.51.100.7') + }) + }) + + describe('mit mehreren Stufen', () => { + it('zaehlt die angegebene Anzahl von hinten ab', () => { + process.env.VERTRAUE_PROXY = 'true' + process.env.VERTRAUE_PROXY_STUFEN = '2' + + // Zwei eigene Zwischenknoten: der aeussere haengt 198.51.100.7 an, + // der innere 10.0.0.5. Der Aufrufer steht eine Stelle weiter links. + const kopf = koepfe({ 'x-forwarded-for': 'erfunden, 198.51.100.7, 10.0.0.5' }) + + expect(anschlussAus(kopf)).toBe('198.51.100.7') + }) + + it('greift nicht ueber den Anfang der Kette hinaus', () => { + process.env.VERTRAUE_PROXY = 'true' + process.env.VERTRAUE_PROXY_STUFEN = '9' + + expect(anschlussAus(koepfe({ 'x-forwarded-for': '198.51.100.7' }))).toBe('198.51.100.7') + }) + }) + + describe('der Schalter selbst', () => { + it('gilt nur bei ausdruecklichem Ja', () => { + for (const wert of ['false', 'nein', '0', 'vielleicht', '']) { + warnungZuruecksetzen() + vi.spyOn(console, 'warn').mockImplementation(() => {}) + process.env.VERTRAUE_PROXY = wert + + expect(anschlussAus(koepfe({ 'x-forwarded-for': '198.51.100.7' }))).toBeNull() + } + }) + + it('nimmt true, 1 und ja', () => { + for (const wert of ['true', '1', 'ja', 'TRUE', ' Ja ']) { + process.env.VERTRAUE_PROXY = wert + + expect(anschlussAus(koepfe({ 'x-forwarded-for': '198.51.100.7' }))).toBe('198.51.100.7') + } + }) + }) +}) diff --git a/tests/mfa-grenze.test.ts b/tests/mfa-grenze.test.ts new file mode 100644 index 0000000..1756356 --- /dev/null +++ b/tests/mfa-grenze.test.ts @@ -0,0 +1,133 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { describe, expect, it } from 'vitest' + +/** + * Die MFA-Pflicht als Grenze, nicht als Behauptung. + * + * "Der zweite Faktor ist Pflicht" stand lange im Kopf von src/auth.ts, + * durchgesetzt wurde es aber nur im App-Layout. Ein Layout schuetzt, was + * durch das Layout gerendert wird - Server Actions und Route Handler laufen + * daran vorbei. Wer eine Passwort-Sitzung hatte, die TOTP-Einrichtung aber + * nie abgeschlossen hat, kam trotzdem an die Daten. + * + * Seitdem steckt die Pruefung in erfordereBenutzer() selbst. Damit sie dort + * bleibt, prueft dieser Test die Bauform statt des Verhaltens: Er liest den + * Quelltext und besteht darauf, dass die Umgehungen aufgezaehlt bleiben. + * + * Ein Integrationstest waere schoener, braeuchte aber eine Sitzung mit + * halbfertigem zweitem Faktor - also genau den Zustand, den die Anwendung + * nicht mehr herstellen laesst. Diese Pruefung greift dafuer bei jedem + * neuen Endpunkt, auch bei dem, an den beim Testeschreiben niemand denkt. + */ + +const WURZEL = join(__dirname, '..', 'src') + +/** + * Wer an aktuellerBenutzer() oder erfordereBenutzerOhneZweitenFaktor() + * vorbei darf - und warum. + * + * Gemeinsam ist allen: Sie geben keine Daten einer betreuten Person heraus. + * Wer hier etwas ergaenzt, hebelt die MFA-Pflicht fuer diesen Pfad aus und + * schuldet eine Begruendung. + */ +const ERLAUBT = new Map([ + ['lib/auth/sitzung.ts', 'definiert die Funktionen'], + ['app/(auth)/anmelden/page.tsx', 'leitet bereits Angemeldete weiter, zeigt nichts an'], + ['app/(auth)/ersteinrichtung/page.tsx', 'laeuft, bevor es einen zweiten Faktor geben kann'], + ['app/(auth)/zwei-faktor/einrichten/page.tsx', 'ist die Einrichtung selbst'], + ['server/actions/zwei-faktor.ts', 'Vorbereiten und Bestaetigen der Einrichtung'], +]) + +/** Route Handler, die absichtlich ohne Anmeldung erreichbar sind. */ +const OEFFENTLICHE_ROUTEN = new Map([ + ['app/api/auth/[...nextauth]/route.ts', 'die Anmeldung selbst'], + ['app/api/gesundheit/route.ts', 'Statusabfrage fuer die Ueberwachung, ohne Daten'], + ['app/api/kalender/[signatur]/route.ts', 'signierter Abo-Link, eigene Pruefung'], + ['app/api/wartung/route.ts', 'Aufruf per Geheimnis aus dem Systemdienst'], +]) + +/** Alles, was einen angemeldeten Benutzer mit fertigem zweitem Faktor sichert. */ +const WAECHTER = [ + 'benutzerFuerRoute', + 'erfordereZugang', + 'erfordereBenutzer(', + 'finanzKontext', + 'berichtsKontext', +] + +function dateienUnter(verzeichnis: string, endungen: string[]): string[] { + const gefunden: string[] = [] + + for (const eintrag of readdirSync(verzeichnis)) { + if (eintrag === 'node_modules' || eintrag === '.next') continue + const pfad = join(verzeichnis, eintrag) + + if (statSync(pfad).isDirectory()) { + gefunden.push(...dateienUnter(pfad, endungen)) + } else if (endungen.some((endung) => eintrag.endsWith(endung))) { + gefunden.push(pfad) + } + } + + return gefunden +} + +/** Pfad relativ zu src/, immer mit Schraegstrich - auch unter Windows. */ +function kennung(pfad: string): string { + return relative(WURZEL, pfad).split(sep).join('/') +} + +describe('MFA-Grenze', () => { + it('umgeht die Pflicht nur an den aufgezaehlten Stellen', () => { + const umgehungen = dateienUnter(WURZEL, ['.ts', '.tsx']) + .filter((pfad) => { + const quelltext = readFileSync(pfad, 'utf-8') + return ( + quelltext.includes('aktuellerBenutzer') || + quelltext.includes('erfordereBenutzerOhneZweitenFaktor') + ) + }) + .map(kennung) + .sort() + + expect(umgehungen).toEqual([...ERLAUBT.keys()].sort()) + }) + + it('sichert jeden Route Handler oder erklaert, warum nicht', () => { + const ungesichert = dateienUnter(join(WURZEL, 'app'), ['route.ts']) + .map(kennung) + .filter((datei) => !OEFFENTLICHE_ROUTEN.has(datei)) + .filter((datei) => { + const quelltext = readFileSync(join(WURZEL, datei), 'utf-8') + return !WAECHTER.some((waechter) => quelltext.includes(waechter)) + }) + + expect(ungesichert).toEqual([]) + }) + + it('weist einen Abruf ohne zweiten Faktor ab, statt ihn umzuleiten', () => { + // Ein Redirect auf die Anmeldeseite waere bei einer Datei die falsche + // Antwort: Der Aufrufer bekaeme HTML, wo er ein Dokument erwartet, und + // ein Skript wertete das womoeglich als Erfolg. + const quelltext = readFileSync(join(WURZEL, 'lib', 'auth', 'sitzung.ts'), 'utf-8') + const abschnitt = quelltext.slice(quelltext.indexOf('export async function benutzerFuerRoute')) + + expect(abschnitt).toContain('status: 401') + expect(abschnitt).toContain('status: 403') + }) + + it('prueft den zweiten Faktor am Datenbankstand, nicht am Sitzungstoken', () => { + const quelltext = readFileSync(join(WURZEL, 'lib', 'auth', 'sitzung.ts'), 'utf-8') + const funktion = quelltext.slice( + quelltext.indexOf('export function zweiterFaktorFertig'), + quelltext.indexOf('export async function erfordereBenutzer'), + ) + + // Die Felder kommen aus dem User-Datensatz. Stuende hier etwas aus der + // Sitzung, liesse es sich ueber session.update() vom Client setzen. + expect(funktion).toContain('benutzer.totpAktiviertAm') + expect(funktion).toContain('benutzer.totpSecretVerschluesselt') + expect(funktion).not.toContain('sitzung') + }) +}) diff --git a/tests/pdf-bombe.test.ts b/tests/pdf-bombe.test.ts new file mode 100644 index 0000000..b053614 --- /dev/null +++ b/tests/pdf-bombe.test.ts @@ -0,0 +1,104 @@ +import { deflateSync } from 'node:zlib' +import { describe, expect, it } from 'vitest' +import { pdfTextLesen } from '@/lib/ocr/pdf-text' + +/** + * Was passiert, wenn jemand ein praepariertes PDF in die Akte legt? + * + * Ein Flate-Strom sagt nicht, wie gross er ausgepackt wird. Ein paar + * Kilobyte Nullen komprimieren auf fast nichts und blaehen sich beim + * Auspacken auf Gigabyte auf - die klassische Dekompressionsbombe. Der + * Textleser packte vorher ohne Obergrenze aus. + * + * Der Test rechnet nicht mit Gigabyte - das waere in einer Testsuite + * unhoeflich. Er baut eine Bombe, die die eingebaute Grenze knapp + * ueberschreitet, und besteht darauf, dass sie nicht ausgepackt wird. + */ + +/** Baut ein minimales PDF mit einem einzigen Flate-Strom. */ +function pdfMitStrom(nutzlast: Buffer, filter = '/FlateDecode'): Buffer { + const kopf = Buffer.from( + `%PDF-1.4\n1 0 obj\n<< /Length ${nutzlast.length} /Filter ${filter} >>\nstream\n`, + 'latin1', + ) + const fuss = Buffer.from('\nendstream\nendobj\n%%EOF\n', 'latin1') + return Buffer.concat([kopf, nutzlast, fuss]) +} + +/** + * Text, den der Extraktor tatsaechlich findet: Er springt nur an, wenn im + * Strom PDF-Textoperatoren stehen. + */ +function inhaltsstrom(text: string): Buffer { + return Buffer.from(`BT /F1 12 Tf (${text}) Tj ET\n`, 'latin1') +} + +describe('PDF-Textleser gegen praeparierte Dateien', () => { + it('liest einen gewoehnlichen Flate-Strom', () => { + const pdf = pdfMitStrom(deflateSync(inhaltsstrom('Bescheid vom Sozialamt'))) + + expect(pdfTextLesen(pdf)).toContain('Bescheid vom Sozialamt') + }) + + it('packt eine Bombe nicht aus, die die Stromgrenze ueberschreitet', () => { + // Die Nutzlast traegt eine erkennbare Marke UND 9 MB Fuellung. Beides + // ist wichtig: Ohne die Marke waere das Ergebnis auch ohne Grenze leer, + // und der Test bewiese nichts. Die Fuellung komprimiert auf wenige + // Kilobyte, die Grenze je Strom liegt bei 8 MB - zlib bricht ab, der + // Strom wird verworfen, und die Marke erscheint eben nicht. + const bombe = deflateSync( + Buffer.concat([inhaltsstrom('BOMBE-MARKE'), Buffer.alloc(9 * 1024 * 1024, 0x20)]), + ) + const pdf = pdfMitStrom(bombe) + + // Kein Absturz, kein Aufblaehen: einfach kein Text. + expect(pdfTextLesen(pdf)).toBe('') + }) + + it('liest denselben Strom, wenn er unter der Grenze bleibt', () => { + // Gegenprobe zum Test darueber: Dieselbe Bauform, nur 4 MB Fuellung. + // Damit ist belegt, dass oben die Grenze greift und nicht die Bauform + // des Testfalls. + const knappDrunter = deflateSync( + Buffer.concat([inhaltsstrom('BOMBE-MARKE'), Buffer.alloc(4 * 1024 * 1024, 0x20)]), + ) + + expect(pdfTextLesen(pdfMitStrom(knappDrunter))).toContain('BOMBE-MARKE') + }) + + it('bleibt handhabbar, wenn viele mittelgrosse Stroeme zusammenkommen', () => { + // Zwoelf Stroeme mit je 6 MB Nutzlast liegen einzeln unter der + // Stromgrenze, zusammen aber ueber der Gesamtgrenze von 64 MB. Ohne den + // zweiten Deckel waeren das 72 MB im Speicher, und zwar je Dokument. + const einer = deflateSync(Buffer.concat([inhaltsstrom('Seite'), Buffer.alloc(6 * 1024 * 1024, 0x20)])) + const teile: Buffer[] = [Buffer.from('%PDF-1.4\n', 'latin1')] + for (let i = 0; i < 12; i += 1) { + teile.push( + Buffer.from(`${i + 1} 0 obj\n<< /Length ${einer.length} /Filter /FlateDecode >>\nstream\n`, 'latin1'), + einer, + Buffer.from('\nendstream\nendobj\n', 'latin1'), + ) + } + teile.push(Buffer.from('%%EOF\n', 'latin1')) + + const text = pdfTextLesen(Buffer.concat(teile)) + + // Etwas wird gelesen - aber nicht alle zwoelf Stroeme. Waere der + // Gesamtdeckel weg, stuende "Seite" zwoelfmal darin. + const treffer = text.split('Seite').length - 1 + expect(treffer).toBeGreaterThan(0) + expect(treffer).toBeLessThan(12) + }) + + it('kommt mit einem kaputten Strom zurecht, ohne zu werfen', () => { + const pdf = pdfMitStrom(Buffer.from('das ist kein zlib-strom', 'latin1')) + + expect(() => pdfTextLesen(pdf)).not.toThrow() + }) + + it('ignoriert Bilddaten, statt sie auszupacken zu versuchen', () => { + const pdf = pdfMitStrom(deflateSync(inhaltsstrom('unsichtbar')), '/DCTDecode') + + expect(pdfTextLesen(pdf)).toBe('') + }) +}) diff --git a/tests/speicher-umschlag.test.ts b/tests/speicher-umschlag.test.ts new file mode 100644 index 0000000..d4eaf29 --- /dev/null +++ b/tests/speicher-umschlag.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from 'vitest' +import { + entschluesselnBinaer, + istVerschluesselt, + KOPF_LAENGE, + verschluesselnBinaer, +} from '@/lib/speicher/umschlag' + +/** + * Der Umschlag um abgelegte Dateien. + * + * Der Dateitreiber hat ihn von Anfang an benutzt, der Objektspeicher nicht: + * Dort stand nur ServerSideEncryption: 'AES256' - Verschluesselung mit dem + * Schluessel des Anbieters, der beim Ausliefern selbst entschluesselt. + * Gegen eine verlorene Platte im Rechenzentrum hilft das, gegen den + * Anbieter und gegen gestohlene Zugangsdaten zum Eimer nicht. + * + * Seit v1.11.0 liegt derselbe Umschlag um beides. Diese Tests halten die + * drei Eigenschaften fest, auf die es dabei ankommt: Er geht wieder auf, + * er merkt Veraenderungen, und er bindet den Inhalt an seinen Platz. + */ + +const INHALT = Buffer.from('Bescheid des Sozialamts vom 3. Maerz, Seite 1 von 2.', 'utf8') +const ORT = 'betreute/abc123/dokumente/eins.pdf' + +describe('Umschlag um abgelegte Dateien', () => { + it('geht wieder auf', () => { + const zu = verschluesselnBinaer(INHALT, ORT) + + expect(entschluesselnBinaer(zu, ORT).equals(INHALT)).toBe(true) + }) + + it('sieht dem Geheimtext den Klartext nicht an', () => { + const zu = verschluesselnBinaer(INHALT, ORT) + + expect(zu.includes(Buffer.from('Sozialamt', 'utf8'))).toBe(false) + expect(istVerschluesselt(zu)).toBe(true) + expect(zu.length).toBe(KOPF_LAENGE + INHALT.length) + }) + + it('erzeugt bei gleichem Inhalt zwei verschiedene Geheimtexte', () => { + // Sonst liesse sich an zwei gleich aussehenden Objekten ablesen, dass + // dasselbe Dokument zweimal in der Akte liegt. + const a = verschluesselnBinaer(INHALT, ORT) + const b = verschluesselnBinaer(INHALT, ORT) + + expect(a.equals(b)).toBe(false) + expect(entschluesselnBinaer(a, ORT).equals(entschluesselnBinaer(b, ORT))).toBe(true) + }) + + it('merkt, wenn am Geheimtext gedreht wurde', () => { + const zu = verschluesselnBinaer(INHALT, ORT) + const letzte = zu.length - 1 + zu.writeUInt8(zu.readUInt8(letzte) ^ 0x01, letzte) + + expect(() => entschluesselnBinaer(zu, ORT)).toThrow() + }) + + it('merkt, wenn am Kopf gedreht wurde', () => { + const zu = verschluesselnBinaer(INHALT, ORT) + // Ein Bit im IV. + zu.writeUInt8(zu.readUInt8(8) ^ 0x01, 8) + + expect(() => entschluesselnBinaer(zu, ORT)).toThrow() + }) + + describe('Bindung an den Ablageort', () => { + it('laesst sich nicht unter einem anderen Namen auspacken', () => { + // Der Angriff dahinter: Wer Schreibrechte auf den Eimer hat, legt die + // Bytes von Dokument A unter dem Namen von Dokument B ab. Ohne die + // Bindung lieferte die Anwendung anschliessend den falschen Bescheid + // aus - und zwar mit gueltiger Pruefsumme fuer den falschen Inhalt. + const zu = verschluesselnBinaer(INHALT, ORT) + + expect(() => + entschluesselnBinaer(zu, 'betreute/abc123/dokumente/zwei.pdf'), + ).toThrow() + }) + + it('laesst sich nicht in eine fremde Akte verschieben', () => { + const zu = verschluesselnBinaer(INHALT, ORT) + + expect(() => entschluesselnBinaer(zu, 'betreute/xyz789/dokumente/eins.pdf')).toThrow() + }) + }) + + describe('Rueckwaertsvertraeglichkeit', () => { + it('reicht Inhalte ohne Umschlag unveraendert durch', () => { + // Genau der Zustand, in dem die Objekte im Speicher liegen, bis der + // Migrationslauf sie umgeschrieben hat. Wuerde hier geworfen, waere + // nach dem Update kein einziges altes Dokument mehr abrufbar. + const roh = Buffer.from('%PDF-1.4 ganz gewoehnlich', 'utf8') + + expect(istVerschluesselt(roh)).toBe(false) + expect(entschluesselnBinaer(roh, ORT).equals(roh)).toBe(true) + }) + + it('haelt auch etwas fuer unverpackt, das zufaellig kurz ist', () => { + const winzig = Buffer.from('abc', 'utf8') + + expect(istVerschluesselt(winzig)).toBe(false) + expect(entschluesselnBinaer(winzig, ORT).equals(winzig)).toBe(true) + }) + + it('oeffnet einen Umschlag, der noch ohne Ortsbindung geschrieben wurde', () => { + // So hat der Dateitreiber bis v1.10.2 geschrieben. Diese Dateien + // muessen weiter aufgehen, sonst waere die Verbesserung ein + // Datenverlust. + const alt = verschluesselnBinaer(INHALT) + + expect(entschluesselnBinaer(alt, ORT).equals(INHALT)).toBe(true) + expect(entschluesselnBinaer(alt).equals(INHALT)).toBe(true) + }) + }) + + it('kommt mit einer leeren Datei zurecht', () => { + const leer = Buffer.alloc(0) + const zu = verschluesselnBinaer(leer, ORT) + + expect(entschluesselnBinaer(zu, ORT).length).toBe(0) + }) + + it('kommt mit einer groesseren Datei zurecht', () => { + const gross = Buffer.alloc(3 * 1024 * 1024, 0x41) + const zu = verschluesselnBinaer(gross, ORT) + + expect(entschluesselnBinaer(zu, ORT).equals(gross)).toBe(true) + }) +}) diff --git a/tests/totp-wiederverwendung.test.ts b/tests/totp-wiederverwendung.test.ts new file mode 100644 index 0000000..4603069 --- /dev/null +++ b/tests/totp-wiederverwendung.test.ts @@ -0,0 +1,190 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { User } from '@prisma/client' + +/** + * Ein TOTP-Code gilt genau einmal. + * + * Ein Code ist 30 Sekunden gueltig, mit der Toleranz von einem Fenster bis + * zu 90. In dieser Zeit liess er sich beliebig oft einloesen: abgelesen + * ueber die Schulter, mitgeschrieben von einem Zwischenglied, oder aus einem + * Formular im Browserverlauf. Der Kommentar in totp.ts versprach von Anfang + * an, der Aufrufer speichere das getroffene Fenster - getan hat es niemand. + * + * Geprueft wird gegen eine Attrappe der Datenbank, die das WHERE von + * updateMany nachbildet. Genau darauf beruht die Einmalverwendung: Die + * Bedingung steht in der Abfrage, nicht in einem if davor. + */ + +let benutzer: User +let updateAufrufe = 0 + +function passtSchritt(bedingung: Record, wert: number | null): boolean { + if ('OR' in bedingung) { + const zweige = bedingung.OR as Record[] + return zweige.some((zweig) => passtSchritt(zweig, wert)) + } + if ('totpLetzterSchritt' in bedingung) { + const erwartet = bedingung.totpLetzterSchritt + if (erwartet === null) return wert === null + if (typeof erwartet === 'object' && erwartet !== null && 'lt' in erwartet) { + return wert !== null && wert < (erwartet as { lt: number }).lt + } + } + return true +} + +vi.mock('@/lib/db', () => ({ + db: { + user: { + updateMany: async ({ + where, + data, + }: { + where: Record + data: Partial + }) => { + updateAufrufe += 1 + + if (where.id !== benutzer.id) return { count: 0 } + + // Die Liste der Wiederherstellungscodes muss noch die sein, die der + // Aufrufer gesehen hat - sonst war jemand schneller. + if ('wiederherstellungscodes' in where) { + const erwartet = (where.wiederherstellungscodes as { equals: string[] }).equals + const gleich = + erwartet.length === benutzer.wiederherstellungscodes.length && + erwartet.every((wert, i) => wert === benutzer.wiederherstellungscodes[i]) + if (!gleich) return { count: 0 } + } + + if (('OR' in where || 'totpLetzterSchritt' in where) && !passtSchritt(where, benutzer.totpLetzterSchritt)) { + return { count: 0 } + } + + benutzer = { ...benutzer, ...data } + return { count: 1 } + }, + }, + }, +})) + +const { zweitenFaktorEinloesen } = await import('@/lib/auth/zweiter-faktor') +const { totpAktuellerCode, totpSchritt, totpSecretErzeugen, totpSecretVerschluesseln } = + await import('@/lib/crypto/totp') +const { wiederherstellungscodeHashen, wiederherstellungscodesErzeugen } = await import( + '@/lib/crypto/hash' +) + +const EMAIL = 'betreuer@example.de' +const SECRET = totpSecretErzeugen() + +function grundstand(): User { + return { + id: 'benutzer-1', + email: EMAIL, + totpSecretVerschluesselt: totpSecretVerschluesseln(SECRET), + totpAktiviertAm: new Date('2026-01-01T00:00:00Z'), + totpLetzterSchritt: null, + wiederherstellungscodes: [], + } as unknown as User +} + +describe('Zweiten Faktor einloesen', () => { + beforeEach(() => { + benutzer = grundstand() + updateAufrufe = 0 + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-08-24T12:00:15Z')) + }) + + it('nimmt einen gueltigen Code an und haelt das Fenster fest', async () => { + const code = totpAktuellerCode(SECRET, EMAIL) + + const ergebnis = await zweitenFaktorEinloesen(benutzer, code) + + expect(ergebnis).toEqual({ ok: true, art: 'totp' }) + expect(benutzer.totpLetzterSchritt).toBe(totpSchritt(0)) + }) + + it('lehnt denselben Code beim zweiten Mal ab', async () => { + const code = totpAktuellerCode(SECRET, EMAIL) + + expect(await zweitenFaktorEinloesen(benutzer, code)).toEqual({ ok: true, art: 'totp' }) + + // Noch im selben Fenster - der Code waere rechnerisch weiter richtig. + vi.setSystemTime(new Date('2026-08-24T12:00:25Z')) + expect(await zweitenFaktorEinloesen(benutzer, code)).toEqual({ + ok: false, + grund: 'wiederverwendet', + }) + }) + + it('lehnt ihn auch im Toleranzfenster danach ab', async () => { + const code = totpAktuellerCode(SECRET, EMAIL) + await zweitenFaktorEinloesen(benutzer, code) + + // Ein Fenster weiter: Die Toleranz liesse den alten Code noch gelten. + vi.setSystemTime(new Date('2026-08-24T12:00:45Z')) + expect(await zweitenFaktorEinloesen(benutzer, code)).toEqual({ + ok: false, + grund: 'wiederverwendet', + }) + }) + + it('nimmt den naechsten Code an', async () => { + await zweitenFaktorEinloesen(benutzer, totpAktuellerCode(SECRET, EMAIL)) + const vorher = benutzer.totpLetzterSchritt + + vi.setSystemTime(new Date('2026-08-24T12:00:45Z')) + const naechster = totpAktuellerCode(SECRET, EMAIL) + expect(await zweitenFaktorEinloesen(benutzer, naechster)).toEqual({ ok: true, art: 'totp' }) + expect(benutzer.totpLetzterSchritt!).toBeGreaterThan(vorher!) + }) + + it('lehnt einen falschen Code ab, ohne zu schreiben', async () => { + expect(await zweitenFaktorEinloesen(benutzer, '000000')).toEqual({ + ok: false, + grund: 'falsch', + }) + expect(updateAufrufe).toBe(0) + expect(benutzer.totpLetzterSchritt).toBeNull() + }) + + it('toleriert Leerzeichen und Bindestriche in der Eingabe', async () => { + const code = totpAktuellerCode(SECRET, EMAIL) + const zerlegt = `${code.slice(0, 3)} ${code.slice(3)}` + + expect(await zweitenFaktorEinloesen(benutzer, zerlegt)).toEqual({ ok: true, art: 'totp' }) + }) + + it('loest einen Wiederherstellungscode genau einmal ein', async () => { + const codes = wiederherstellungscodesErzeugen(3) + benutzer = { + ...grundstand(), + wiederherstellungscodes: await Promise.all(codes.map(wiederherstellungscodeHashen)), + } as User + + const erster = codes[0]! + expect(await zweitenFaktorEinloesen(benutzer, erster)).toEqual({ + ok: true, + art: 'wiederherstellungscode', + }) + expect(benutzer.wiederherstellungscodes).toHaveLength(2) + + // Derselbe Code noch einmal: Er steht nicht mehr in der Liste. + expect(await zweitenFaktorEinloesen(benutzer, erster)).toEqual({ + ok: false, + grund: 'falsch', + }) + expect(benutzer.wiederherstellungscodes).toHaveLength(2) + }) + + it('haelt die Reihenfolge der Zeitschritte ein', () => { + const frueh = totpSchritt(0, Date.parse('2026-08-24T12:00:00Z')) + const spaet = totpSchritt(0, Date.parse('2026-08-24T12:00:30Z')) + + expect(spaet).toBe(frueh + 1) + // Das Delta verschiebt um ganze Fenster, nicht um Sekunden. + expect(totpSchritt(1, Date.parse('2026-08-24T12:00:00Z'))).toBe(spaet) + }) +})