From 2c1b8f4c28c0dd541a9afce248225ef633ce3288 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 02:12:37 +0200 Subject: [PATCH 01/10] Abhaengigkeiten auf Fassungen ohne bekannte Luecken heben npm audit meldete 32 Schwachstellen, davon 5 kritische. Der Vorschlag des Werkzeugs war ein Sprung auf Next.js 16 - noetig war er nicht: next, next-auth und @auth/core waren nur durchgereicht markiert, ein eigenes Advisory hatten allein nodemailer, postcss und sharp. Die drei sind Blattpakete und lassen sich gezielt ueberschreiben. Nodemailer 9 kollidiert mit einer optionalen Peer-Angabe von next-auth (hoechstens 8). Das ist folgenlos, weil der E-Mail-Provider von next-auth hier nicht benutzt wird, sondern Nodemailer direkt. Danach: 0 Schwachstellen. Refs #38 --- package-lock.json | 3832 ++++++++++++++++++--------------------------- package.json | 29 +- 2 files changed, 1579 insertions(+), 2282 deletions(-) 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..bdff040 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,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 +54,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 +69,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 +99,9 @@ "next-auth": { "@simplewebauthn/server": "$@simplewebauthn/server", "@simplewebauthn/browser": "$@simplewebauthn/browser" - } + }, + "nodemailer": "9.0.5", + "postcss": "8.5.26", + "sharp": "0.35.3" } } From 2bed78915f1a71fcb38b5362990a124b0090495f Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 02:12:50 +0200 Subject: [PATCH 02/10] Zweiten Faktor zur Grenze machen, nicht nur zur Anzeige "Der zweite Faktor ist Pflicht" stand im Kopf von 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 an die Daten. Die Pruefung steckt jetzt in erfordereBenutzer() selbst und damit ueber erfordereZugang() automatisch in jeder Server Action. Massgeblich ist der Datenbankstand, nicht das Feld im Sitzungstoken - das laesst sich ueber session.update() vom Client beeinflussen. Fuer Route Handler gibt es benutzerFuerRoute(): 401 bzw. 403 statt einer Umleitung. Eine Anmeldeseite als Inhalt einer Word-Datei waere die falsche Antwort. Dabei aufgefallen: Der Dokumentenabruf unter /api/dokumente/datei fragte nur nach der Anmeldung. Auch geschlossen. Ausnahmen sind aufgezaehlt und werden von tests/mfa-grenze.test.ts festgehalten - der Test liest den Quelltext und schlaegt an, sobald eine weitere dazukommt. Refs #38 --- src/app/(app)/berichte/export/datei/route.ts | 8 +- .../api/dokumente/datei/[signatur]/route.ts | 18 ++- src/lib/auth/sitzung.ts | 72 +++++++++- src/server/actions/zwei-faktor.ts | 13 +- tests/mfa-grenze.test.ts | 133 ++++++++++++++++++ 5 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 tests/mfa-grenze.test.ts 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/api/dokumente/datei/[signatur]/route.ts b/src/app/api/dokumente/datei/[signatur]/route.ts index a4b5db4..cb3e3f3 100644 --- a/src/app/api/dokumente/datei/[signatur]/route.ts +++ b/src/app/api/dokumente/datei/[signatur]/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server' import type { Modul } from '@prisma/client' import { protokollieren } from '@/lib/audit' import { darf } from '@/lib/auth/rollen' -import { aktuellerBenutzer } from '@/lib/auth/sitzung' +import { benutzerFuerRoute } from '@/lib/auth/sitzung' import { db, nichtGeloescht } from '@/lib/db' import { typErkennen } from '@/lib/speicher/dateityp' import { betreuteIdAusSchluessel, lesen } from '@/lib/speicher' @@ -55,7 +55,10 @@ function nurText(status: number, meldung: string): NextResponse { export async function GET( _anfrage: Request, { params }: { params: Promise<{ signatur: string }> }, -): 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/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/server/actions/zwei-faktor.ts b/src/server/actions/zwei-faktor.ts index 7a5fae3..1b4854b 100644 --- a/src/server/actions/zwei-faktor.ts +++ b/src/server/actions/zwei-faktor.ts @@ -3,7 +3,11 @@ 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, @@ -40,7 +44,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 +84,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.' } 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') + }) +}) From 133b0314c566e5178eaa36dd83a93181b8cd33de Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 02:13:04 +0200 Subject: [PATCH 03/10] Anmeldesperre gegen Selbstverlaengerung und Schreiblast sichern Die Sperre hielt sich selbst am Leben. Jeder Aufruf wurde festgehalten, auch der, der wegen der laufenden Sperre gar nicht geprueft wurde; die Frist lief zugleich ab dem juengsten Fehlversuch. Wer also alle zehn Minuten ein falsches Passwort an eine fremde Adresse schickte, sperrte deren Inhaber dauerhaft aus - ohne das Passwort zu kennen. Jeder dieser Aufrufe hinterliess ausserdem eine Zeile, die erst der Wartungslauf nach 30 Tagen wieder los wurde. Jetzt zwei getrennte Dinge: Ein geprueft fehlgeschlagener Versuch zaehlt und wird einzeln festgehalten. Ein waehrend der Sperre abgewiesener Aufruf zaehlt nicht und wird hoechstens einmal je Sperre vermerkt. Die Auswertung geht die Versuche von alt nach neu durch und zieht die Runden nach: Ist das Mass voll, beginnt an diesem Versuch eine Sperre und der Zaehler faengt von vorn an. Damit laeuft die Frist auch dann ab, wenn die Fehlversuche noch im Zeitfenster stehen - vorher blieb der Zugang bis zum Fensterende dicht. Die Tests sind gegen vier Mutationen geprueft; jede wird rot. Refs #38 --- src/auth.ts | 5 +- src/lib/auth/ratelimit.ts | 96 ++++++++++++-- tests/anmeldesperre.test.ts | 250 ++++++++++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+), 16 deletions(-) create mode 100644 tests/anmeldesperre.test.ts diff --git a/src/auth.ts b/src/auth.ts index d70aecd..b3b5a0b 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -9,6 +9,7 @@ import { anmeldungErlaubt, aufMindestdauerWarten, fehlversucheZuruecksetzen, + sperreVermerken, versuchFesthalten, } from '@/lib/auth/ratelimit' import { protokollieren } from '@/lib/audit' @@ -110,7 +111,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) } @@ -274,7 +275,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/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/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) + }) +}) From 6bd8f7e8a4ddb2f37ebc4616cf0c771677fffca1 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 08:03:32 +0200 Subject: [PATCH 04/10] Schema: Zeitschritt fuer TOTP und Sperre fuer die Ersteinrichtung Zwei Felder fuer zwei Luecken, in einer Migration: benutzer.totpLetzterSchritt haelt das zuletzt akzeptierte TOTP-Fenster fest. Ohne das gilt ein Code bis zu 90 Sekunden lang beliebig oft. Die Tabelle ersteinrichtung hat genau eine Zeile mit fester Id. Das ist kein Protokoll, sondern eine Sperre: Bisher entschied "gibt es schon einen Benutzer?" und danach wurde angelegt - zwei Anfragen gleichzeitig kamen beide durch. Jetzt entscheidet der Primaerschluessel. Enthalten ist eine Datenmigration: Wo schon ein Zugang besteht, wird die Sperrzeile gesetzt. Ohne sie stuende die Einrichtungsseite auf jedem bestehenden System nach dem Update wieder offen. Die DROP-INDEX-Zeilen zu den Trigramm-Indizes hat das Skript aus #36 wieder entfernt; "npm run db:pruefen" bestaetigt, dass beide stehen. Refs #38 --- .../migration.sql | 31 +++++++++++++++++++ prisma/schema.prisma | 26 ++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 prisma/migrations/20260824074643_sicherheitsaudit/migration.sql 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 { From 609aa5d27d039cd4a91398946ca49b8e2639bdf5 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 08:03:33 +0200 Subject: [PATCH 05/10] Anmeldung haerten: Einmalcodes, Anschluss, Host, Passkey-Vorzimmer Vier Stellen, an denen eine Zusage nicht gehalten wurde. 1. Ein TOTP-Code galt mehrfach. Der Kommentar in totp.ts versprach seit dem Anfang, der Aufrufer speichere das getroffene Zeitfenster - getan hat es niemand. Damit liess sich ein abgelesener Code innerhalb von 90 Sekunden noch einmal einloesen. Jetzt geht jede Pruefung durch zweitenFaktorEinloesen(), und die Bedingung steht im WHERE von updateMany: Zwei gleichzeitige Anmeldungen mit demselben Code treffen nicht beide eine Zeile. 2. Der Anschluss kam aus dem ERSTEN Eintrag von "x-forwarded-for". Das ist der Teil, den der Aufrufer selbst schreibt. Wer bei jeder Anfrage eine andere Zahl hineinschrieb, sah fuer die Anmeldesperre wie tausend Anschluesse aus - die Zaehlung war nicht nur wirkungslos, sondern irrefuehrend. Jetzt von hinten gelesen, und nur wenn VERTRAUE_PROXY gesetzt ist. Ohne Proxy ist der Kopf fremdbestimmt; dann lieber keine Zaehlung nach Anschluss als eine, die eine Kopfzeile aushebelt. Beim ersten Mal steht eine Zeile im Log, damit das Fehlen auffaellt. 3. trustHost stand fest auf true. Ist AUTH_URL gesetzt - der dokumentierte Betrieb setzt es, Passkeys brauchen es -, gibt es keinen Grund, die Adresse aus einer Kopfzeile von aussen abzuleiten. AUTH_TRUST_HOST bleibt wirksam, damit die dokumentierte Einstellung nicht stillschweigend uebergangen wird. 4. Passkeys liessen sich ohne erneute Bestaetigung anlegen und entfernen. Ein Passkey ist ein vollwertiger Zugang, der einen Passwortwechsel ueberlebt - an einem unbeaufsichtigten Rechner waren das zwei Klicks. Jetzt dasselbe Vorzimmer wie beim Vollexport. Dabei musste alsFehler() lernen, die Weiterleitung von Next durchzulassen: Sonst verschwand die Zwischenfrage in "Der Passkey konnte nicht geprueft werden". Refs #38 --- .env.example | 31 ++++- src/auth.ts | 47 +++---- src/lib/auth/anschluss.ts | 92 ++++++++++++++ src/lib/auth/zweiter-faktor.ts | 93 ++++++++++++++ src/lib/crypto/totp.ts | 21 ++- src/lib/env.ts | 71 +++++++++++ src/server/actions/anmelden.ts | 6 +- src/server/actions/bestaetigen.ts | 12 +- src/server/actions/passkey.ts | 77 ++++++++--- src/server/actions/zwei-faktor.ts | 14 +- tests/anschluss.test.ts | 136 ++++++++++++++++++++ tests/totp-wiederverwendung.test.ts | 190 ++++++++++++++++++++++++++++ 12 files changed, 726 insertions(+), 64 deletions(-) create mode 100644 src/lib/auth/anschluss.ts create mode 100644 src/lib/auth/zweiter-faktor.ts create mode 100644 tests/anschluss.test.ts create mode 100644 tests/totp-wiederverwendung.test.ts 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/src/auth.ts b/src/auth.ts index b3b5a0b..8d72fa5 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,8 +3,7 @@ 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, @@ -13,6 +12,8 @@ import { 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' @@ -65,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: { @@ -142,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', @@ -176,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') 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/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 { 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/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 1b4854b..7cf0856 100644 --- a/src/server/actions/zwei-faktor.ts +++ b/src/server/actions/zwei-faktor.ts @@ -11,6 +11,7 @@ import { import { protokollieren } from '@/lib/audit' import { totpPruefen, + totpSchritt, totpQrCode, totpSecretEntschluesseln, totpSecretErzeugen, @@ -100,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 — ' + @@ -115,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/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/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) + }) +}) From 5b54d7bc5f3f5b2503a83254f2dccbfe8aa49dc9 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 08:03:49 +0200 Subject: [PATCH 06/10] Ersteinrichtung schliessen: Geheimnis und eine Sperre statt zweier Schritte Die Aktion ist die einzige Stelle der Anwendung, an der jemand ohne Anmeldung schreiben darf. Zwei Dinge fehlten. Erstens stand die Seite zwischen dem ersten Start und dem Anlegen des Zugangs fuer jeden 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; verglichen wird ueber Hashes in gleichbleibender Zeit, und ein falscher Wert antwortet mit derselben Mindestdauer wie das Anmeldeformular. Fehlt der Wert, ist die Einrichtung gesperrt statt offen. Das ist Absicht: Eine Installation, bei der die Zeile vergessen wurde, soll eine Meldung zeigen und nicht einrichtbar sein. Zweitens waren Pruefung und Anlegen zwei Schritte. Beide laufen jetzt in einer Transaktion, und die Sperrzeile mit fester Id entscheidet - eine zweite gleichzeitige Anfrage scheitert am Primaerschluessel, bevor ein zweiter Administrator entsteht. Die Seite liest die Sperrzeile mit: Lag der erste Zugang im Papierkorb, stand Schritt 1 sonst wieder offen. Der Seed setzt sie ebenfalls. Refs #38 --- docs/INSTALLATION.md | 21 +++- prisma/seed.ts | 8 ++ scripts/schluessel-erzeugen.mjs | 1 + src/app/(auth)/ersteinrichtung/page.tsx | 10 +- .../ersteinrichtung/zugang-formular.tsx | 17 +++ src/server/actions/ersteinrichtung.ts | 119 +++++++++++++++--- 6 files changed, 153 insertions(+), 23 deletions(-) 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/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/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() { /> + + + + > + 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, }) From 676fbb8ee4592492afb928b7363f2cc634388dd1 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 09:41:35 +0200 Subject: [PATCH 07/10] Texterkennung in Grenzen zwingen, die das Dokument nicht selbst setzt Zwei Wege, auf denen ein hochgeladenes Dokument den Server umbringen konnte. Beide haben gemeinsam, dass die Groesse aus der Datei kam. 1. Dekompressionsbombe. Die PDF-Stroeme wurden mit inflateSync ohne maxOutputLength ausgepackt. Ein Flate-Strom sagt nicht, wie gross er ausgepackt wird - ein paar Kilobyte Fuellung blaehen sich auf Gigabyte auf. Jetzt ein Deckel je Strom (8 MB) und einer ueber die Summe (64 MB); zlib bricht selbst ab, der Strom wird verworfen, der Text ist eben unvollstaendig. 2. Riesenseiten. "pdftoppm -r 300" rendert mit fester Aufloesung, aber die Seitengroesse steht im PDF. Bei 200 x 200 Zoll ergeben 300 dpi ein Bild von 60000 x 60000 Pixeln, und zwar je Seite. Jetzt "-scale-to" statt fester dpi: Die laengste Kante bekommt eine feste Pixelzahl, unabhaengig davon, was im Dokument als Papierformat steht. Fuer A4 entspricht das etwa den bisherigen 300 dpi. Dazu ein Deckel auf die Gesamtgroesse der gerenderten Dateien. Der erste Bombentest war wertlos: Die Nutzlast enthielt keine PDF-Textoperatoren, also war das Ergebnis mit und ohne Grenze leer. Mit einer Marke in der Nutzlast und einer Gegenprobe knapp unter der Grenze schlagen beide Mutationen an. Refs #38 --- src/lib/ocr/index.ts | 55 +++++++++++++++++++-- src/lib/ocr/pdf-text.ts | 62 +++++++++++++++++++++--- tests/pdf-bombe.test.ts | 104 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 12 deletions(-) create mode 100644 tests/pdf-bombe.test.ts diff --git a/src/lib/ocr/index.ts b/src/lib/ocr/index.ts index b9decfb..30a1c1f 100644 --- a/src/lib/ocr/index.ts +++ b/src/lib/ocr/index.ts @@ -1,6 +1,6 @@ import 'server-only' import { execFile } from 'node:child_process' -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' @@ -43,6 +43,26 @@ const MAX_SEITEN = 30 /** Mehr als das braucht keine Volltextsuche - und die Spalte bleibt handhabbar. */ const MAX_ZEICHEN = 400_000 +/** + * Laengste Kante einer gerenderten Seite in Pixeln. + * + * Vorher wurde mit festen 300 dpi gerendert. Bei einer normalen A4-Seite + * sind das rund 2480 x 3510 Pixel - unproblematisch. Die Seitengroesse + * steht aber im PDF und kann beliebig gross angegeben werden: Bei einer + * Seite von 200 x 200 Zoll ergeben 300 dpi ein Bild von 60000 x 60000 + * Pixeln, also mehrere Gigabyte auf der Platte, und das je Seite. Ein + * Dokument, das jemand in die Akte legt, haette so den Server vollschreiben + * koennen. + * + * "-scale-to" haengt die Aufloesung an die Pixelzahl statt an die + * angegebene Papiergroesse. 3500 Pixel entsprechen bei A4 etwa 300 dpi - + * fuer den Regelfall bleibt also alles wie vorher. + */ +const MAX_KANTE_PIXEL = 3500 + +/** Wie viel die gerenderten Seiten zusammen belegen duerfen. */ +const MAX_BILDER_BYTES = 512 * 1024 * 1024 + // ----------------------------------------------------------------------------- // Verfuegbarkeit // ----------------------------------------------------------------------------- @@ -146,14 +166,39 @@ async function tesseractAufDatei(pfad: string): Promise<{ text: string; meldung? async function pdfInBilder(verzeichnis: string, pdfPfad: 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/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('') + }) +}) From 92ff8efe39514953cf025f98d2f610288315e4e8 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 09:50:41 +0200 Subject: [PATCH 08/10] Objektspeicher verschluesseln, statt es dem Anbieter zu ueberlassen Im S3-Treiber stand nur ServerSideEncryption: 'AES256'. Das ist Verschluesselung mit dem Schluessel des Anbieters - er entschluesselt beim Ausliefern selbst. Gegen eine verlorene Platte im Rechenzentrum hilft das, gegen den Anbieter und gegen gestohlene Zugangsdaten zum Eimer nicht. In diesem Eimer liegen Bescheide, Arztbriefe und Kontoauszuege. Der Dateitreiber hat es von Anfang an richtig gemacht. Der Umschlag zieht deshalb nach src/lib/speicher/umschlag.ts um und wird von beiden benutzt, statt ihn ein zweites Mal zu schreiben. Neu ist die Bindung an den Ablageort: Der Schluessel des Objekts wandert in die Zusatzdaten. Wer Schreibrechte auf den Eimer hat, kann die Bytes von Dokument A damit nicht unter dem Namen von Dokument B ablegen - beim Lesen schlaegt die Pruefung fehl, statt den falschen Bescheid mit gueltiger Pruefsumme auszuliefern. Damit fallen die presigned URLs weg. Sie liessen den Browser direkt beim Anbieter laden; was dort liegt, ist jetzt Geheimtext. Die Auslieferung geht ueber dieselbe eigene Route wie beim Dateitreiber - die prueft zusaetzlich die Berechtigung und schreibt einen Audit-Eintrag. Ein weitergeleiteter Link allein oeffnet also nichts mehr. Bestehende Dateien bleiben durchgehend lesbar: Der Treiber erkennt am Kopf, ob ein Umschlag da ist, und reicht alles andere unveraendert durch. Auch die alten Umschlaege ohne Ortsbindung gehen weiter auf - sonst waere die Verbesserung ein Datenverlust. Nachgezogen wird mit npm run speicher:verschluesseln # nur schauen npm run speicher:verschluesseln -- --anwenden Geprueft: 12 Tests gegen drei Mutationen, ein Trockenlauf ueber die 120 Dateien der Entwicklungsablage, und eine Probe mit einer Klartextdatei, die nach dem Lauf verschluesselt auf der Platte liegt und sich trotzdem korrekt zurueeklesen laesst. Refs #38 --- package.json | 1 + scripts/speicher-verschluesseln.mts | 184 ++++++++++++++++++++++++++++ src/lib/speicher/datei.ts | 57 +-------- src/lib/speicher/s3.ts | 114 +++++++++++++---- src/lib/speicher/umschlag.ts | 119 ++++++++++++++++++ tests/speicher-umschlag.test.ts | 129 +++++++++++++++++++ 6 files changed, 531 insertions(+), 73 deletions(-) create mode 100644 scripts/speicher-verschluesseln.mts create mode 100644 src/lib/speicher/umschlag.ts create mode 100644 tests/speicher-umschlag.test.ts diff --git a/package.json b/package.json index bdff040..5f0cf8d 100644 --- a/package.json +++ b/package.json @@ -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", 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/lib/speicher/datei.ts b/src/lib/speicher/datei.ts index bc4264b..db9cb16 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) } @@ -52,57 +47,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/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) + }) +}) From e48eda43322f5c6d3002e953f6f39718e267d4e2 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 09:53:43 +0200 Subject: [PATCH 09/10] Irrefuehrende Sicherheitszusagen entfernen, OWASP-Durchgang festhalten Zwei Kommentare versprachen Schutz, den es nicht gab. Das ist schlimmer als eine fehlende Zusage, weil man sich darauf verlaesst: - In middleware.ts stand "Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen". Die Middleware prueft keine Anmeldung, sie setzt Kopfzeilen; die Liste steuert allein den Cache-Control-Kopf. Wer den Satz glaubt, verzichtet an einer neuen Seite auf erfordereBenutzer(). Die Liste heisst jetzt OHNE_CACHE_SPERRE und sagt selbst, was sie ist. - In datei.ts hiess es, die Pfadpruefung fange "Symlink-Tricks" ab. resolve() rechnet mit Zeichenketten und folgt keinem Link - dafuer braeuchte es realpath(). Nachgeruestet wird es nicht: Wer dort einen Symlink anlegen kann, hat schon Schreibrechte im Datenverzeichnis. Der Satz musste trotzdem weg. Dazu docs/SICHERHEIT.md: Bedrohungsmodell, der Durchgang entlang der OWASP Top 10 - und ein eigener Abschnitt darueber, was ausdruecklich NICHT geschuetzt ist. Aus dem OWASP-Durchgang kamen keine weiteren Funde. Zwei Stellen sahen danach aus und waren es nicht: Prisma.join() in suche.ts fuegt nur bereits parametrisierte Fragmente zusammen, und der Suchausschnitt mit den -Markern aus ts_headline wird zerlegt und als React-Kinder gerendert, nicht als HTML - obwohl die Quelle der OCR-Text hochgeladener Dokumente ist. Refs #38 --- docs/SICHERHEIT.md | 168 ++++++++++++++++++++++++++++++++++++++ src/lib/speicher/datei.ts | 14 +++- src/middleware.ts | 19 ++++- 3 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 docs/SICHERHEIT.md 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/src/lib/speicher/datei.ts b/src/lib/speicher/datei.ts index db9cb16..415cdd3 100644 --- a/src/lib/speicher/datei.ts +++ b/src/lib/speicher/datei.ts @@ -34,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) 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') } From 406f7854b7b2376746cac5efefaa5277e2f69ac0 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 10:06:13 +0200 Subject: [PATCH 10/10] chore: Version 1.11.0 --- CHANGELOG.md | 97 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- src/lib/version.ts | 2 +- 3 files changed, 98 insertions(+), 3 deletions(-) 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/package.json b/package.json index 5f0cf8d..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": { 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: