From de1f92376724fdb23b2a9a986d1c240c72a84573 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 12:39:40 +0200 Subject: [PATCH 01/19] =?UTF-8?q?chore:=20Projektger=C3=BCst=20und=20Werkz?= =?UTF-8?q?eugkette=20einrichten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next 16 mit App Router und standalone-Ausgabe, React 19, TypeScript strict, Tailwind 4, Vitest und Playwright. Alle Versionen exakt gepinnt. Abweichungen vom ursprünglich notierten Stand, jeweils begründet: - Prisma 7.10.0 statt 6, weil 6 überholt ist und 8 nur als RC vorliegt. - TypeScript 6.0.3 statt 7, weil typescript-eslint 8 nur "<6.1.0" trägt. - ESLint 9.39.5 statt 10, weil das in eslint-config-next gebündelte eslint-plugin-react noch context.getFilename() benutzt. - npm-overrides für SimpleWebAuthn und nodemailer, weil next-auth dort ältere optionale Peers deklariert. Die betroffenen next-auth-Provider werden nicht benutzt: Passkeys laufen direkt über SimpleWebAuthn 14. "npm run pruefen" bündelt Prettier, ESLint, tsc, Vitest und ShellCheck. ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 12 + .editorconfig | 12 + .env.example | 62 + .gitattributes | 8 + .gitignore | 41 + .prettierignore | 10 + .prettierrc.json | 9 + docker-compose.yml | 65 + eslint.config.mjs | 57 + next.config.ts | 15 + package-lock.json | 12607 +++++++++++++++++++++++++++++++++++ package.json | 101 + postcss.config.mjs | 8 + scripts/pruefen.mjs | 145 + scripts/zeit.sh | 175 + test/aufbau-oberflaeche.ts | 8 + tsconfig.json | 38 + vitest.config.ts | 41 + 18 files changed, 13414 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 docker-compose.yml create mode 100644 eslint.config.mjs create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 scripts/pruefen.mjs create mode 100755 scripts/zeit.sh create mode 100644 test/aufbau-oberflaeche.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9561351 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.env +.env.local +logs +sicherungen +coverage +test-results +playwright-report +docs/bilder +devlog diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1014ba7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..279a421 --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# --------------------------------------------------------------------------- +# Planer – Beispielkonfiguration +# Kopieren nach .env und ausfuellen. .env gehoert NIE ins Git. +# --------------------------------------------------------------------------- + +# Welche Instanz laeuft hier? Steuert Provider, Integrationen und Routen. +# Erlaubt: firma | daheim +INSTANZ=daheim + +# Oeffentliche Adresse der Anwendung, ohne abschliessenden Schraegstrich. +APP_URL=http://localhost:3000 + +# --- Datenbank ------------------------------------------------------------- +# Muss zu den POSTGRES_*-Werten unten passen, wenn Docker Compose benutzt wird. +DATABASE_URL=postgresql://planer:bitte-aendern@localhost:5432/planer?schema=public + +# Nur fuer docker compose: Zugangsdaten des Postgres-Containers. +POSTGRES_USER=planer +POSTGRES_PASSWORD=bitte-aendern +POSTGRES_DB=planer + +# Nach aussen veroeffentlichte Ports. Aendern, wenn 5432 oder 3000 auf dem +# Rechner schon belegt sind; DATABASE_URL muss dann denselben Port nennen. +POSTGRES_PORT=5432 +APP_PORT=3000 + +# --- Anmeldung ------------------------------------------------------------- +# Signiert Sitzungen und Tokens. Erzeugen mit: +# openssl rand -base64 32 +AUTH_SECRET= + +# Hinter einem Reverse Proxy auf true setzen, damit Auth.js dem Host vertraut. +AUTH_TRUST_HOST=true + +# Anmeldung per E-Mail und Passwort erlauben. In der Firma ueblicherweise +# false, damit nur ueber OIDC angemeldet wird. +ANMELDUNG_PASSWORT=true + +# --- OIDC ------------------------------------------------------------------ +# firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab. +# Der Rueckleit-URI beim Anbieter lautet: +# /api/auth/callback/oidc +OIDC_NAME= +OIDC_ISSUER= +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= + +# --- Passkeys (WebAuthn) --------------------------------------------------- +# RP_ID ist die nackte Domain ohne Schema und Port, z. B. planer.example.de. +# Lokal ist "localhost" richtig. +WEBAUTHN_RP_ID=localhost +WEBAUTHN_RP_NAME=Planer +# Muss exakt der Herkunft im Browser entsprechen, inklusive Port. +WEBAUTHN_ORIGIN=http://localhost:3000 + +# --- Sonstiges ------------------------------------------------------------- +# Zeitzone fuer Anzeigen und Tagesgrenzen. +TZ=Europe/Berlin + +# Nur fuer die Screenshot-Instanz. Schaltet den Demo-Kalenderprovider frei +# und darf in echten Umgebungen niemals true sein. +DEMO_MODUS=false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..168ae29 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Im Git immer LF, damit die Shellskripte auch unter Linux laufen. +* text=auto eol=lf +*.sh text eol=lf +*.png binary +*.jpg binary +*.ico binary +*.woff2 binary +package-lock.json -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42e9a5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Abhängigkeiten +node_modules/ +.pnp +.pnp.js + +# Next.js +.next/ +out/ +next-env.d.ts + +# Erzeugter Prisma-Client +src/generated/ + +# Tests +coverage/ +test-results/ +playwright-report/ +blob-report/ +.playwright/ + +# Geheimnisse und lokale Umgebung +.env +.env.local +.env.*.local +!.env.example + +# Betrieb +logs/ +sicherungen/ +*.dump + +# Zeiterfassung: laufender Eintrag ist lokal +devlog/.zeit-offen + +# Werkzeuge +*.tsbuildinfo +.DS_Store +Thumbs.db +.vscode/* +!.vscode/extensions.json +.idea/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ebb889a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +.next +node_modules +package-lock.json +docs/bilder +prisma/migrations +logs +test-results +playwright-report +coverage +devlog/zeiten.csv diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..df47e06 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 90, + "tabWidth": 2, + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..34b0ecd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +services: + db: + image: postgres:16-alpine + container_name: planer-db + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-planer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env} + POSTGRES_DB: ${POSTGRES_DB:-planer} + TZ: ${TZ:-Europe/Berlin} + volumes: + - db-daten:/var/lib/postgresql/data + ports: + - '${POSTGRES_PORT:-5432}:5432' + healthcheck: + test: + ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-planer} -d ${POSTGRES_DB:-planer}'] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + + app: + build: + context: . + dockerfile: Dockerfile + container_name: planer-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: production + TZ: ${TZ:-Europe/Berlin} + INSTANZ: ${INSTANZ:?INSTANZ fehlt in .env} + APP_URL: ${APP_URL:?APP_URL fehlt in .env} + DATABASE_URL: postgresql://${POSTGRES_USER:-planer}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-planer}?schema=public + AUTH_SECRET: ${AUTH_SECRET:?AUTH_SECRET fehlt in .env} + AUTH_TRUST_HOST: ${AUTH_TRUST_HOST:-true} + ANMELDUNG_PASSWORT: ${ANMELDUNG_PASSWORT:-false} + OIDC_NAME: ${OIDC_NAME:-} + OIDC_ISSUER: ${OIDC_ISSUER:-} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} + WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:?WEBAUTHN_RP_ID fehlt in .env} + WEBAUTHN_RP_NAME: ${WEBAUTHN_RP_NAME:-Planer} + WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:?WEBAUTHN_ORIGIN fehlt in .env} + DEMO_MODUS: 'false' + ports: + - '${APP_PORT:-3000}:3000' + healthcheck: + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 5 + start_period: 40s + +volumes: + db-daten: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..09e0de2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,57 @@ +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import naechstesWebVitals from 'eslint-config-next/core-web-vitals' +import tseslint from 'typescript-eslint' + +const wurzel = dirname(fileURLToPath(import.meta.url)) + +export default tseslint.config( + { + ignores: [ + '.next/**', + 'node_modules/**', + 'coverage/**', + 'playwright-report/**', + 'test-results/**', + // Vom Generator erzeugt, nicht von Hand gepflegt. + 'src/generated/**', + 'next-env.d.ts', + ], + }, + ...naechstesWebVitals, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: wurzel, + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/require-await': 'error', + // Keine Geheimnisse in Logs: console nur fuer Warnungen und Fehler. + 'no-console': ['error', { allow: ['warn', 'error'] }], + eqeqeq: ['error', 'always', { null: 'ignore' }], + }, + }, + // Skripte, Seed und Tests duerfen auf die Konsole schreiben. + { + files: ['scripts/**', 'prisma/**', '**/*.test.ts', '**/*.test.tsx', 'e2e/**'], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['**/*.js', '**/*.mjs', '**/*.cjs'], + extends: [tseslint.configs.disableTypeChecked], + }, +) diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..245a685 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from 'next' + +const config: NextConfig = { + output: 'standalone', + reactStrictMode: true, + poweredByHeader: false, + typedRoutes: true, + experimental: { + serverActions: { + bodySizeLimit: '2mb', + }, + }, +} + +export default config diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bacfb3c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12607 @@ +{ + "name": "planer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "planer", + "version": "0.0.0", + "dependencies": { + "@auth/prisma-adapter": "2.11.3", + "@date-fns/tz": "1.5.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slot": "1.3.3", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "bcryptjs": "3.0.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.4.0", + "lucide-react": "1.44.0", + "next": "16.3.4", + "next-auth": "5.0.0-beta.32", + "next-themes": "0.4.6", + "node-cron": "4.6.0", + "nodemailer": "10.0.3", + "pg": "8.23.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "server-only": "0.0.1", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "zod": "4.6.1" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "4.3.3", + "@testing-library/react": "16.3.3", + "@types/node": "24.13.4", + "@types/nodemailer": "8.0.1", + "@types/pg": "8.23.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "@vitest/coverage-v8": "5.0.0", + "dotenv": "17.4.2", + "eslint": "9.39.5", + "eslint-config-next": "16.3.4", + "jsdom": "30.0.1", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", + "prisma": "7.10.0", + "tailwindcss": "4.3.3", + "tsx": "4.23.13", + "tw-animate-css": "1.4.0", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@auth/core": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.3.tgz", + "integrity": "sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7 || ^8.0.5" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.3.tgz", + "integrity": "sha512-jZbpVAO6PTc9zNtdTWc0RLWG8qap4iMc54/3oWaWbuKdj92wHxzPrs3HivWB2mB9975GPW0l3YM/wFUWiUlTlg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.3" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", + "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", + "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", + "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@next/env": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.4.tgz", + "integrity": "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.4.tgz", + "integrity": "sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.4.tgz", + "integrity": "sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.4.tgz", + "integrity": "sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.4.tgz", + "integrity": "sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.4.tgz", + "integrity": "sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.4.tgz", + "integrity": "sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.4.tgz", + "integrity": "sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.4.tgz", + "integrity": "sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.4.tgz", + "integrity": "sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.9.4.tgz", + "integrity": "sha512-SYHm4SoWSI0nRCoos6jpGusIqhPH9bbGBqv7ohlZ+H6BunrDzzQPk2ePgDuEUzV82OdvbLgtW4twUDwhU9P3YQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-asym-key": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.9.4.tgz", + "integrity": "sha512-s7SJfjcXlR3MYMngDTeMLCy3VjGaIxeEy905CQ0j09pDbeYhNBvBGA7r7cAvCZYkVFo9kVg9RAki0K2iUz7SGQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-post-quantum": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.9.4.tgz", + "integrity": "sha512-GD0k7BY3dsEIeai7C7bVRPddj/PzZu+sTawRPRxdHKbdy1f9b58WQyk0eFJdr28GShyLVTU8poyi4+bTAZfEtQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-asym-key": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz", + "integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-csr": "^2.9.4", + "@peculiar/asn1-ecc": "^2.9.4", + "@peculiar/asn1-pkcs9": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz", + "integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.10.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz", + "integrity": "sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.10.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.0.tgz", + "integrity": "sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz", + "integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.17", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", + "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.3", + "@electric-sql/pglite-socket": "0.1.3", + "@electric-sql/pglite-tools": "0.3.3", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.11", + "find-my-way": "9.7.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.4.2", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/dev/node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz", + "integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", + "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/fetch-engine": "7.10.0", + "@prisma/get-platform": "7.10.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", + "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", + "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/get-platform": "7.10.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", + "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.2.0", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", + "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "@visx/curve": "4.0.1-alpha.0", + "@visx/event": "4.0.1-alpha.0", + "@visx/grid": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/responsive": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "d3-array": "3.2.4", + "d3-shape": "3.2.0", + "elkjs": "0.11.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simplewebauthn/browser": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-14.0.0.tgz", + "integrity": "sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.1.tgz", + "integrity": "sha512-kjWgcm9NdSv+Tobo4Swxd1WnEawsBogpYFMjg9pKsVLPN3FdCmR6BipkL+DcPDgmQ7UKbbBZqRqn3E3YaA7NBw==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.0.tgz", + "integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.0", + "vitest": "5.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", + "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", + "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", + "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.426", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.426.tgz", + "integrity": "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "devOptional": true, + "license": "EPL-2.0" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.4.tgz", + "integrity": "sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.3.4", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-config-next/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-config-next/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", + "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.44.0.tgz", + "integrity": "sha512-2egNApH4hX4j/qdCgRublh88+9u3mEhz9iSlW5ckm4kaQEqZbXbMr0l5u5JZLy8nmWRx2dbHGQkEDYz6C9aCgw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", + "integrity": "sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.4", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.4", + "@next/swc-darwin-x64": "16.3.4", + "@next/swc-linux-arm64-gnu": "16.3.4", + "@next/swc-linux-arm64-musl": "16.3.4", + "@next/swc-linux-x64-gnu": "16.3.4", + "@next/swc-linux-x64-musl": "16.3.4", + "@next/swc-win32-arm64-msvc": "16.3.4", + "@next/swc-win32-x64-msvc": "16.3.4", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.32", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.32.tgz", + "integrity": "sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7 || ^8.0.5", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-cron": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.6.0.tgz", + "integrity": "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==", + "license": "ISC", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.3.tgz", + "integrity": "sha512-h5c8ypcMoOIh44bIcVROvTDbjRdOJX8AXTkxMht2C92HlSjkG+/8kjoqmNPypfjQaVNaUHWyZ3tqs5I3K91F3g==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.8", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.8.tgz", + "integrity": "sha512-8N28E+a/oxfXWBgOMt+ZP/JUf/XR+IFbvkAEPP3gznXOMv9BpAAwiIj0TFNz3tGTPc0ZQ8zmWBNgN1nAys0gng==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.1", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz", + "integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/prisma": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", + "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.10.0", + "@prisma/dev": "0.24.17", + "@prisma/engines": "7.10.0", + "@prisma/studio-core": "0.33.0", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", + "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.7", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "devOptional": true, + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", + "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.12" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", + "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.1.tgz", + "integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5190880 --- /dev/null +++ b/package.json @@ -0,0 +1,101 @@ +{ + "name": "planer", + "version": "0.0.0", + "private": true, + "description": "Aufgaben, Ideen, Projekte und Zeitblöcke mit Anbindung an Microsoft 365 und Google Kalender", + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "pruefen": "node scripts/pruefen.mjs", + "format": "prettier --write .", + "format:pruefen": "prettier --check .", + "lint": "eslint . --max-warnings 0", + "typen": "tsc --noEmit", + "test": "vitest run", + "test:beobachten": "vitest", + "test:integration": "tsx scripts/integrationstest.ts", + "test:migration": "tsx scripts/migrationstest.ts", + "test:e2e": "playwright test", + "screenshots": "tsx scripts/screenshots.ts", + "db:generieren": "prisma generate", + "db:migrieren": "prisma migrate dev", + "db:einspielen": "prisma migrate deploy", + "db:seed": "tsx prisma/seed.ts", + "db:studio": "prisma studio" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "overrides": { + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "nodemailer": "10.0.3" + }, + "dependencies": { + "@auth/prisma-adapter": "2.11.3", + "@date-fns/tz": "1.5.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@radix-ui/react-avatar": "1.2.6", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-dropdown-menu": "2.1.24", + "@radix-ui/react-label": "2.1.15", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-slot": "1.3.3", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.1", + "bcryptjs": "3.0.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.4.0", + "lucide-react": "1.44.0", + "next": "16.3.4", + "next-auth": "5.0.0-beta.32", + "next-themes": "0.4.6", + "node-cron": "4.6.0", + "nodemailer": "10.0.3", + "pg": "8.23.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "server-only": "0.0.1", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "zod": "4.6.1" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "4.3.3", + "@testing-library/react": "16.3.3", + "@types/node": "24.13.4", + "@types/nodemailer": "8.0.1", + "@types/pg": "8.23.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "@vitest/coverage-v8": "5.0.0", + "dotenv": "17.4.2", + "eslint": "9.39.5", + "eslint-config-next": "16.3.4", + "jsdom": "30.0.1", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", + "prisma": "7.10.0", + "tailwindcss": "4.3.3", + "tsx": "4.23.13", + "tw-animate-css": "1.4.0", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + }, + "allowScripts": { + "prisma@7.10.0": true, + "@prisma/engines@7.10.0": true, + "esbuild@0.28.2": true, + "unrs-resolver@1.12.2": true + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..a869506 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +} + +export default config diff --git a/scripts/pruefen.mjs b/scripts/pruefen.mjs new file mode 100644 index 0000000..38f1c38 --- /dev/null +++ b/scripts/pruefen.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * Sammelpruefung vor jedem Commit: Format, Lint, Typen, Tests, Shellskripte. + * Bricht beim ersten Fehler ab und nennt den Schritt, der gefallen ist. + * + * Die Werkzeuge werden ueber ihren JavaScript-Einstiegspunkt gestartet, nicht + * ueber npx. Das spart eine Shell und laeuft unter Windows wie unter Linux + * gleich. + */ +import { spawnSync } from 'node:child_process' +import { existsSync, readdirSync, statSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { fileURLToPath } from 'node:url' + +const WURZEL = fileURLToPath(new URL('..', import.meta.url)) +const istWindows = process.platform === 'win32' + +/** Windows-Pfad in Schraegstrich-Schreibweise, wie Docker sie erwartet. */ +function mitSchraegstrich(pfad) { + return pfad.split(sep).join('/') +} + +/** Ein mitgeliefertes Werkzeug im selben Node starten. */ +function werkzeug(paket, einstieg, argumente) { + const ergebnis = spawnSync( + process.execPath, + [join(WURZEL, 'node_modules', paket, einstieg), ...argumente], + { cwd: WURZEL, stdio: 'inherit' }, + ) + return ergebnis.status ?? 1 +} + +function programm(befehl, argumente, optionen = {}) { + const ergebnis = spawnSync(befehl, argumente, { + cwd: WURZEL, + stdio: 'inherit', + ...optionen, + }) + return ergebnis.status ?? 1 +} + +function vorhanden(befehl) { + const ergebnis = spawnSync(istWindows ? 'where' : 'which', [befehl], { + cwd: WURZEL, + stdio: 'ignore', + }) + return ergebnis.status === 0 +} + +const UEBERGEHEN = new Set([ + 'node_modules', + '.git', + '.next', + 'coverage', + 'logs', + 'sicherungen', + 'playwright-report', + 'test-results', +]) + +/** Alle .sh-Dateien im Projekt, ohne node_modules und Konsorten. */ +function shellskripteFinden(verzeichnis = WURZEL, gefunden = []) { + for (const eintrag of readdirSync(verzeichnis)) { + if (UEBERGEHEN.has(eintrag)) continue + const pfad = join(verzeichnis, eintrag) + if (statSync(pfad).isDirectory()) { + shellskripteFinden(pfad, gefunden) + } else if (eintrag.endsWith('.sh')) { + gefunden.push(mitSchraegstrich(relative(WURZEL, pfad))) + } + } + return gefunden +} + +function shellskriptePruefen() { + const skripte = shellskripteFinden() + if (skripte.length === 0) { + console.log(' Keine Shellskripte gefunden.') + return 0 + } + console.log(` ${skripte.length} Skript(e): ${skripte.join(', ')}`) + const argumente = ['--shell=bash', '--severity=style', ...skripte] + + if (vorhanden('shellcheck')) { + return programm('shellcheck', argumente) + } + + if (programm('docker', ['info'], { stdio: 'ignore' }) === 0) { + console.log(' shellcheck ist nicht installiert, nehme das Docker-Abbild.') + return programm('docker', [ + 'run', + '--rm', + '-v', + `${mitSchraegstrich(WURZEL)}:/mnt`, + 'koalaman/shellcheck:stable', + ...argumente, + ]) + } + + console.error( + 'Fehler: Weder shellcheck noch ein laufender Docker-Daemon gefunden. ' + + 'Entweder shellcheck installieren (https://www.shellcheck.net/) oder ' + + 'Docker starten. Die Shellskripte bleiben sonst ungeprueft.', + ) + return 1 +} + +const schritte = [ + { + name: 'Format (Prettier)', + tun: () => werkzeug('prettier', 'bin/prettier.cjs', ['--check', '.']), + }, + { + name: 'Lint (ESLint)', + tun: () => werkzeug('eslint', 'bin/eslint.js', ['.', '--max-warnings', '0']), + }, + { + name: 'Typen (TypeScript)', + tun: () => werkzeug('typescript', 'bin/tsc', ['--noEmit']), + }, + { name: 'Tests (Vitest)', tun: () => werkzeug('vitest', 'vitest.mjs', ['run']) }, + { name: 'Shellskripte (ShellCheck)', tun: shellskriptePruefen }, +] + +// Ohne erzeugten Prisma-Client schlagen Typen und Tests fehl. +if (!existsSync(join(WURZEL, 'src', 'generated', 'prisma', 'client.ts'))) { + console.log('▸ Prisma-Client fehlt, erzeuge ihn') + if (werkzeug('prisma', 'build/index.js', ['generate']) !== 0) process.exit(1) +} + +let gefallen = null +for (const schritt of schritte) { + console.log(`▸ ${schritt.name}`) + if (schritt.tun() !== 0) { + gefallen = schritt.name + break + } +} + +if (gefallen) { + console.error(`✗ Gescheitert: ${gefallen}`) + process.exit(1) +} + +console.log('✓ Alle Pruefungen bestanden.') diff --git a/scripts/zeit.sh b/scripts/zeit.sh new file mode 100755 index 0000000..6d6d719 --- /dev/null +++ b/scripts/zeit.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Zeiterfassung für den Planer. +# Schreibt nach devlog/zeiten.csv: datum;start;ende;minuten;meilenstein;art;notiz +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CSV="$WURZEL/devlog/zeiten.csv" +KOPF='datum;start;ende;minuten;meilenstein;art;notiz' +OFFEN="$WURZEL/devlog/.zeit-offen" + +hilfe() { + cat <<'ENDE' +Verwendung: zeit.sh [argumente] + + start [Notiz] Eintrag öffnen (Art: claude) + stop [Notiz] Offenen Eintrag schließen + nachtrag + Eigene Zeit nachtragen + (Art: review, test, einrichtung) + summe [Meilenstein] Summen je Meilenstein und Art + status Zeigt, ob ein Eintrag offen ist + hilfe Diese Übersicht +ENDE +} + +fehler() { + printf 'Fehler: %s\n' "$1" >&2 + exit 1 +} + +# Semikolons und Zeilenumbrüche würden die CSV zerlegen. +saeubern() { + printf '%s' "$1" | tr '\n\r;' ' ' | sed 's/ */ /g; s/^ //; s/ $//' +} + +csv_anlegen() { + mkdir -p "$(dirname "$CSV")" + if [ ! -f "$CSV" ]; then + printf '%s\n' "$KOPF" > "$CSV" + fi +} + +# Minuten seit Mitternacht, damit auch ohne GNU date gerechnet werden kann. +zu_minuten() { + local uhr="$1" + local h="${uhr%%:*}" + local m="${uhr##*:}" + printf '%s' "$((10#$h * 60 + 10#$m))" +} + +befehl_start() { + local meilenstein="${1:-}" + [ -n "$meilenstein" ] || fehler 'Meilenstein fehlt. Beispiel: zeit.sh start M1 "Gerüst"' + shift + local notiz + notiz="$(saeubern "${*:-}")" + + if [ -f "$OFFEN" ]; then + local alt + alt="$(cat "$OFFEN")" + fehler "Es läuft bereits ein Eintrag ($alt). Erst 'zeit.sh stop'." + fi + + csv_anlegen + mkdir -p "$(dirname "$OFFEN")" + printf '%s;%s;%s;%s\n' "$(date '+%Y-%m-%d')" "$(date '+%H:%M')" \ + "$(saeubern "$meilenstein")" "$notiz" > "$OFFEN" + printf 'Gestartet: %s um %s Uhr%s\n' "$meilenstein" "$(date '+%H:%M')" \ + "${notiz:+ – $notiz}" +} + +befehl_stop() { + [ -f "$OFFEN" ] || fehler "Kein offener Eintrag. Erst 'zeit.sh start '." + + local zeile datum start meilenstein notiz_start + zeile="$(cat "$OFFEN")" + IFS=';' read -r datum start meilenstein notiz_start <<< "$zeile" + + local ende notiz + ende="$(date '+%H:%M')" + notiz="$(saeubern "${*:-}")" + [ -n "$notiz" ] || notiz="$notiz_start" + + local von bis minuten + von="$(zu_minuten "$start")" + bis="$(zu_minuten "$ende")" + minuten=$((bis - von)) + # Über Mitternacht hinweg. + [ "$minuten" -ge 0 ] || minuten=$((minuten + 1440)) + + csv_anlegen + printf '%s;%s;%s;%s;%s;claude;%s\n' \ + "$datum" "$start" "$ende" "$minuten" "$meilenstein" "$notiz" >> "$CSV" + rm -f "$OFFEN" + printf 'Gestoppt: %s, %s Minuten (%s–%s)\n' "$meilenstein" "$minuten" "$start" "$ende" +} + +befehl_nachtrag() { + local meilenstein="${1:-}" minuten="${2:-}" art="${3:-}" + [ -n "$meilenstein" ] || fehler 'Meilenstein fehlt.' + [ -n "$minuten" ] || fehler 'Minuten fehlen.' + [ -n "$art" ] || fehler 'Art fehlt (review, test, einrichtung).' + case "$minuten" in + ''|*[!0-9]*) fehler "Minuten müssen eine ganze Zahl sein, nicht '$minuten'." ;; + esac + shift 3 + local notiz + notiz="$(saeubern "${*:-}")" + [ -n "$notiz" ] || fehler 'Notiz fehlt.' + + csv_anlegen + printf '%s;;;%s;%s;%s;%s\n' \ + "$(date '+%Y-%m-%d')" "$minuten" "$(saeubern "$meilenstein")" \ + "$(saeubern "$art")" "$notiz" >> "$CSV" + printf 'Nachgetragen: %s Minuten auf %s (%s)\n' "$minuten" "$meilenstein" "$art" +} + +befehl_summe() { + local filter="${1:-}" + [ -f "$CSV" ] || fehler "Noch keine Zeiten erfasst ($CSV fehlt)." + + awk -F';' -v filter="$filter" ' + NR == 1 { next } + $5 == "" { next } + filter != "" && $5 != filter { next } + { + schluessel = $5 ";" $6 + summe[schluessel] += $4 + je_meilenstein[$5] += $4 + gesamt += $4 + if (!($5 in gesehen)) { gesehen[$5] = 1; reihenfolge[++anzahl] = $5 } + } + END { + if (gesamt == 0) { print "Keine Einträge gefunden."; exit } + printf "%-14s %-14s %10s %10s\n", "Meilenstein", "Art", "Minuten", "Stunden" + printf "%s\n", "------------------------------------------------------" + for (i = 1; i <= anzahl; i++) { + m = reihenfolge[i] + for (s in summe) { + split(s, teil, ";") + if (teil[1] == m) { + printf "%-14s %-14s %10d %10.2f\n", m, teil[2], summe[s], summe[s] / 60 + } + } + printf "%-14s %-14s %10d %10.2f\n", m, "= Summe", je_meilenstein[m], je_meilenstein[m] / 60 + printf "%s\n", "------------------------------------------------------" + } + printf "%-29s %10d %10.2f\n", "Gesamt", gesamt, gesamt / 60 + } + ' "$CSV" +} + +befehl_status() { + if [ -f "$OFFEN" ]; then + local zeile datum start meilenstein notiz + zeile="$(cat "$OFFEN")" + IFS=';' read -r datum start meilenstein notiz <<< "$zeile" + printf 'Offen seit %s %s Uhr: %s%s\n' "$datum" "$start" "$meilenstein" "${notiz:+ – $notiz}" + else + printf 'Kein offener Eintrag.\n' + fi +} + +befehl="${1:-hilfe}" +shift || true + +case "$befehl" in + start) befehl_start "$@" ;; + stop) befehl_stop "$@" ;; + nachtrag) befehl_nachtrag "$@" ;; + summe) befehl_summe "$@" ;; + status) befehl_status ;; + hilfe|-h|--hilfe|--help) hilfe ;; + *) fehler "Unbekannter Befehl '$befehl'. 'zeit.sh hilfe' zeigt die Übersicht." ;; +esac diff --git a/test/aufbau-oberflaeche.ts b/test/aufbau-oberflaeche.ts new file mode 100644 index 0000000..ed1966c --- /dev/null +++ b/test/aufbau-oberflaeche.ts @@ -0,0 +1,8 @@ +import { cleanup } from '@testing-library/react' +import { afterEach } from 'vitest' + +// Nach jedem Test das gerenderte DOM abraeumen, damit Tests sich nicht +// gegenseitig sehen. +afterEach(() => { + cleanup() +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..07b5708 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "ES2022"], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "preserve", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "noEmit": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "vitest.config.ts", + "playwright.config.ts" + ], + "exclude": ["node_modules", ".next", "out"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..dfea4bc --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,41 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [react()], + resolve: { + // Loest den Alias @/ aus der tsconfig.json auf. + tsconfigPaths: true, + }, + test: { + // Die Zeitrechnung des Planers gilt in Europe/Berlin. Damit Tests nicht + // von der Zeitzone des Rechners abhaengen, laufen sie in UTC – so faellt + // sofort auf, wenn irgendwo die Ortszeit des Servers durchschlaegt. + env: { TZ: 'UTC' }, + coverage: { + provider: 'v8', + reportsDirectory: 'coverage', + include: ['src/lib/**/*.ts'], + exclude: ['**/*.test.ts'], + }, + projects: [ + { + extends: true, + test: { + name: 'logik', + environment: 'node', + include: ['src/lib/**/*.test.ts', 'src/server/**/*.test.ts'], + }, + }, + { + extends: true, + test: { + name: 'oberflaeche', + environment: 'jsdom', + include: ['src/components/**/*.test.tsx', 'src/app/**/*.test.tsx'], + setupFiles: ['./test/aufbau-oberflaeche.ts'], + }, + }, + ], + }, +}) From 748d032f70e63b7b1a141fa514b24a75e91e0a5d Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 12:39:58 +0200 Subject: [PATCH 02/19] =?UTF-8?q?feat(datenbank):=20Datenmodell=20f=C3=BCr?= =?UTF-8?q?=20Anmeldung,=20Bereiche=20und=20Audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benutzer mit Rolle, OIDC-Konten, Sitzungen, Passkeys samt kurzlebiger WebAuthn-Herausforderung, Bereiche mit Tageskapazität und Arbeitsfenster sowie ein unveränderlicher Audit-Eintrag je schreibender Aktion. Aufgabe und Zeitblock kommen mit M2 und M4, Kalenderkonto mit M3 — dann mit den Feldern, die der jeweilige Provider wirklich braucht. Prisma 7 nimmt die Verbindung nicht mehr aus dem Schema entgegen: die URL steht in prisma.config.ts, der Client bekommt den pg-Treiberadapter. Co-Authored-By: Claude Opus 5 (1M context) --- prisma.config.ts | 13 ++ .../20260910103056_fundament/migration.sql | 179 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 170 +++++++++++++++++ src/server/db.ts | 31 +++ 5 files changed, 396 insertions(+) create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20260910103056_fundament/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/server/db.ts diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..2774414 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,13 @@ +import 'dotenv/config' +import { defineConfig, env } from 'prisma/config' + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + seed: 'tsx prisma/seed.ts', + }, + datasource: { + url: env('DATABASE_URL'), + }, +}) diff --git a/prisma/migrations/20260910103056_fundament/migration.sql b/prisma/migrations/20260910103056_fundament/migration.sql new file mode 100644 index 0000000..59096f8 --- /dev/null +++ b/prisma/migrations/20260910103056_fundament/migration.sql @@ -0,0 +1,179 @@ +-- CreateEnum +CREATE TYPE "Rolle" AS ENUM ('ADMIN', 'BENUTZER'); + +-- CreateTable +CREATE TABLE "benutzer" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "passwortHash" TEXT, + "rolle" "Rolle" NOT NULL DEFAULT 'BENUTZER', + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "bildUrl" TEXT, + "emailGeprueft" TIMESTAMP(3), + "zuletztAktiv" TIMESTAMP(3), + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "aktualisiertAm" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "benutzer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "konto" ( + "id" TEXT NOT NULL, + "benutzerId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "konto_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sitzung" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "benutzerId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sitzung_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "verifizierungs_token" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "passkey" ( + "id" TEXT NOT NULL, + "benutzerId" TEXT NOT NULL, + "credentialId" TEXT NOT NULL, + "publicKey" BYTEA NOT NULL, + "counter" BIGINT NOT NULL DEFAULT 0, + "transports" TEXT, + "geraetetyp" TEXT, + "gesichert" BOOLEAN NOT NULL DEFAULT false, + "bezeichnung" TEXT NOT NULL, + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "zuletztBenutzt" TIMESTAMP(3), + + CONSTRAINT "passkey_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "webauthn_challenge" ( + "id" TEXT NOT NULL, + "kennung" TEXT NOT NULL, + "challenge" TEXT NOT NULL, + "zweck" TEXT NOT NULL, + "gueltigBis" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "webauthn_challenge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "bereich" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "farbe" TEXT NOT NULL, + "reihenfolge" INTEGER NOT NULL DEFAULT 0, + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "kapazitaetMo" INTEGER NOT NULL DEFAULT 0, + "kapazitaetDi" INTEGER NOT NULL DEFAULT 0, + "kapazitaetMi" INTEGER NOT NULL DEFAULT 0, + "kapazitaetDo" INTEGER NOT NULL DEFAULT 0, + "kapazitaetFr" INTEGER NOT NULL DEFAULT 0, + "kapazitaetSa" INTEGER NOT NULL DEFAULT 0, + "kapazitaetSo" INTEGER NOT NULL DEFAULT 0, + "arbeitsfensterVon" INTEGER NOT NULL DEFAULT 480, + "arbeitsfensterBis" INTEGER NOT NULL DEFAULT 1020, + "erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "aktualisiertAm" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "bereich_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_eintrag" ( + "id" TEXT NOT NULL, + "zeitpunkt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "benutzerId" TEXT, + "benutzerEmail" TEXT NOT NULL, + "aktion" TEXT NOT NULL, + "objektTyp" TEXT NOT NULL, + "objektId" TEXT, + "details" JSONB, + "instanz" TEXT NOT NULL, + + CONSTRAINT "audit_eintrag_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "benutzer_email_key" ON "benutzer"("email"); + +-- CreateIndex +CREATE INDEX "benutzer_aktiv_idx" ON "benutzer"("aktiv"); + +-- CreateIndex +CREATE INDEX "konto_benutzerId_idx" ON "konto"("benutzerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "konto_provider_providerAccountId_key" ON "konto"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "sitzung_sessionToken_key" ON "sitzung"("sessionToken"); + +-- CreateIndex +CREATE INDEX "sitzung_benutzerId_idx" ON "sitzung"("benutzerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "verifizierungs_token_token_key" ON "verifizierungs_token"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "verifizierungs_token_identifier_token_key" ON "verifizierungs_token"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "passkey_credentialId_key" ON "passkey"("credentialId"); + +-- CreateIndex +CREATE INDEX "passkey_benutzerId_idx" ON "passkey"("benutzerId"); + +-- CreateIndex +CREATE INDEX "webauthn_challenge_kennung_zweck_idx" ON "webauthn_challenge"("kennung", "zweck"); + +-- CreateIndex +CREATE UNIQUE INDEX "bereich_name_key" ON "bereich"("name"); + +-- CreateIndex +CREATE INDEX "bereich_aktiv_reihenfolge_idx" ON "bereich"("aktiv", "reihenfolge"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_zeitpunkt_idx" ON "audit_eintrag"("zeitpunkt"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_objektTyp_objektId_idx" ON "audit_eintrag"("objektTyp", "objektId"); + +-- CreateIndex +CREATE INDEX "audit_eintrag_benutzerId_idx" ON "audit_eintrag"("benutzerId"); + +-- AddForeignKey +ALTER TABLE "konto" ADD CONSTRAINT "konto_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sitzung" ADD CONSTRAINT "sitzung_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "passkey" ADD CONSTRAINT "passkey_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_eintrag" ADD CONSTRAINT "audit_eintrag_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..313b933 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,170 @@ +// Datenmodell des Planers. +// Fachbegriffe auf Deutsch, technische Felder auf Englisch. + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" + moduleFormat = "esm" +} + +datasource db { + provider = "postgresql" +} + +enum Rolle { + ADMIN + BENUTZER +} + +/// Ein Mensch, der sich anmelden darf. +model Benutzer { + id String @id @default(cuid()) + email String @unique + name String? + /// Nur gesetzt, wenn die Anmeldung per Passwort erlaubt ist. + passwortHash String? + rolle Rolle @default(BENUTZER) + aktiv Boolean @default(true) + bildUrl String? + emailGeprueft DateTime? + zuletztAktiv DateTime? + erstelltAm DateTime @default(now()) + aktualisiertAm DateTime @updatedAt + + konten Konto[] + sitzungen Sitzung[] + passkeys Passkey[] + audit AuditEintrag[] + + @@index([aktiv]) + @@map("benutzer") +} + +/// Verknuepfung zu einem OIDC-Anbieter (Entra ID, authentik). +model Konto { + id String @id @default(cuid()) + benutzerId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([benutzerId]) + @@map("konto") +} + +model Sitzung { + id String @id @default(cuid()) + sessionToken String @unique + benutzerId String + expires DateTime + + benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) + + @@index([benutzerId]) + @@map("sitzung") +} + +model VerifizierungsToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map("verifizierungs_token") +} + +/// Ein registrierter Passkey (WebAuthn) eines Benutzers. +model Passkey { + id String @id @default(cuid()) + benutzerId String + /// Base64URL der Credential-ID. + credentialId String @unique + publicKey Bytes + counter BigInt @default(0) + /// Kommagetrennt, z. B. "internal,hybrid". + transports String? + geraetetyp String? + gesichert Boolean @default(false) + bezeichnung String + erstelltAm DateTime @default(now()) + zuletztBenutzt DateTime? + + benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) + + @@index([benutzerId]) + @@map("passkey") +} + +/// Laufende WebAuthn-Herausforderung; kurzlebig, wird nach Gebrauch geloescht. +model WebauthnChallenge { + id String @id @default(cuid()) + /// E-Mail bei der Anmeldung, Benutzer-ID bei der Registrierung. + kennung String + challenge String + zweck String + gueltigBis DateTime + + @@index([kennung, zweck]) + @@map("webauthn_challenge") +} + +/// Lebensbereich mit Farbe, Tageskapazitaet und Arbeitsfenster. +model Bereich { + id String @id @default(cuid()) + name String @unique + /// Hex-Farbe inklusive Raute, z. B. "#2563eb". + farbe String + reihenfolge Int @default(0) + aktiv Boolean @default(true) + + /// Tageskapazitaet in Minuten, Montag bis Sonntag. + kapazitaetMo Int @default(0) + kapazitaetDi Int @default(0) + kapazitaetMi Int @default(0) + kapazitaetDo Int @default(0) + kapazitaetFr Int @default(0) + kapazitaetSa Int @default(0) + kapazitaetSo Int @default(0) + + /// Arbeitsfenster als Minuten seit Mitternacht, Europe/Berlin. + arbeitsfensterVon Int @default(480) + arbeitsfensterBis Int @default(1020) + + erstelltAm DateTime @default(now()) + aktualisiertAm DateTime @updatedAt + + @@index([aktiv, reihenfolge]) + @@map("bereich") +} + +/// Unveraenderlicher Nachweis jeder schreibenden Aktion. +model AuditEintrag { + id String @id @default(cuid()) + zeitpunkt DateTime @default(now()) + benutzerId String? + /// Bleibt erhalten, auch wenn der Benutzer spaeter geloescht wird. + benutzerEmail String + aktion String + objektTyp String + objektId String? + /// Nur fachliche Felder, niemals Tokens, Mailinhalte oder Termin-Betreffs. + details Json? + instanz String + + benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull) + + @@index([zeitpunkt]) + @@index([objektTyp, objektId]) + @@index([benutzerId]) + @@map("audit_eintrag") +} diff --git a/src/server/db.ts b/src/server/db.ts new file mode 100644 index 0000000..b5170ec --- /dev/null +++ b/src/server/db.ts @@ -0,0 +1,31 @@ +import 'server-only' +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from '@/generated/prisma/client' + +/** + * Ein einziger Prisma-Client je Prozess. Im Entwicklungsbetrieb ueberlebt er + * das Neuladen der Module, damit nicht bei jedem Speichern ein neuer + * Verbindungspool entsteht. + */ + +const globalerBereich = globalThis as unknown as { + prisma: PrismaClient | undefined +} + +function erzeugen(): PrismaClient { + const verbindung = process.env.DATABASE_URL + if (!verbindung) { + throw new Error('DATABASE_URL ist nicht gesetzt.') + } + const adapter = new PrismaPg({ connectionString: verbindung }) + return new PrismaClient({ + adapter, + log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], + }) +} + +export const db: PrismaClient = globalerBereich.prisma ?? erzeugen() + +if (process.env.NODE_ENV !== 'production') { + globalerBereich.prisma = db +} From e2b00f4a57653af6c3eb5ba4758e159de6eb7dae Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 12:39:58 +0200 Subject: [PATCH 03/19] feat(instanz): Instanzsteuerung mit serverseitigem Riegel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit INSTANZ entscheidet, welche Kalenderanbieter, Postfächer und Integrationen es gibt. Was eine Instanz nicht darf, liefert dort 404 statt eines Hinweises. Ein falsch geschriebener Wert bricht ab, statt still auf einen Standard zu fallen. Die Konfiguration wird mit zod geprüft; Fehlermeldungen nennen den Variablennamen, nie den Wert. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/instanz.test.ts | 91 ++++++++++++++++++++++++++++++++++++++ src/lib/instanz.ts | 96 +++++++++++++++++++++++++++++++++++++++++ src/server/instanz.ts | 46 ++++++++++++++++++++ src/server/umgebung.ts | 71 ++++++++++++++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 src/lib/instanz.test.ts create mode 100644 src/lib/instanz.ts create mode 100644 src/server/instanz.ts create mode 100644 src/server/umgebung.ts diff --git a/src/lib/instanz.test.ts b/src/lib/instanz.test.ts new file mode 100644 index 0000000..0261a21 --- /dev/null +++ b/src/lib/instanz.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { + FAEHIGKEITEN, + INSTANZEN, + faehigkeitenVon, + istErlaubt, + istInstanz, + kalenderAnbieterVon, + leseInstanz, +} from './instanz' + +describe('leseInstanz', () => { + it('nimmt die erlaubten Werte an', () => { + expect(leseInstanz('firma')).toBe('firma') + expect(leseInstanz('daheim')).toBe('daheim') + }) + + it('ist unempfindlich gegen Gross- und Kleinschreibung und Leerzeichen', () => { + expect(leseInstanz(' FIRMA ')).toBe('firma') + }) + + it('bricht ab, wenn nichts gesetzt ist', () => { + expect(() => leseInstanz(undefined)).toThrow(/nicht gesetzt/) + expect(() => leseInstanz(' ')).toThrow(/nicht gesetzt/) + }) + + it('faellt bei einem Tippfehler nicht still auf einen Standard zurueck', () => { + expect(() => leseInstanz('fimra')).toThrow(/unbekannten Wert/) + }) +}) + +describe('istInstanz', () => { + it('erkennt gueltige Werte', () => { + expect(istInstanz('firma')).toBe(true) + expect(istInstanz('daheim')).toBe(true) + }) + + it('weist alles andere ab', () => { + expect(istInstanz('privat')).toBe(false) + expect(istInstanz(undefined)).toBe(false) + expect(istInstanz(42)).toBe(false) + }) +}) + +describe('istErlaubt', () => { + it('trennt die Kalenderanbieter der beiden Instanzen', () => { + expect(istErlaubt('firma', 'kalender:graph')).toBe(true) + expect(istErlaubt('firma', 'kalender:google')).toBe(false) + expect(istErlaubt('daheim', 'kalender:google')).toBe(true) + }) + + it('haelt die Integrationen auseinander', () => { + expect(istErlaubt('firma', 'integration:zammad')).toBe(true) + expect(istErlaubt('daheim', 'integration:zammad')).toBe(false) + expect(istErlaubt('daheim', 'integration:telegram')).toBe(true) + expect(istErlaubt('firma', 'integration:telegram')).toBe(false) + }) + + it('richtet den Spiegel in genau eine Richtung ein', () => { + expect(istErlaubt('firma', 'spiegel:senden')).toBe(true) + expect(istErlaubt('firma', 'spiegel:empfangen')).toBe(false) + expect(istErlaubt('daheim', 'spiegel:empfangen')).toBe(true) + expect(istErlaubt('daheim', 'spiegel:senden')).toBe(false) + }) +}) + +describe('faehigkeitenVon', () => { + it('liefert nur bekannte Faehigkeiten', () => { + for (const instanz of INSTANZEN) { + for (const faehigkeit of faehigkeitenVon(instanz)) { + expect(FAEHIGKEITEN).toContain(faehigkeit) + } + } + }) + + it('gibt keiner Instanz alles', () => { + for (const instanz of INSTANZEN) { + expect(faehigkeitenVon(instanz).length).toBeLessThan(FAEHIGKEITEN.length) + } + }) +}) + +describe('kalenderAnbieterVon', () => { + it('nennt fuer die Firma nur Graph', () => { + expect(kalenderAnbieterVon('firma')).toEqual(['GRAPH']) + }) + + it('nennt daheim beide', () => { + expect(kalenderAnbieterVon('daheim')).toEqual(['GRAPH', 'GOOGLE']) + }) +}) diff --git a/src/lib/instanz.ts b/src/lib/instanz.ts new file mode 100644 index 0000000..6726ae3 --- /dev/null +++ b/src/lib/instanz.ts @@ -0,0 +1,96 @@ +/** + * Instanzsteuerung. + * + * Ein Image, zwei Deployments. Welche Provider, Integrationen und Routen + * erlaubt sind, haengt allein an INSTANZ und wird serverseitig erzwungen – + * nicht in der Oberflaeche versteckt. + * + * Diese Datei ist reine Logik: kein Zugriff auf process.env, keine Systemzeit. + */ + +export const INSTANZEN = ['firma', 'daheim'] as const +export type Instanz = (typeof INSTANZEN)[number] + +/** + * Alles, was je nach Instanz an- oder abgeschaltet ist. Eine Faehigkeit, die + * hier nicht steht, gibt es ueberall. + */ +export const FAEHIGKEITEN = [ + // Kalender- und Mailanbindung + 'kalender:graph', + 'kalender:google', + 'mail:graph', + 'mail:gmail', + // Integrationen + 'integration:zammad', + 'integration:wartungsplaner', + 'integration:telegram', + 'spiegel:senden', + 'spiegel:empfangen', +] as const +export type Faehigkeit = (typeof FAEHIGKEITEN)[number] + +const ERLAUBT: Readonly> = { + firma: [ + 'kalender:graph', + 'mail:graph', + 'integration:zammad', + 'integration:wartungsplaner', + 'spiegel:senden', + ], + daheim: [ + 'kalender:graph', + 'kalender:google', + 'mail:graph', + 'mail:gmail', + 'integration:telegram', + 'spiegel:empfangen', + ], +} + +/** Anzeigename der Instanz fuer die Oberflaeche. */ +export const INSTANZ_NAME: Readonly> = { + firma: 'Firma', + daheim: 'Daheim', +} + +export function istInstanz(wert: unknown): wert is Instanz { + return typeof wert === 'string' && (INSTANZEN as readonly string[]).includes(wert) +} + +/** + * Liest den Konfigurationswert und bricht bei Unsinn ab. Ein falsch + * geschriebenes INSTANZ darf nicht stillschweigend zu "daheim" werden – + * sonst laufen in der Firma Provider an, die dort nichts zu suchen haben. + */ +export function leseInstanz(wert: string | undefined): Instanz { + const bereinigt = wert?.trim().toLowerCase() + if (!bereinigt) { + throw new Error('INSTANZ ist nicht gesetzt. Erlaubt: ' + INSTANZEN.join(', ')) + } + if (!istInstanz(bereinigt)) { + throw new Error( + `INSTANZ hat den unbekannten Wert "${wert ?? ''}". Erlaubt: ` + + INSTANZEN.join(', '), + ) + } + return bereinigt +} + +/** Ist diese Faehigkeit in dieser Instanz erlaubt? */ +export function istErlaubt(instanz: Instanz, faehigkeit: Faehigkeit): boolean { + return ERLAUBT[instanz].includes(faehigkeit) +} + +/** Alle in dieser Instanz erlaubten Faehigkeiten, in fester Reihenfolge. */ +export function faehigkeitenVon(instanz: Instanz): readonly Faehigkeit[] { + return FAEHIGKEITEN.filter((f) => istErlaubt(instanz, f)) +} + +/** Welche Kalenderanbieter darf diese Instanz verbinden? */ +export function kalenderAnbieterVon(instanz: Instanz): readonly ('GRAPH' | 'GOOGLE')[] { + const anbieter: ('GRAPH' | 'GOOGLE')[] = [] + if (istErlaubt(instanz, 'kalender:graph')) anbieter.push('GRAPH') + if (istErlaubt(instanz, 'kalender:google')) anbieter.push('GOOGLE') + return anbieter +} diff --git a/src/server/instanz.ts b/src/server/instanz.ts new file mode 100644 index 0000000..aaefe84 --- /dev/null +++ b/src/server/instanz.ts @@ -0,0 +1,46 @@ +import 'server-only' +import { notFound } from 'next/navigation' +import { + type Faehigkeit, + type Instanz, + istErlaubt, + kalenderAnbieterVon, +} from '@/lib/instanz' +import { umgebung } from '@/server/umgebung' + +/** Welche Instanz laeuft hier? */ +export function aktuelleInstanz(): Instanz { + return umgebung().instanz +} + +/** Ist die Faehigkeit in dieser Instanz erlaubt? */ +export function kann(faehigkeit: Faehigkeit): boolean { + return istErlaubt(aktuelleInstanz(), faehigkeit) +} + +/** + * Riegel fuer Seiten und Route Handler: was diese Instanz nicht darf, + * existiert hier auch nicht. Kein Hinweis, kein 403 – schlicht 404. + */ +export function erzwingeFaehigkeit(faehigkeit: Faehigkeit): void { + if (!kann(faehigkeit)) { + notFound() + } +} + +/** + * Riegel fuer Server Actions und alles ausserhalb des Renderns, wo notFound() + * nicht greift. + */ +export function pruefeFaehigkeit(faehigkeit: Faehigkeit): void { + if (!kann(faehigkeit)) { + throw new Error( + `Die Faehigkeit "${faehigkeit}" ist in der Instanz "${aktuelleInstanz()}" nicht erlaubt.`, + ) + } +} + +/** Kalenderanbieter, die in dieser Instanz verbunden werden duerfen. */ +export function erlaubteKalenderAnbieter(): readonly ('GRAPH' | 'GOOGLE')[] { + return kalenderAnbieterVon(aktuelleInstanz()) +} diff --git a/src/server/umgebung.ts b/src/server/umgebung.ts new file mode 100644 index 0000000..5900dfb --- /dev/null +++ b/src/server/umgebung.ts @@ -0,0 +1,71 @@ +import 'server-only' +import { z } from 'zod' +import { INSTANZEN, type Instanz } from '@/lib/instanz' + +/** + * Jede Einstellung von aussen wird geprueft, bevor sie benutzt wird. Fehlt + * etwas Pflichtiges, faellt das beim ersten Zugriff auf – mit einer Meldung, + * die den Variablennamen nennt, nie deren Wert. + */ + +const jaNein = z + .enum(['true', 'false', '1', '0', 'ja', 'nein']) + .transform((w) => w === 'true' || w === '1' || w === 'ja') + +const schema = z.object({ + INSTANZ: z.enum(INSTANZEN), + APP_URL: z.url().transform((u) => u.replace(/\/+$/, '')), + DATABASE_URL: z.string().min(1), + + AUTH_SECRET: z.string().min(16, 'AUTH_SECRET ist zu kurz, mindestens 16 Zeichen.'), + AUTH_TRUST_HOST: jaNein.default(false), + ANMELDUNG_PASSWORT: jaNein.default(false), + + OIDC_NAME: z.string().trim().default(''), + OIDC_ISSUER: z.string().trim().default(''), + OIDC_CLIENT_ID: z.string().trim().default(''), + OIDC_CLIENT_SECRET: z.string().trim().default(''), + + WEBAUTHN_RP_ID: z.string().min(1), + WEBAUTHN_RP_NAME: z.string().min(1).default('Planer'), + WEBAUTHN_ORIGIN: z.url(), + + DEMO_MODUS: jaNein.default(false), +}) + +export type Umgebung = z.infer & { + instanz: Instanz + /** OIDC ist nur eingerichtet, wenn alle vier Werte gesetzt sind. */ + oidcAktiv: boolean +} + +let zwischengespeichert: Umgebung | null = null + +function lesen(): Umgebung { + const ergebnis = schema.safeParse(process.env) + if (!ergebnis.success) { + const felder = ergebnis.error.issues + .map((f) => `${f.path.join('.')}: ${f.message}`) + .join('\n ') + throw new Error(`Die Konfiguration ist unvollstaendig:\n ${felder}`) + } + const werte = ergebnis.data + return { + ...werte, + instanz: werte.INSTANZ, + oidcAktiv: Boolean( + werte.OIDC_ISSUER && werte.OIDC_CLIENT_ID && werte.OIDC_CLIENT_SECRET, + ), + } +} + +/** Die geprueften Einstellungen. Erst beim ersten Aufruf ausgewertet. */ +export function umgebung(): Umgebung { + zwischengespeichert ??= lesen() + return zwischengespeichert +} + +/** Nur fuer Tests: erzwingt beim naechsten Aufruf ein erneutes Einlesen. */ +export function umgebungVergessen(): void { + zwischengespeichert = null +} From 19ca0826e641e00beee769808b8c07b774c32c9a Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 12:39:59 +0200 Subject: [PATCH 04/19] feat(zeit): Zeitrechnung und Anzeigeformate in Europe/Berlin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tagesgrenzen, Wochenbeginn am Montag und Wochentagsindex passend zu den Kapazitätsfeldern eines Bereichs. Alle Funktionen bekommen den Zeitpunkt als Parameter, lesen also nie selbst die Uhr. Die Tests laufen bewusst in UTC, damit sofort auffällt, wenn irgendwo die Ortszeit des Servers durchschlägt; sie decken beide Zeitzonen-Halbjahre und die Umstellungswoche ab. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/utils.ts | 7 ++ src/lib/version.test.ts | 14 +++ src/lib/version.ts | 9 ++ src/lib/zeit.test.ts | 196 ++++++++++++++++++++++++++++++++++++++++ src/lib/zeit.ts | 123 +++++++++++++++++++++++++ 5 files changed, 349 insertions(+) create mode 100644 src/lib/utils.ts create mode 100644 src/lib/version.test.ts create mode 100644 src/lib/version.ts create mode 100644 src/lib/zeit.test.ts create mode 100644 src/lib/zeit.ts diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3b367b0 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +/** Klassennamen zusammenfuehren, spaetere Tailwind-Klassen gewinnen. */ +export function cn(...eingaben: ClassValue[]): string { + return twMerge(clsx(eingaben)) +} diff --git a/src/lib/version.test.ts b/src/lib/version.test.ts new file mode 100644 index 0000000..3442a19 --- /dev/null +++ b/src/lib/version.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { VERSION, versionAnzeige } from './version' + +describe('VERSION', () => { + it('kommt aus der package.json und sieht nach Semver aus', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+(?:-[\w.]+)?$/) + }) +}) + +describe('versionAnzeige', () => { + it('stellt ein v voran', () => { + expect(versionAnzeige('0.1.0')).toBe('v0.1.0') + }) +}) diff --git a/src/lib/version.ts b/src/lib/version.ts new file mode 100644 index 0000000..e343e70 --- /dev/null +++ b/src/lib/version.ts @@ -0,0 +1,9 @@ +import paket from '../../package.json' with { type: 'json' } + +/** Die Version steht ausschliesslich in package.json. */ +export const VERSION: string = paket.version + +/** Fuer die Fusszeile: "v0.1.0". */ +export function versionAnzeige(version: string = VERSION): string { + return `v${version}` +} diff --git a/src/lib/zeit.test.ts b/src/lib/zeit.test.ts new file mode 100644 index 0000000..b2493c0 --- /dev/null +++ b/src/lib/zeit.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it } from 'vitest' +import { + alsDatum, + alsDatumUhrzeit, + alsDauer, + alsTagesschluessel, + alsUhrzeit, + ausUhrzeit, + gleicherTag, + minutenSeitMitternacht, + tageDazwischen, + tagesBeginn, + tagesEnde, + wochenBeginn, + wochenTage, + wochentagIndex, +} from './zeit' + +// Winterzeit: Berlin ist UTC+1. Sommerzeit: UTC+2. +const winter = new Date('2026-01-15T12:00:00Z') +const sommer = new Date('2026-07-15T12:00:00Z') + +describe('tagesBeginn', () => { + it('schneidet in der Winterzeit bei 23:00 UTC des Vortags', () => { + expect(tagesBeginn(winter).toISOString()).toBe('2026-01-14T23:00:00.000Z') + }) + + it('schneidet in der Sommerzeit bei 22:00 UTC des Vortags', () => { + expect(tagesBeginn(sommer).toISOString()).toBe('2026-07-14T22:00:00.000Z') + }) + + it('ordnet 22:30 UTC im Winter noch demselben Berliner Tag zu', () => { + // 22:30 UTC = 23:30 Berlin, also derselbe Tag. + expect(tagesBeginn(new Date('2026-01-15T22:30:00Z')).toISOString()).toBe( + '2026-01-14T23:00:00.000Z', + ) + }) + + it('ordnet 23:30 UTC im Winter bereits dem Folgetag zu', () => { + // 23:30 UTC = 00:30 Berlin am 16. + expect(tagesBeginn(new Date('2026-01-15T23:30:00Z')).toISOString()).toBe( + '2026-01-15T23:00:00.000Z', + ) + }) +}) + +describe('tagesEnde', () => { + it('endet eine Millisekunde vor dem naechsten Tagesbeginn', () => { + expect(tagesEnde(winter).getTime()).toBe( + tagesBeginn(new Date('2026-01-16T12:00:00Z')).getTime() - 1, + ) + }) +}) + +describe('wochenBeginn', () => { + it('beginnt am Montag, nicht am Sonntag', () => { + // 2026-01-15 ist ein Donnerstag, der Montag davor ist der 12. + expect(alsTagesschluessel(wochenBeginn(winter))).toBe('2026-01-12') + }) + + it('bleibt am Montag selbst stehen', () => { + const montag = new Date('2026-01-12T09:00:00Z') + expect(alsTagesschluessel(wochenBeginn(montag))).toBe('2026-01-12') + }) + + it('rechnet den Sonntag zur ablaufenden Woche', () => { + const sonntag = new Date('2026-01-18T09:00:00Z') + expect(alsTagesschluessel(wochenBeginn(sonntag))).toBe('2026-01-12') + }) +}) + +describe('wochenTage', () => { + it('liefert sieben Tage von Montag bis Sonntag', () => { + const tage = wochenTage(winter).map((t) => alsTagesschluessel(t)) + expect(tage).toEqual([ + '2026-01-12', + '2026-01-13', + '2026-01-14', + '2026-01-15', + '2026-01-16', + '2026-01-17', + '2026-01-18', + ]) + }) + + it('haelt die sieben Tage auch ueber die Zeitumstellung hinweg', () => { + // Die Sommerzeit beginnt 2026 am 29. Maerz. + const tage = wochenTage(new Date('2026-03-25T12:00:00Z')).map((t) => + alsTagesschluessel(t), + ) + expect(tage).toEqual([ + '2026-03-23', + '2026-03-24', + '2026-03-25', + '2026-03-26', + '2026-03-27', + '2026-03-28', + '2026-03-29', + ]) + }) +}) + +describe('wochentagIndex', () => { + it('zaehlt Montag als 0 und Sonntag als 6', () => { + expect(wochentagIndex(new Date('2026-01-12T12:00:00Z'))).toBe(0) + expect(wochentagIndex(new Date('2026-01-18T12:00:00Z'))).toBe(6) + }) +}) + +describe('tageDazwischen', () => { + it('zaehlt Kalendertage, nicht 24-Stunden-Bloecke', () => { + const spaetAbends = new Date('2026-01-15T22:30:00Z') // 23:30 Berlin + const frueh = new Date('2026-01-16T06:00:00Z') // 07:00 Berlin am Folgetag + expect(tageDazwischen(spaetAbends, frueh)).toBe(1) + }) +}) + +describe('gleicherTag', () => { + it('erkennt denselben Berliner Tag trotz unterschiedlicher UTC-Tage', () => { + expect( + gleicherTag(new Date('2026-01-15T22:30:00Z'), new Date('2026-01-15T08:00:00Z')), + ).toBe(true) + }) + + it('trennt zwei Tage', () => { + expect(gleicherTag(winter, new Date('2026-01-16T12:00:00Z'))).toBe(false) + }) +}) + +describe('minutenSeitMitternacht', () => { + it('rechnet in Berliner Ortszeit', () => { + expect(minutenSeitMitternacht(winter)).toBe(13 * 60) // 12:00 UTC = 13:00 Berlin + expect(minutenSeitMitternacht(sommer)).toBe(14 * 60) // 12:00 UTC = 14:00 Berlin + }) +}) + +describe('alsUhrzeit und ausUhrzeit', () => { + it('wandelt Minuten in eine Uhrzeit', () => { + expect(alsUhrzeit(0)).toBe('00:00') + expect(alsUhrzeit(480)).toBe('08:00') + expect(alsUhrzeit(1020)).toBe('17:00') + expect(alsUhrzeit(1439)).toBe('23:59') + }) + + it('faengt Werte ausserhalb des Tages ab', () => { + expect(alsUhrzeit(1440)).toBe('00:00') + expect(alsUhrzeit(-60)).toBe('23:00') + }) + + it('liest eine Uhrzeit zurueck', () => { + expect(ausUhrzeit('08:00')).toBe(480) + expect(ausUhrzeit('8:05')).toBe(485) + expect(ausUhrzeit(' 23:59 ')).toBe(1439) + }) + + it('weist Unsinn ab', () => { + expect(ausUhrzeit('24:00')).toBeNull() + expect(ausUhrzeit('08:60')).toBeNull() + expect(ausUhrzeit('acht')).toBeNull() + expect(ausUhrzeit('')).toBeNull() + }) + + it('ist in beide Richtungen stimmig', () => { + for (const minuten of [0, 1, 59, 60, 480, 719, 720, 1439]) { + expect(ausUhrzeit(alsUhrzeit(minuten))).toBe(minuten) + } + }) +}) + +describe('alsDauer', () => { + it('schreibt Minuten, Stunden und beides', () => { + expect(alsDauer(45)).toBe('45 min') + expect(alsDauer(60)).toBe('1 h') + expect(alsDauer(90)).toBe('1 h 30 min') + expect(alsDauer(0)).toBe('0 min') + }) + + it('rundet und bleibt bei Unsinn bei null', () => { + expect(alsDauer(89.6)).toBe('1 h 30 min') + expect(alsDauer(-30)).toBe('0 min') + }) +}) + +describe('Anzeigeformate', () => { + it('schreibt das Datum deutsch mit Wochentag', () => { + expect(alsDatum(winter)).toBe('Do., 15.01.2026') + }) + + it('schreibt Datum und Uhrzeit in Berliner Zeit', () => { + expect(alsDatumUhrzeit(sommer)).toBe('15.07.2026, 14:00') + }) + + it('liefert einen sortierbaren Tagesschluessel', () => { + expect(alsTagesschluessel(sommer)).toBe('2026-07-15') + }) +}) diff --git a/src/lib/zeit.ts b/src/lib/zeit.ts new file mode 100644 index 0000000..4d196de --- /dev/null +++ b/src/lib/zeit.ts @@ -0,0 +1,123 @@ +/** + * Zeitrechnung des Planers. + * + * Alle Anzeigen und Tagesgrenzen gelten in Europe/Berlin, unabhaengig davon, + * in welcher Zeitzone der Server laeuft. Reine Logik: die aktuelle Zeit wird + * immer als Parameter hereingereicht, nie hier gelesen. + */ +import { TZDate } from '@date-fns/tz' +import { + addDays, + differenceInCalendarDays, + endOfDay, + format, + startOfDay, + startOfWeek, +} from 'date-fns' +import { de } from 'date-fns/locale' + +export const ZEITZONE = 'Europe/Berlin' + +/** Dasselbe Zeitpunkt, betrachtet in der Zeitzone des Planers. */ +export function inZeitzone(zeitpunkt: Date, zeitzone: string = ZEITZONE): TZDate { + return new TZDate(zeitpunkt, zeitzone) +} + +/** 00:00:00 des Tages, in dem der Zeitpunkt in Europe/Berlin liegt. */ +export function tagesBeginn(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date(startOfDay(inZeitzone(zeitpunkt, zeitzone)).getTime()) +} + +/** 23:59:59.999 desselben Tages. */ +export function tagesEnde(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date(endOfDay(inZeitzone(zeitpunkt, zeitzone)).getTime()) +} + +/** Montag 00:00 der Woche, in der der Zeitpunkt liegt. */ +export function wochenBeginn(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date { + return new Date( + startOfWeek(inZeitzone(zeitpunkt, zeitzone), { weekStartsOn: 1 }).getTime(), + ) +} + +/** Die sieben Tagesanfaenge der Woche, Montag zuerst. */ +export function wochenTage(zeitpunkt: Date, zeitzone: string = ZEITZONE): Date[] { + const montag = inZeitzone(wochenBeginn(zeitpunkt, zeitzone), zeitzone) + return Array.from({ length: 7 }, (_, i) => new Date(addDays(montag, i).getTime())) +} + +/** + * Wochentag als Index 0 = Montag bis 6 = Sonntag. Die Kapazitaetsfelder eines + * Bereichs stehen in dieser Reihenfolge. + */ +export function wochentagIndex(zeitpunkt: Date, zeitzone: string = ZEITZONE): number { + return (inZeitzone(zeitpunkt, zeitzone).getDay() + 6) % 7 +} + +/** Ganze Kalendertage zwischen zwei Zeitpunkten, in Europe/Berlin gezaehlt. */ +export function tageDazwischen( + von: Date, + bis: Date, + zeitzone: string = ZEITZONE, +): number { + return differenceInCalendarDays(inZeitzone(bis, zeitzone), inZeitzone(von, zeitzone)) +} + +/** Liegen beide Zeitpunkte am selben Kalendertag? */ +export function gleicherTag(a: Date, b: Date, zeitzone: string = ZEITZONE): boolean { + return tagesBeginn(a, zeitzone).getTime() === tagesBeginn(b, zeitzone).getTime() +} + +/** Minuten seit Mitternacht, z. B. fuer das Arbeitsfenster eines Bereichs. */ +export function minutenSeitMitternacht( + zeitpunkt: Date, + zeitzone: string = ZEITZONE, +): number { + const lokal = inZeitzone(zeitpunkt, zeitzone) + return lokal.getHours() * 60 + lokal.getMinutes() +} + +/** "08:00" aus 480. */ +export function alsUhrzeit(minutenSeitMitternacht: number): string { + const gesamt = ((minutenSeitMitternacht % 1440) + 1440) % 1440 + const stunden = Math.floor(gesamt / 60) + const minuten = gesamt % 60 + return `${String(stunden).padStart(2, '0')}:${String(minuten).padStart(2, '0')}` +} + +/** 480 aus "08:00". Gibt null zurueck, wenn die Eingabe keine Uhrzeit ist. */ +export function ausUhrzeit(text: string): number | null { + const treffer = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(text.trim()) + if (!treffer) return null + return Number(treffer[1]) * 60 + Number(treffer[2]) +} + +/** "1 h 30 min", "45 min", "2 h". Fuer Aufwaende und Kapazitaeten. */ +export function alsDauer(minuten: number): string { + const gerundet = Math.max(0, Math.round(minuten)) + const stunden = Math.floor(gerundet / 60) + const rest = gerundet % 60 + if (stunden === 0) return `${rest} min` + if (rest === 0) return `${stunden} h` + return `${stunden} h ${rest} min` +} + +/** "Mo., 10.09.2026" */ +export function alsDatum(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'EEE, dd.MM.yyyy', { locale: de }) +} + +/** "10.09.2026, 14:30" */ +export function alsDatumUhrzeit(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'dd.MM.yyyy, HH:mm', { locale: de }) +} + +/** "14:30" */ +export function alsUhrzeitAnzeige(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'HH:mm', { locale: de }) +} + +/** ISO-Datum "2026-09-10" in Europe/Berlin, taugt als Schluessel je Tag. */ +export function alsTagesschluessel(zeitpunkt: Date, zeitzone: string = ZEITZONE): string { + return format(inZeitzone(zeitpunkt, zeitzone), 'yyyy-MM-dd') +} From 4ff67c15c269c5c6da6c885d470f4ec27802b6c3 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:02:07 +0200 Subject: [PATCH 05/19] feat(oberflaeche): Grundlayout, Navigation und Farbschema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deutsche Oberfläche, Hell und Dunkel über next-themes, Navigation mit Rollenfilter und die shadcn-Primitiven, die M1 braucht. Der Farbschema-Knopf kommt ohne Effekt und ohne Zustand aus: welches Symbol gilt, entscheidet die dark-Klasse und damit CSS. next-themes setzt sie schon vor der Hydratation, deshalb blitzt nichts auf — und die React-Compiler-Regel gegen setState im Effekt bleibt eingehalten. Farben als OKLCH, damit Hell und Dunkel dieselbe wahrgenommene Helligkeit halten. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/globals.css | 90 ++++++++++++++++++++ src/app/keine-berechtigung/page.tsx | 22 +++++ src/app/layout.tsx | 36 ++++++++ src/app/not-found.tsx | 20 +++++ src/components/farbschema-anbieter.tsx | 20 +++++ src/components/kopfleiste.tsx | 109 +++++++++++++++++++++++++ src/components/ui/button.tsx | 47 +++++++++++ src/components/ui/card.tsx | 40 +++++++++ src/components/ui/input.tsx | 20 +++++ src/components/ui/label.tsx | 19 +++++ src/components/ui/separator.tsx | 26 ++++++ src/lib/rollen.test.ts | 31 +++++++ src/lib/rollen.ts | 25 ++++++ 13 files changed, 505 insertions(+) create mode 100644 src/app/globals.css create mode 100644 src/app/keine-berechtigung/page.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/farbschema-anbieter.tsx create mode 100644 src/components/kopfleiste.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/lib/rollen.test.ts create mode 100644 src/lib/rollen.ts diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..24f0a63 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,90 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +/* + * Farben als OKLCH, damit Hell und Dunkel dieselbe wahrgenommene Helligkeit + * halten. Die Bereichsfarben kommen aus der Datenbank und werden zur Laufzeit + * gesetzt, nicht hier. + */ +:root { + --radius: 0.625rem; + + --hintergrund: oklch(0.99 0.002 250); + --vordergrund: oklch(0.21 0.02 260); + --karte: oklch(1 0 0); + --karte-vordergrund: oklch(0.21 0.02 260); + --gedaempft: oklch(0.96 0.004 250); + --gedaempft-vordergrund: oklch(0.5 0.015 260); + --rand: oklch(0.91 0.006 255); + --eingabe: oklch(0.91 0.006 255); + --ring: oklch(0.55 0.16 258); + + --betont: oklch(0.52 0.17 258); + --betont-vordergrund: oklch(0.99 0.002 250); + --gefahr: oklch(0.55 0.21 27); + --gefahr-vordergrund: oklch(0.99 0.002 250); + --erfolg: oklch(0.6 0.13 155); + --warnung: oklch(0.7 0.15 75); +} + +.dark { + --hintergrund: oklch(0.17 0.015 262); + --vordergrund: oklch(0.95 0.005 250); + --karte: oklch(0.21 0.018 262); + --karte-vordergrund: oklch(0.95 0.005 250); + --gedaempft: oklch(0.26 0.018 262); + --gedaempft-vordergrund: oklch(0.7 0.015 258); + --rand: oklch(0.31 0.018 262); + --eingabe: oklch(0.31 0.018 262); + --ring: oklch(0.62 0.15 258); + + --betont: oklch(0.62 0.16 258); + --betont-vordergrund: oklch(0.17 0.015 262); + --gefahr: oklch(0.62 0.19 25); + --gefahr-vordergrund: oklch(0.98 0.002 250); + --erfolg: oklch(0.68 0.12 155); + --warnung: oklch(0.78 0.13 75); +} + +@theme inline { + --color-hintergrund: var(--hintergrund); + --color-vordergrund: var(--vordergrund); + --color-karte: var(--karte); + --color-karte-vordergrund: var(--karte-vordergrund); + --color-gedaempft: var(--gedaempft); + --color-gedaempft-vordergrund: var(--gedaempft-vordergrund); + --color-rand: var(--rand); + --color-eingabe: var(--eingabe); + --color-ring: var(--ring); + --color-betont: var(--betont); + --color-betont-vordergrund: var(--betont-vordergrund); + --color-gefahr: var(--gefahr); + --color-gefahr-vordergrund: var(--gefahr-vordergrund); + --color-erfolg: var(--erfolg); + --color-warnung: var(--warnung); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +@layer base { + * { + @apply border-rand outline-ring/50; + } + + body { + @apply bg-hintergrund text-vordergrund; + font-feature-settings: + 'liga' 1, + 'calt' 1; + } + + /* Wer mit der Tastatur bedient, muss sehen, wo er ist. */ + :focus-visible { + @apply outline-ring outline-2 outline-offset-2; + } +} diff --git a/src/app/keine-berechtigung/page.tsx b/src/app/keine-berechtigung/page.tsx new file mode 100644 index 0000000..bba52ca --- /dev/null +++ b/src/app/keine-berechtigung/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { ShieldAlert } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export const metadata: Metadata = { title: 'Keine Berechtigung' } + +export default function KeineBerechtigungSeite() { + return ( +
+ +

Dazu fehlt dir die Berechtigung

+

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

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

Fehler 404

+

Diese Seite gibt es nicht

+

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

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

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

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

+} + +function CardFuss({ className, ...rest }: React.ComponentProps<'div'>) { + return
+} + +export { Card, CardKopf, CardTitel, CardBeschreibung, CardInhalt, CardFuss } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..450e8bc --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import { cn } from '@/lib/utils' + +function Input({ className, type, ...rest }: React.ComponentProps<'input'>) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..b8a8beb --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +'use client' + +import * as LabelPrimitive from '@radix-ui/react-label' +import { cn } from '@/lib/utils' + +function Label({ className, ...rest }: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..6110590 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +'use client' + +import * as SeparatorPrimitive from '@radix-ui/react-separator' +import { cn } from '@/lib/utils' + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...rest +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/lib/rollen.test.ts b/src/lib/rollen.test.ts new file mode 100644 index 0000000..07c6ae5 --- /dev/null +++ b/src/lib/rollen.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { ROLLEN_NAME, istAdmin, rolleReichtAus } from './rollen' + +describe('rolleReichtAus', () => { + it('laesst jede Rolle das, was sie selbst ist', () => { + expect(rolleReichtAus('BENUTZER', 'BENUTZER')).toBe(true) + expect(rolleReichtAus('ADMIN', 'ADMIN')).toBe(true) + }) + + it('laesst die Verwaltung auch alles Niedrigere', () => { + expect(rolleReichtAus('ADMIN', 'BENUTZER')).toBe(true) + }) + + it('laesst einen Benutzer nicht in die Verwaltung', () => { + expect(rolleReichtAus('BENUTZER', 'ADMIN')).toBe(false) + }) +}) + +describe('istAdmin', () => { + it('erkennt nur die Verwaltung', () => { + expect(istAdmin('ADMIN')).toBe(true) + expect(istAdmin('BENUTZER')).toBe(false) + }) +}) + +describe('ROLLEN_NAME', () => { + it('benennt jede Rolle deutsch', () => { + expect(ROLLEN_NAME.BENUTZER).toBe('Benutzer') + expect(ROLLEN_NAME.ADMIN).toBe('Verwaltung') + }) +}) diff --git a/src/lib/rollen.ts b/src/lib/rollen.ts new file mode 100644 index 0000000..3cfa889 --- /dev/null +++ b/src/lib/rollen.ts @@ -0,0 +1,25 @@ +import type { Rolle } from '@/generated/prisma/enums' + +/** + * Rangfolge der Rollen. Reine Logik, damit Seite, Server Action und + * Oberflaeche dieselbe Antwort geben. + */ + +const RANG: Record = { + BENUTZER: 1, + ADMIN: 2, +} + +export const ROLLEN_NAME: Record = { + BENUTZER: 'Benutzer', + ADMIN: 'Verwaltung', +} + +/** Reicht die vorhandene Rolle fuer das Verlangte? */ +export function rolleReichtAus(vorhanden: Rolle, verlangt: Rolle): boolean { + return RANG[vorhanden] >= RANG[verlangt] +} + +export function istAdmin(rolle: Rolle): boolean { + return rolleReichtAus(rolle, 'ADMIN') +} From cc013fd6a1fc6e71fb946b3463ff7d92551ffadc Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:02:07 +0200 Subject: [PATCH 06/19] feat(anmeldung): OIDC, Passkeys und Passwort mit Auth.js v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei Wege hinein, je Instanz zuschaltbar: generischer OIDC-Anbieter (Entra ID beziehungsweise authentik), Passkeys und wahlweise E-Mail mit Passwort. Entscheidungen: - Passkeys laufen direkt über SimpleWebAuthn 14, nicht über den experimentellen WebAuthn-Provider von Auth.js, der noch auf Fassung 9 festgelegt ist. - Eigener Prisma-Adapter, weil der mitgelieferte fest auf prisma.user und prisma.account zugreift, unsere Modelle aber Benutzer und Konto heißen. - Sitzungen als JWT, weil Credentials-Provider das verlangen. Damit eine Sperre trotzdem sofort wirkt, liest der session-Callback Rolle und Status bei jeder Anfrage frisch aus der Datenbank. Die Tabellen sitzung und verifizierungs_token entfallen deshalb wieder. - allowDangerousEmailAccountLinking ist bewusst an: je Instanz gibt es genau einen vertrauenswürdigen Anbieter, der die Adresse selbst prüft. Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, damit sich nicht abfragen lässt, wer hier ein Konto hat. Die WebAuthn-Herausforderung wird nach einem Versuch verworfen. Co-Authored-By: Claude Opus 5 (1M context) --- .../migration.sql | 32 -- prisma/schema.prisma | 22 -- src/app/anmelden/anmelde-formular.tsx | 193 ++++++++++++ src/app/anmelden/page.tsx | 56 ++++ src/app/api/auth/[...nextauth]/route.ts | 3 + src/app/api/passkey/anmeldung/route.ts | 33 ++ src/app/api/passkey/registrierung/route.ts | 71 +++++ src/lib/passwort.test.ts | 59 ++++ src/lib/passwort.ts | 37 +++ src/server/auth/adapter.ts | 123 ++++++++ src/server/auth/config.ts | 171 +++++++++++ src/server/auth/index.ts | 10 + src/server/auth/passkey.ts | 281 ++++++++++++++++++ src/server/auth/passwort.ts | 30 ++ src/server/berechtigung.ts | 74 +++++ src/types/next-auth.d.ts | 20 ++ 16 files changed, 1161 insertions(+), 54 deletions(-) rename prisma/migrations/{20260910103056_fundament => 20260910104400_fundament}/migration.sql (82%) create mode 100644 src/app/anmelden/anmelde-formular.tsx create mode 100644 src/app/anmelden/page.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/passkey/anmeldung/route.ts create mode 100644 src/app/api/passkey/registrierung/route.ts create mode 100644 src/lib/passwort.test.ts create mode 100644 src/lib/passwort.ts create mode 100644 src/server/auth/adapter.ts create mode 100644 src/server/auth/config.ts create mode 100644 src/server/auth/index.ts create mode 100644 src/server/auth/passkey.ts create mode 100644 src/server/auth/passwort.ts create mode 100644 src/server/berechtigung.ts create mode 100644 src/types/next-auth.d.ts diff --git a/prisma/migrations/20260910103056_fundament/migration.sql b/prisma/migrations/20260910104400_fundament/migration.sql similarity index 82% rename from prisma/migrations/20260910103056_fundament/migration.sql rename to prisma/migrations/20260910104400_fundament/migration.sql index 59096f8..5db03f3 100644 --- a/prisma/migrations/20260910103056_fundament/migration.sql +++ b/prisma/migrations/20260910104400_fundament/migration.sql @@ -36,23 +36,6 @@ CREATE TABLE "konto" ( CONSTRAINT "konto_pkey" PRIMARY KEY ("id") ); --- CreateTable -CREATE TABLE "sitzung" ( - "id" TEXT NOT NULL, - "sessionToken" TEXT NOT NULL, - "benutzerId" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "sitzung_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "verifizierungs_token" ( - "identifier" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL -); - -- CreateTable CREATE TABLE "passkey" ( "id" TEXT NOT NULL, @@ -130,18 +113,6 @@ CREATE INDEX "konto_benutzerId_idx" ON "konto"("benutzerId"); -- CreateIndex CREATE UNIQUE INDEX "konto_provider_providerAccountId_key" ON "konto"("provider", "providerAccountId"); --- CreateIndex -CREATE UNIQUE INDEX "sitzung_sessionToken_key" ON "sitzung"("sessionToken"); - --- CreateIndex -CREATE INDEX "sitzung_benutzerId_idx" ON "sitzung"("benutzerId"); - --- CreateIndex -CREATE UNIQUE INDEX "verifizierungs_token_token_key" ON "verifizierungs_token"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "verifizierungs_token_identifier_token_key" ON "verifizierungs_token"("identifier", "token"); - -- CreateIndex CREATE UNIQUE INDEX "passkey_credentialId_key" ON "passkey"("credentialId"); @@ -169,9 +140,6 @@ CREATE INDEX "audit_eintrag_benutzerId_idx" ON "audit_eintrag"("benutzerId"); -- AddForeignKey ALTER TABLE "konto" ADD CONSTRAINT "konto_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "sitzung" ADD CONSTRAINT "sitzung_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "passkey" ADD CONSTRAINT "passkey_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 313b933..da39cac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,7 +32,6 @@ model Benutzer { aktualisiertAm DateTime @updatedAt konten Konto[] - sitzungen Sitzung[] passkeys Passkey[] audit AuditEintrag[] @@ -62,27 +61,6 @@ model Konto { @@map("konto") } -model Sitzung { - id String @id @default(cuid()) - sessionToken String @unique - benutzerId String - expires DateTime - - benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) - - @@index([benutzerId]) - @@map("sitzung") -} - -model VerifizierungsToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) - @@map("verifizierungs_token") -} - /// Ein registrierter Passkey (WebAuthn) eines Benutzers. model Passkey { id String @id @default(cuid()) diff --git a/src/app/anmelden/anmelde-formular.tsx b/src/app/anmelden/anmelde-formular.tsx new file mode 100644 index 0000000..90f4014 --- /dev/null +++ b/src/app/anmelden/anmelde-formular.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { startAuthentication } from '@simplewebauthn/browser' +import { Fingerprint, KeyRound, Loader2 } from 'lucide-react' +import { signIn } from 'next-auth/react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Card, CardInhalt } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Separator } from '@/components/ui/separator' + +type Eigenschaften = { + ziel: string + oidcAktiv: boolean + oidcName: string + passwortAktiv: boolean + fehlerAusUrl?: string | undefined +} + +/** + * Bewusst dieselbe Meldung für „gibt es nicht", „falsches Passwort" und + * „gesperrt". Sonst lässt sich abfragen, wer hier ein Konto hat. + */ +const ABGELEHNT = 'Anmeldung nicht möglich. Bitte Angaben prüfen.' + +export function AnmeldeFormular({ + ziel, + oidcAktiv, + oidcName, + passwortAktiv, + fehlerAusUrl, +}: Eigenschaften) { + const router = useRouter() + const [email, setEmail] = useState('') + const [passwort, setPasswort] = useState('') + const [laeuft, setLaeuft] = useState(null) + + useEffect(() => { + if (fehlerAusUrl) toast.error(ABGELEHNT) + }, [fehlerAusUrl]) + + async function mitOidc() { + setLaeuft('oidc') + await signIn('oidc', { callbackUrl: ziel }) + } + + async function mitPasskey(ereignis: React.FormEvent) { + ereignis.preventDefault() + if (!email.trim()) { + toast.error('Bitte zuerst die E-Mail-Adresse eintragen.') + return + } + + setLaeuft('passkey') + try { + const antwortOptionen = await fetch('/api/passkey/anmeldung', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + if (!antwortOptionen.ok) { + toast.error(ABGELEHNT) + return + } + + const optionen = (await antwortOptionen.json()) as Parameters< + typeof startAuthentication + >[0]['optionsJSON'] + + const antwort = await startAuthentication({ optionsJSON: optionen }) + + const ergebnis = await signIn('passkey', { + email, + antwort: JSON.stringify(antwort), + redirect: false, + }) + + if (ergebnis?.error) { + toast.error(ABGELEHNT) + return + } + router.push(ziel) + router.refresh() + } catch (fehler) { + // Bricht der Benutzer den Systemdialog ab, ist das kein Fehler. + if (fehler instanceof Error && fehler.name === 'NotAllowedError') return + toast.error('Die Anmeldung mit Passkey hat nicht geklappt.') + } finally { + setLaeuft(null) + } + } + + async function mitPasswort(ereignis: React.FormEvent) { + ereignis.preventDefault() + setLaeuft('passwort') + try { + const ergebnis = await signIn('passwort', { + email, + passwort, + redirect: false, + }) + if (ergebnis?.error) { + toast.error(ABGELEHNT) + return + } + router.push(ziel) + router.refresh() + } finally { + setLaeuft(null) + } + } + + const gesperrt = laeuft !== null + + return ( + + + {oidcAktiv && ( + <> + +
+ + oder + +
+ + )} + +
void mitPasskey(e)}> +
+ + setEmail(e.target.value)} + disabled={gesperrt} + /> +
+ + +
+ + {passwortAktiv && ( +
void mitPasswort(e)}> +
+ + setPasswort(e.target.value)} + disabled={gesperrt} + /> +
+ +
+ )} +
+
+ ) +} diff --git a/src/app/anmelden/page.tsx b/src/app/anmelden/page.tsx new file mode 100644 index 0000000..3d2683b --- /dev/null +++ b/src/app/anmelden/page.tsx @@ -0,0 +1,56 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' +import { AnmeldeFormular } from '@/app/anmelden/anmelde-formular' +import { INSTANZ_NAME } from '@/lib/instanz' +import { versionAnzeige } from '@/lib/version' +import { angemeldet } from '@/server/berechtigung' +import { aktuelleInstanz } from '@/server/instanz' +import { umgebung } from '@/server/umgebung' + +export const metadata: Metadata = { title: 'Anmelden' } + +/** Nur Pfade innerhalb der App sind als Ziel erlaubt – keine fremden Adressen. */ +function sicheresZiel(weiter: string | undefined): string { + if (!weiter) return '/' + if (!weiter.startsWith('/') || weiter.startsWith('//')) return '/' + return weiter +} + +export default async function AnmeldeSeite({ + searchParams, +}: { + searchParams: Promise<{ weiter?: string; error?: string }> +}) { + const parameter = await searchParams + const ziel = sicheresZiel(parameter.weiter) + + if (await angemeldet()) { + redirect(ziel) + } + + const einst = umgebung() + const instanz = aktuelleInstanz() + + return ( +
+
+
+

Planer

+

+ {INSTANZ_NAME[instanz]} +

+
+ + +
+ +

{versionAnzeige()}

+
+ ) +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7ed0211 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@/server/auth' + +export const { GET, POST } = handlers diff --git a/src/app/api/passkey/anmeldung/route.ts b/src/app/api/passkey/anmeldung/route.ts new file mode 100644 index 0000000..cbcc186 --- /dev/null +++ b/src/app/api/passkey/anmeldung/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { anmeldeOptionen } from '@/server/auth/passkey' + +export const dynamic = 'force-dynamic' + +const eingabe = z.object({ email: z.email() }) + +/** + * Liefert die WebAuthn-Optionen fuer die Anmeldung. + * + * Antwortet fuer unbekannte Adressen genauso wie fuer bekannte – sonst + * liesse sich hier abfragen, wer ein Konto hat. + */ +export async function POST(anfrage: Request) { + let inhalt: unknown + try { + inhalt = await anfrage.json() + } catch { + return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 }) + } + + const gelesen = eingabe.safeParse(inhalt) + if (!gelesen.success) { + return NextResponse.json( + { fehler: 'Bitte eine gültige E-Mail-Adresse angeben.' }, + { status: 400 }, + ) + } + + const optionen = await anmeldeOptionen(gelesen.data.email) + return NextResponse.json(optionen, { headers: { 'Cache-Control': 'no-store' } }) +} diff --git a/src/app/api/passkey/registrierung/route.ts b/src/app/api/passkey/registrierung/route.ts new file mode 100644 index 0000000..392167f --- /dev/null +++ b/src/app/api/passkey/registrierung/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' +import { z } from 'zod' +import type { RegistrationResponseJSON } from '@simplewebauthn/server' +import { auditSchreiben } from '@/server/audit' +import { registrierungPruefen, registrierungsOptionen } from '@/server/auth/passkey' +import { verlangeAnmeldungFuerApi } from '@/server/berechtigung' + +export const dynamic = 'force-dynamic' + +const abschluss = z.object({ + bezeichnung: z.string().trim().min(1).max(60), + antwort: z.looseObject({ id: z.string().min(1) }), +}) + +/** Schritt 1: Optionen fuer einen neuen Passkey des angemeldeten Benutzers. */ +export async function GET() { + const benutzer = await verlangeAnmeldungFuerApi() + if (!benutzer) { + return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 }) + } + + const optionen = await registrierungsOptionen(benutzer.id) + return NextResponse.json(optionen, { headers: { 'Cache-Control': 'no-store' } }) +} + +/** Schritt 2: Antwort des Browsers pruefen und den Passkey speichern. */ +export async function POST(anfrage: Request) { + const benutzer = await verlangeAnmeldungFuerApi() + if (!benutzer) { + return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 }) + } + + let inhalt: unknown + try { + inhalt = await anfrage.json() + } catch { + return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 }) + } + + const gelesen = abschluss.safeParse(inhalt) + if (!gelesen.success) { + return NextResponse.json( + { fehler: 'Bitte einen Namen für das Gerät angeben.' }, + { status: 400 }, + ) + } + + try { + const ergebnis = await registrierungPruefen( + benutzer.id, + gelesen.data.antwort as unknown as RegistrationResponseJSON, + gelesen.data.bezeichnung, + ) + + await auditSchreiben({ + akteur: { id: benutzer.id, email: benutzer.email }, + aktion: 'passkey.angelegt', + objektTyp: 'Passkey', + objektId: ergebnis.passkeyId, + details: { bezeichnung: ergebnis.bezeichnung }, + }) + + return NextResponse.json({ ok: true, bezeichnung: ergebnis.bezeichnung }) + } catch (fehler) { + const meldung = + fehler instanceof Error + ? fehler.message + : 'Der Passkey konnte nicht gespeichert werden.' + return NextResponse.json({ fehler: meldung }, { status: 400 }) + } +} diff --git a/src/lib/passwort.test.ts b/src/lib/passwort.test.ts new file mode 100644 index 0000000..28596be --- /dev/null +++ b/src/lib/passwort.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { + PASSWORT_HOECHSTLAENGE, + PASSWORT_MINDESTLAENGE, + passwortIstGut, + passwortMaengel, +} from './passwort' + +describe('passwortMaengel', () => { + it('nimmt ein ausreichend langes Passwort an', () => { + expect(passwortMaengel('richtiges pferd batterie klammer')).toEqual([]) + }) + + it('bemaengelt zu kurze Passwoerter', () => { + expect(passwortMaengel('kurz')).toContain( + `Mindestens ${PASSWORT_MINDESTLAENGE} Zeichen.`, + ) + }) + + it('setzt die Grenze bei genau der Mindestlaenge', () => { + expect( + passwortMaengel('a'.repeat(PASSWORT_MINDESTLAENGE - 1)).length, + ).toBeGreaterThan(0) + // Genau die Mindestlaenge, aber ein einziges wiederholtes Zeichen. + expect(passwortMaengel('a'.repeat(PASSWORT_MINDESTLAENGE))).toEqual([ + 'Darf nicht aus einem einzigen wiederholten Zeichen bestehen.', + ]) + expect(passwortMaengel('abcdefghijkl')).toEqual([]) + }) + + it('weist ueberlange Eingaben ab, damit das Hashen begrenzt bleibt', () => { + expect(passwortMaengel('a'.repeat(PASSWORT_HOECHSTLAENGE + 1))).toContain( + `Höchstens ${PASSWORT_HOECHSTLAENGE} Zeichen.`, + ) + }) + + it('weist reine Leerzeichen ab', () => { + expect(passwortMaengel(' '.repeat(20))).toContain( + 'Darf nicht nur aus Leerzeichen bestehen.', + ) + }) + + it('weist ein einzelnes wiederholtes Zeichen ab', () => { + expect(passwortMaengel('xxxxxxxxxxxxxxxx')).toContain( + 'Darf nicht aus einem einzigen wiederholten Zeichen bestehen.', + ) + }) + + it('meldet mehrere Maengel gleichzeitig', () => { + expect(passwortMaengel('aaa').length).toBe(2) + }) +}) + +describe('passwortIstGut', () => { + it('fasst das Ergebnis zusammen', () => { + expect(passwortIstGut('richtiges pferd batterie')).toBe(true) + expect(passwortIstGut('kurz')).toBe(false) + }) +}) diff --git a/src/lib/passwort.ts b/src/lib/passwort.ts new file mode 100644 index 0000000..67ac1cc --- /dev/null +++ b/src/lib/passwort.ts @@ -0,0 +1,37 @@ +/** + * Regeln fuer Passwoerter. Reine Logik, damit sie in Formular und Server + * Action dieselbe Antwort gibt. + */ + +export const PASSWORT_MINDESTLAENGE = 12 +export const PASSWORT_HOECHSTLAENGE = 200 + +/** + * Prueft ein Passwort und gibt alle Verstoesse zurueck. Eine leere Liste + * heisst: in Ordnung. + * + * Bewusst keine Pflicht zu Sonderzeichen – Laenge traegt mehr zur Sicherheit + * bei als erzwungene Zeichenklassen, die zu "Passwort1!" fuehren. + */ +export function passwortMaengel(passwort: string): string[] { + const maengel: string[] = [] + + if (passwort.length < PASSWORT_MINDESTLAENGE) { + maengel.push(`Mindestens ${PASSWORT_MINDESTLAENGE} Zeichen.`) + } + if (passwort.length > PASSWORT_HOECHSTLAENGE) { + maengel.push(`Höchstens ${PASSWORT_HOECHSTLAENGE} Zeichen.`) + } + if (passwort.trim().length === 0) { + maengel.push('Darf nicht nur aus Leerzeichen bestehen.') + } + if (/^(.)\1*$/.test(passwort) && passwort.length > 0) { + maengel.push('Darf nicht aus einem einzigen wiederholten Zeichen bestehen.') + } + + return maengel +} + +export function passwortIstGut(passwort: string): boolean { + return passwortMaengel(passwort).length === 0 +} diff --git a/src/server/auth/adapter.ts b/src/server/auth/adapter.ts new file mode 100644 index 0000000..62317e5 --- /dev/null +++ b/src/server/auth/adapter.ts @@ -0,0 +1,123 @@ +import 'server-only' +import type { Adapter, AdapterAccount, AdapterUser } from 'next-auth/adapters' +import type { PrismaClient } from '@/generated/prisma/client' + +/** + * Adapter fuer Auth.js auf unsere deutschen Modellnamen. + * + * Der mitgelieferte @auth/prisma-adapter greift fest auf prisma.user, + * prisma.account und prisma.session zu. Unsere Modelle heissen Benutzer und + * Konto, deshalb dieser schmale Zwischenschritt. + * + * Sitzungen fuehrt Auth.js hier als JWT – das verlangt der Credentials-Provider. + * Die Sitzungsmethoden des Adapters bleiben daher bewusst unbesetzt. + */ + +type BenutzerZeile = { + id: string + email: string + name: string | null + bildUrl: string | null + emailGeprueft: Date | null +} + +const AUSWAHL = { + id: true, + email: true, + name: true, + bildUrl: true, + emailGeprueft: true, +} as const + +function alsAdapterBenutzer(zeile: BenutzerZeile): AdapterUser { + return { + id: zeile.id, + email: zeile.email, + name: zeile.name, + image: zeile.bildUrl, + emailVerified: zeile.emailGeprueft, + } +} + +export function prismaAdapter(db: PrismaClient): Adapter { + return { + async createUser(daten) { + // Neue Benutzer entstehen ueber OIDC. Die Rolle bleibt der Standard; + // hochgestuft wird nur von Hand in der Verwaltung. + const zeile = await db.benutzer.create({ + data: { + email: daten.email, + name: daten.name ?? null, + bildUrl: daten.image ?? null, + emailGeprueft: daten.emailVerified ?? null, + }, + select: AUSWAHL, + }) + return alsAdapterBenutzer(zeile) + }, + + async getUser(id) { + const zeile = await db.benutzer.findUnique({ where: { id }, select: AUSWAHL }) + return zeile ? alsAdapterBenutzer(zeile) : null + }, + + async getUserByEmail(email) { + const zeile = await db.benutzer.findUnique({ where: { email }, select: AUSWAHL }) + return zeile ? alsAdapterBenutzer(zeile) : null + }, + + async getUserByAccount({ provider, providerAccountId }) { + const konto = await db.konto.findUnique({ + where: { provider_providerAccountId: { provider, providerAccountId } }, + select: { benutzer: { select: AUSWAHL } }, + }) + return konto ? alsAdapterBenutzer(konto.benutzer) : null + }, + + async updateUser(daten) { + if (!daten.id) throw new Error('updateUser ohne id aufgerufen.') + const zeile = await db.benutzer.update({ + where: { id: daten.id }, + data: { + ...(daten.email === undefined ? {} : { email: daten.email }), + ...(daten.name === undefined ? {} : { name: daten.name }), + ...(daten.image === undefined ? {} : { bildUrl: daten.image }), + ...(daten.emailVerified === undefined + ? {} + : { emailGeprueft: daten.emailVerified }), + }, + select: AUSWAHL, + }) + return alsAdapterBenutzer(zeile) + }, + + async deleteUser(id) { + await db.benutzer.delete({ where: { id } }) + }, + + async linkAccount(konto: AdapterAccount) { + await db.konto.create({ + data: { + benutzerId: konto.userId, + type: konto.type, + provider: konto.provider, + providerAccountId: konto.providerAccountId, + refresh_token: konto.refresh_token ?? null, + access_token: konto.access_token ?? null, + expires_at: konto.expires_at ?? null, + token_type: konto.token_type ?? null, + scope: konto.scope ?? null, + id_token: konto.id_token ?? null, + session_state: + typeof konto.session_state === 'string' ? konto.session_state : null, + }, + }) + }, + + async unlinkAccount({ provider, providerAccountId }) { + await db.konto.delete({ + where: { provider_providerAccountId: { provider, providerAccountId } }, + }) + }, + } +} diff --git a/src/server/auth/config.ts b/src/server/auth/config.ts new file mode 100644 index 0000000..8a1a3b8 --- /dev/null +++ b/src/server/auth/config.ts @@ -0,0 +1,171 @@ +import 'server-only' +import type { NextAuthConfig } from 'next-auth' +import Credentials from 'next-auth/providers/credentials' +import { z } from 'zod' +import type { Rolle } from '@/generated/prisma/enums' +import { db } from '@/server/db' +import { prismaAdapter } from '@/server/auth/adapter' +import { passwortStimmt } from '@/server/auth/passwort' +import { passkeyAnmeldungPruefen } from '@/server/auth/passkey' +import { umgebung } from '@/server/umgebung' + +const zugangsdaten = z.object({ + email: z.email(), + passwort: z.string().min(1).max(200), +}) + +const passkeyEingabe = z.object({ + email: z.email(), + antwort: z.string().min(1).max(20_000), +}) + +/** Anzeigename fuer den OIDC-Knopf, mit brauchbarem Rueckfallwert. */ +function oidcName(): string { + return umgebung().OIDC_NAME || 'Unternehmensanmeldung' +} + +function providerZusammenstellen(): NextAuthConfig['providers'] { + const einst = umgebung() + const provider: NextAuthConfig['providers'] = [] + + if (einst.oidcAktiv) { + provider.push({ + id: 'oidc', + name: oidcName(), + type: 'oidc', + issuer: einst.OIDC_ISSUER, + clientId: einst.OIDC_CLIENT_ID, + clientSecret: einst.OIDC_CLIENT_SECRET, + // Je Instanz gibt es genau einen vertrauenswuerdigen Anbieter, der die + // E-Mail-Adresse selbst prueft. Deshalb darf ein bereits angelegter + // Benutzer beim ersten OIDC-Login mit derselben Adresse verknuepft + // werden, statt an OAuthAccountNotLinked zu scheitern. + allowDangerousEmailAccountLinking: true, + }) + } + + // Passkeys laufen ueber SimpleWebAuthn, nicht ueber den experimentellen + // WebAuthn-Provider von Auth.js. + provider.push( + Credentials({ + id: 'passkey', + name: 'Passkey', + credentials: { + email: { label: 'E-Mail', type: 'email' }, + antwort: { label: 'Antwort', type: 'text' }, + }, + authorize: async (eingabe) => { + const gelesen = passkeyEingabe.safeParse(eingabe) + if (!gelesen.success) return null + return passkeyAnmeldungPruefen(gelesen.data.email, gelesen.data.antwort) + }, + }), + ) + + if (einst.ANMELDUNG_PASSWORT) { + provider.push( + Credentials({ + id: 'passwort', + name: 'E-Mail und Passwort', + credentials: { + email: { label: 'E-Mail', type: 'email' }, + passwort: { label: 'Passwort', type: 'password' }, + }, + authorize: async (eingabe) => { + const gelesen = zugangsdaten.safeParse(eingabe) + if (!gelesen.success) return null + + const benutzer = await db.benutzer.findUnique({ + where: { email: gelesen.data.email.toLowerCase() }, + select: { + id: true, + email: true, + name: true, + bildUrl: true, + aktiv: true, + passwortHash: true, + }, + }) + + const stimmt = await passwortStimmt( + gelesen.data.passwort, + benutzer?.passwortHash ?? null, + ) + // Gleiche Antwort fuer "gibt es nicht", "falsches Passwort" und + // "gesperrt" – sonst laesst sich abfragen, wer ein Konto hat. + if (!benutzer || !benutzer.aktiv || !stimmt) return null + + return { + id: benutzer.id, + email: benutzer.email, + name: benutzer.name, + image: benutzer.bildUrl, + } + }, + }), + ) + } + + return provider +} + +export function authKonfiguration(): NextAuthConfig { + return { + adapter: prismaAdapter(db), + // Credentials-Provider verlangen JWT. Damit eine Sperre trotzdem sofort + // wirkt, wird die Rolle bei jeder Anfrage frisch aus der Datenbank + // gelesen (siehe session-Callback). + session: { strategy: 'jwt', maxAge: 60 * 60 * 24 * 30 }, + trustHost: umgebung().AUTH_TRUST_HOST, + pages: { + signIn: '/anmelden', + error: '/anmelden', + }, + callbacks: { + signIn: async ({ user }) => { + if (!user.email) return false + const benutzer = await db.benutzer.findUnique({ + where: { email: user.email.toLowerCase() }, + select: { aktiv: true }, + }) + // Unbekannte Adressen legt der Adapter ueber OIDC neu an; gesperrte + // Konten kommen nicht durch. + return benutzer ? benutzer.aktiv : true + }, + + jwt: ({ token, user }) => { + if (user?.id) token.sub = user.id + return token + }, + + session: async ({ session, token }) => { + if (!token.sub) return session + const benutzer = await db.benutzer.findUnique({ + where: { id: token.sub }, + select: { + id: true, + email: true, + name: true, + bildUrl: true, + rolle: true, + aktiv: true, + }, + }) + if (!benutzer?.aktiv) { + // Gesperrt oder geloescht: Sitzung sofort entwerten. + return { ...session, user: undefined } as unknown as typeof session + } + session.user = { + ...session.user, + id: benutzer.id, + email: benutzer.email, + name: benutzer.name, + image: benutzer.bildUrl, + rolle: benutzer.rolle satisfies Rolle, + } + return session + }, + }, + providers: providerZusammenstellen(), + } +} diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts new file mode 100644 index 0000000..b36beeb --- /dev/null +++ b/src/server/auth/index.ts @@ -0,0 +1,10 @@ +import 'server-only' +import NextAuth from 'next-auth' +import { authKonfiguration } from '@/server/auth/config' + +/** + * Einstiegspunkt fuer Auth.js. Die Konfiguration wird erst hier ausgewertet, + * damit fehlende Umgebungsvariablen beim Start auffallen und nicht beim + * Einlesen des Moduls. + */ +export const { handlers, auth, signIn, signOut } = NextAuth(authKonfiguration()) diff --git a/src/server/auth/passkey.ts b/src/server/auth/passkey.ts new file mode 100644 index 0000000..ffe69c4 --- /dev/null +++ b/src/server/auth/passkey.ts @@ -0,0 +1,281 @@ +import 'server-only' +import { + generateAuthenticationOptions, + generateRegistrationOptions, + verifyAuthenticationResponse, + verifyRegistrationResponse, +} from '@simplewebauthn/server' +import type { + AuthenticationResponseJSON, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/server' +import { db } from '@/server/db' +import { umgebung } from '@/server/umgebung' + +/** + * Passkeys mit SimpleWebAuthn. + * + * Die Herausforderung wird serverseitig abgelegt und nach einem Versuch + * geloescht – gleich ob er geglueckt ist. Ohne das liesse sich eine Antwort + * beliebig oft einreichen. + */ + +const CHALLENGE_GUELTIG_MINUTEN = 5 + +type Zweck = 'anmeldung' | 'registrierung' + +export type AngemeldeterBenutzer = { + id: string + email: string + name: string | null + image: string | null +} + +function frist(jetzt: Date): Date { + return new Date(jetzt.getTime() + CHALLENGE_GUELTIG_MINUTEN * 60_000) +} + +async function challengeMerken( + kennung: string, + zweck: Zweck, + challenge: string, + jetzt: Date, +): Promise { + // Aeltere Herausforderungen desselben Zwecks verfallen sofort, damit nie + // mehrere gleichzeitig gueltig sind. + await db.webauthnChallenge.deleteMany({ where: { kennung, zweck } }) + await db.webauthnChallenge.create({ + data: { kennung, zweck, challenge, gueltigBis: frist(jetzt) }, + }) +} + +/** Holt die Herausforderung und verbraucht sie im selben Zug. */ +async function challengeVerbrauchen( + kennung: string, + zweck: Zweck, + jetzt: Date, +): Promise { + const eintrag = await db.webauthnChallenge.findFirst({ + where: { kennung, zweck }, + orderBy: { gueltigBis: 'desc' }, + }) + if (!eintrag) return null + + await db.webauthnChallenge.deleteMany({ where: { kennung, zweck } }) + if (eintrag.gueltigBis.getTime() < jetzt.getTime()) return null + return eintrag.challenge +} + +/** Abgelaufene Herausforderungen aufraeumen; laeuft spaeter im Hintergrundjob mit. */ +export async function abgelaufeneChallengesEntfernen(jetzt: Date): Promise { + const ergebnis = await db.webauthnChallenge.deleteMany({ + where: { gueltigBis: { lt: jetzt } }, + }) + return ergebnis.count +} + +function alsErlaubteZugangsdaten( + passkeys: { credentialId: string; transports: string | null }[], +): { id: string; transports?: string[] }[] { + return passkeys.map((p) => + p.transports + ? { id: p.credentialId, transports: p.transports.split(',') } + : { id: p.credentialId }, + ) +} + +// --- Registrierung --------------------------------------------------------- + +export async function registrierungsOptionen( + benutzerId: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const benutzer = await db.benutzer.findUnique({ + where: { id: benutzerId }, + select: { + id: true, + email: true, + name: true, + passkeys: { select: { credentialId: true, transports: true } }, + }, + }) + if (!benutzer) throw new Error('Benutzer nicht gefunden.') + + const optionen = await generateRegistrationOptions({ + rpName: einst.WEBAUTHN_RP_NAME, + rpID: einst.WEBAUTHN_RP_ID, + userName: benutzer.email, + userDisplayName: benutzer.name ?? benutzer.email, + userID: new TextEncoder().encode(benutzer.id), + attestationType: 'none', + // Bereits hinterlegte Passkeys ausschliessen, damit dasselbe Geraet nicht + // zweimal registriert wird. + excludeCredentials: alsErlaubteZugangsdaten(benutzer.passkeys), + authenticatorSelection: { + residentKey: 'preferred', + userVerification: 'preferred', + }, + }) + + await challengeMerken(benutzer.id, 'registrierung', optionen.challenge, jetzt) + return optionen +} + +export async function registrierungPruefen( + benutzerId: string, + antwort: RegistrationResponseJSON, + bezeichnung: string, + jetzt: Date = new Date(), +): Promise<{ passkeyId: string; bezeichnung: string }> { + const einst = umgebung() + const erwartet = await challengeVerbrauchen(benutzerId, 'registrierung', jetzt) + if (!erwartet) { + throw new Error('Die Anfrage ist abgelaufen. Bitte noch einmal beginnen.') + } + + const ergebnis = await verifyRegistrationResponse({ + response: antwort, + expectedChallenge: erwartet, + expectedOrigin: einst.WEBAUTHN_ORIGIN, + expectedRPID: einst.WEBAUTHN_RP_ID, + requireUserVerification: false, + }) + + if (!ergebnis.verified || !ergebnis.registrationInfo) { + throw new Error('Der Passkey konnte nicht geprüft werden.') + } + + const { credential, credentialDeviceType, credentialBackedUp } = + ergebnis.registrationInfo + const name = bezeichnung.trim() || 'Unbenanntes Gerät' + + const gespeichert = await db.passkey.create({ + data: { + benutzerId, + credentialId: credential.id, + publicKey: Buffer.from(credential.publicKey), + counter: BigInt(credential.counter), + transports: credential.transports?.join(',') ?? null, + geraetetyp: credentialDeviceType, + gesichert: credentialBackedUp, + bezeichnung: name, + }, + select: { id: true }, + }) + + return { passkeyId: gespeichert.id, bezeichnung: name } +} + +// --- Anmeldung ------------------------------------------------------------- + +export async function anmeldeOptionen( + email: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const kennung = email.trim().toLowerCase() + + const benutzer = await db.benutzer.findUnique({ + where: { email: kennung }, + select: { + aktiv: true, + passkeys: { select: { credentialId: true, transports: true } }, + }, + }) + + // Auch fuer unbekannte Adressen werden Optionen erzeugt. Sonst liesse sich + // hier abfragen, wer ein Konto mit Passkey hat. + const erlaubte = + benutzer?.aktiv === true ? alsErlaubteZugangsdaten(benutzer.passkeys) : [] + + const optionen = await generateAuthenticationOptions({ + rpID: einst.WEBAUTHN_RP_ID, + allowCredentials: erlaubte, + userVerification: 'preferred', + }) + + await challengeMerken(kennung, 'anmeldung', optionen.challenge, jetzt) + return optionen +} + +/** + * Prueft die Antwort des Browsers und gibt den Benutzer zurueck, wenn alles + * stimmt. Wird vom Credentials-Provider "passkey" aufgerufen. + */ +export async function passkeyAnmeldungPruefen( + email: string, + antwortRoh: string, + jetzt: Date = new Date(), +): Promise { + const einst = umgebung() + const kennung = email.trim().toLowerCase() + + const erwartet = await challengeVerbrauchen(kennung, 'anmeldung', jetzt) + if (!erwartet) return null + + let antwort: AuthenticationResponseJSON + try { + antwort = JSON.parse(antwortRoh) as AuthenticationResponseJSON + } catch { + return null + } + if (typeof antwort.id !== 'string') return null + + const passkey = await db.passkey.findUnique({ + where: { credentialId: antwort.id }, + select: { + id: true, + publicKey: true, + counter: true, + transports: true, + benutzer: { + select: { id: true, email: true, name: true, bildUrl: true, aktiv: true }, + }, + }, + }) + + // Der Passkey muss zu genau der Adresse gehoeren, fuer die die + // Herausforderung ausgestellt wurde. + if (!passkey || !passkey.benutzer.aktiv || passkey.benutzer.email !== kennung) { + return null + } + + let ergebnis + try { + ergebnis = await verifyAuthenticationResponse({ + response: antwort, + expectedChallenge: erwartet, + expectedOrigin: einst.WEBAUTHN_ORIGIN, + expectedRPID: einst.WEBAUTHN_RP_ID, + requireUserVerification: false, + credential: { + id: antwort.id, + publicKey: new Uint8Array(passkey.publicKey), + counter: Number(passkey.counter), + ...(passkey.transports ? { transports: passkey.transports.split(',') } : {}), + }, + }) + } catch { + return null + } + + if (!ergebnis.verified) return null + + await db.passkey.update({ + where: { id: passkey.id }, + data: { + counter: BigInt(ergebnis.authenticationInfo.newCounter), + zuletztBenutzt: jetzt, + }, + }) + + return { + id: passkey.benutzer.id, + email: passkey.benutzer.email, + name: passkey.benutzer.name, + image: passkey.benutzer.bildUrl, + } +} diff --git a/src/server/auth/passwort.ts b/src/server/auth/passwort.ts new file mode 100644 index 0000000..abd7b6b --- /dev/null +++ b/src/server/auth/passwort.ts @@ -0,0 +1,30 @@ +import 'server-only' +import bcrypt from 'bcryptjs' +import { PASSWORT_HOECHSTLAENGE } from '@/lib/passwort' + +const RUNDEN = 12 + +export async function passwortHashen(passwort: string): Promise { + if (passwort.length > PASSWORT_HOECHSTLAENGE) { + throw new Error('Passwort ist zu lang.') + } + return bcrypt.hash(passwort, RUNDEN) +} + +/** + * Vergleicht Passwort und Hash. Ist kein Hash hinterlegt, wird trotzdem + * gerechnet: sonst verraet die Antwortzeit, ob es das Konto gibt. + */ +export async function passwortStimmt( + passwort: string, + hash: string | null, +): Promise { + if (!hash) { + await bcrypt.compare( + passwort, + '$2b$12$ungueltigerHashAlsPlatzhalterXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + ) + return false + } + return bcrypt.compare(passwort, hash) +} diff --git a/src/server/berechtigung.ts b/src/server/berechtigung.ts new file mode 100644 index 0000000..5a2ef6d --- /dev/null +++ b/src/server/berechtigung.ts @@ -0,0 +1,74 @@ +import 'server-only' +import { redirect } from 'next/navigation' +import type { Session } from 'next-auth' +import type { Rolle } from '@/generated/prisma/enums' +import { rolleReichtAus } from '@/lib/rollen' +import { auth } from '@/server/auth' + +/** + * Rollenpruefung. + * + * Jede schreibende Aktion prueft serverseitig – in der Seite und noch einmal + * in der Server Action. Die Oberflaeche blendet Knoepfe nur zusaetzlich aus; + * verlassen wird sich darauf nie. + */ + +export type Angemeldet = Session['user'] + +/** Fehler, den Server Actions dem Aufrufer zurueckgeben. */ +export class NichtErlaubt extends Error { + constructor(nachricht = 'Dazu fehlt dir die Berechtigung.') { + super(nachricht) + this.name = 'NichtErlaubt' + } +} + +/** Die angemeldete Person, oder null. */ +export async function angemeldet(): Promise { + const sitzung = await auth() + return sitzung?.user?.id ? sitzung.user : null +} + +/** + * Riegel fuer Seiten: ohne Anmeldung geht es zur Anmeldeseite zurueck, + * mit Ziel, damit es danach dort weitergeht, wo man hinwollte. + */ +export async function verlangeAnmeldung(zielPfad?: string): Promise { + const benutzer = await angemeldet() + if (!benutzer) { + const ziel = zielPfad ? `?weiter=${encodeURIComponent(zielPfad)}` : '' + redirect(`/anmelden${ziel}`) + } + return benutzer +} + +/** Riegel fuer Seiten, die eine bestimmte Rolle verlangen. */ +export async function verlangeRolle( + verlangt: Rolle, + zielPfad?: string, +): Promise { + const benutzer = await verlangeAnmeldung(zielPfad) + if (!rolleReichtAus(benutzer.rolle, verlangt)) { + redirect('/keine-berechtigung') + } + return benutzer +} + +/** + * Riegel fuer Server Actions. Hier wird nicht umgeleitet, sondern geworfen – + * der Aufrufer wandelt das in eine Meldung fuer die Oberflaeche. + */ +export async function pruefeRolle(verlangt: Rolle): Promise { + const benutzer = await angemeldet() + if (!benutzer) throw new NichtErlaubt('Bitte zuerst anmelden.') + if (!rolleReichtAus(benutzer.rolle, verlangt)) throw new NichtErlaubt() + return benutzer +} + +/** + * Riegel fuer Route Handler: gibt null zurueck, statt umzuleiten. Der + * Aufrufer antwortet dann mit 401. + */ +export async function verlangeAnmeldungFuerApi(): Promise { + return angemeldet() +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..a72f305 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,20 @@ +import type { Rolle } from '@/generated/prisma/enums' + +/** + * Die Sitzung traegt zusaetzlich Kennung und Rolle. Beides wird im + * session-Callback bei jeder Anfrage frisch aus der Datenbank gelesen, damit + * eine Sperre oder ein Rollenwechsel sofort wirkt. + */ +declare module 'next-auth' { + interface Session { + user: { + id: string + rolle: Rolle + email: string + name?: string | null + image?: string | null + } + } +} + +export {} From e9a2b4b4631c4aaaf9c20c3838ab09ab2ebb4f23 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:02:49 +0200 Subject: [PATCH 07/19] =?UTF-8?q?feat(bereiche):=20Bereiche=20verwalten=20?= =?UTF-8?q?mit=20Rollenpr=C3=BCfung=20und=20Audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bereiche mit Farbe, Arbeitsfenster und Tageskapazität – die erste schreibende Funktion und damit der Beleg, dass die Kette trägt: zod prüft jede Eingabe, die Rolle wird in der Seite und noch einmal in der Server Action geprüft, jede Änderung hinterlässt einen Audit-Eintrag. Die Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name nach Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt. Das gilt auch in verschachtelten Objekten und Listen. Dazu die Protokollseite für die Verwaltung und die Startseite. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/(app)/bereiche/bereich-formular.tsx | 235 ++++++++++++++++++ src/app/(app)/bereiche/page.tsx | 124 +++++++++ src/app/(app)/einstellungen/page.tsx | 77 ++++++ .../einstellungen/passkey-verwaltung.tsx | 153 ++++++++++++ src/app/(app)/layout.tsx | 36 +++ src/app/(app)/page.tsx | 80 ++++++ src/app/(app)/verwaltung/protokoll/page.tsx | 91 +++++++ src/lib/audit.test.ts | 77 ++++++ src/lib/audit.ts | 66 +++++ src/lib/bereich.test.ts | 132 ++++++++++ src/lib/bereich.ts | 100 ++++++++ src/server/aktionen/bereiche.ts | 169 +++++++++++++ src/server/aktionen/passkeys.ts | 48 ++++ src/server/audit.ts | 44 ++++ 14 files changed, 1432 insertions(+) create mode 100644 src/app/(app)/bereiche/bereich-formular.tsx create mode 100644 src/app/(app)/bereiche/page.tsx create mode 100644 src/app/(app)/einstellungen/page.tsx create mode 100644 src/app/(app)/einstellungen/passkey-verwaltung.tsx create mode 100644 src/app/(app)/layout.tsx create mode 100644 src/app/(app)/page.tsx create mode 100644 src/app/(app)/verwaltung/protokoll/page.tsx create mode 100644 src/lib/audit.test.ts create mode 100644 src/lib/audit.ts create mode 100644 src/lib/bereich.test.ts create mode 100644 src/lib/bereich.ts create mode 100644 src/server/aktionen/bereiche.ts create mode 100644 src/server/aktionen/passkeys.ts create mode 100644 src/server/audit.ts diff --git a/src/app/(app)/bereiche/bereich-formular.tsx b/src/app/(app)/bereiche/bereich-formular.tsx new file mode 100644 index 0000000..978d696 --- /dev/null +++ b/src/app/(app)/bereiche/bereich-formular.tsx @@ -0,0 +1,235 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Loader2, Trash2 } from 'lucide-react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { KAPAZITAET_FELDER, WOCHENTAGE } from '@/lib/bereich' +import { + bereichAendern, + bereichAnlegen, + bereichLoeschen, + type Rueckmeldung, +} from '@/server/aktionen/bereiche' + +type Werte = { + id: string + name: string + farbe: string + reihenfolge: number + aktiv: boolean + arbeitsfensterVon: string + arbeitsfensterBis: string +} & Record<(typeof KAPAZITAET_FELDER)[number], number> + +const LEER = { + name: '', + farbe: '#2563eb', + reihenfolge: 0, + aktiv: true, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '17:00', + kapazitaetMo: 0, + kapazitaetDi: 0, + kapazitaetMi: 0, + kapazitaetDo: 0, + kapazitaetFr: 0, + kapazitaetSa: 0, + kapazitaetSo: 0, +} satisfies Omit + +export function BereichFormular({ bereich }: { bereich?: Werte }) { + const [laeuft, starte] = useTransition() + const [felder, setFelder] = useState>({}) + const [offen, setOffen] = useState(!bereich) + const werte = bereich ?? LEER + + function verarbeite(antwort: Rueckmeldung, formular: HTMLFormElement) { + if (antwort.ok) { + setFelder({}) + toast.success(antwort.meldung) + if (!bereich) formular.reset() + return + } + setFelder(antwort.felder ?? {}) + toast.error(antwort.fehler) + } + + function absenden(ereignis: React.FormEvent) { + ereignis.preventDefault() + const formular = ereignis.currentTarget + const daten = new FormData(formular) + starte(async () => { + const antwort = bereich + ? await bereichAendern(bereich.id, daten) + : await bereichAnlegen(daten) + verarbeite(antwort, formular) + }) + } + + function loeschen() { + if (!bereich) return + const daten = new FormData() + daten.set('id', bereich.id) + starte(async () => { + const antwort = await bereichLoeschen(daten) + if (antwort.ok) toast.success(antwort.meldung) + else toast.error(antwort.fehler) + }) + } + + if (bereich && !offen) { + return ( + + ) + } + + return ( +
+
+
+ + + {felder.name && ( +

+ {felder.name} +

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

{felder.arbeitsfensterBis}

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

{felder.kapazitaetMo}

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

Bereiche

+

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

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

Einstellungen

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

+ Noch kein Passkey hinterlegt. +

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

    {passkey.bezeichnung}

    +

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

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

Guten Tag, {anrede}

+

{alsDatum(jetzt)}

+
+ +
+

Deine Bereiche

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

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

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

Protokoll

+

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

+
+ + {eintraege.length === 0 ? ( + + + Noch nichts protokolliert. + + + ) : ( + + + + + + + + + + + + + {eintraege.map((eintrag) => ( + + + + + + + ))} + +
+ Zeitpunkt + + Wer + + Was + + Objekt +
+ {alsDatumUhrzeit(eintrag.zeitpunkt)} + {eintrag.benutzerEmail}{eintrag.aktion} + {eintrag.objektTyp} + {eintrag.details ? ( + + {JSON.stringify(eintrag.details)} + + ) : null} +
+
+
+ )} +
+ ) +} diff --git a/src/lib/audit.test.ts b/src/lib/audit.test.ts new file mode 100644 index 0000000..6dfbef1 --- /dev/null +++ b/src/lib/audit.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest' +import { ENTFERNT, auditDetails, detailsSaeubern } from './audit' + +describe('auditDetails', () => { + it('laesst fachliche Felder unangetastet', () => { + expect(auditDetails({ name: 'Gewerbe', farbe: '#2563eb', reihenfolge: 2 })).toEqual({ + name: 'Gewerbe', + farbe: '#2563eb', + reihenfolge: 2, + }) + }) + + it('gibt fuer nichts auch nichts zurueck', () => { + expect(auditDetails(undefined)).toBeUndefined() + }) + + it('entfernt Passwoerter und Tokens', () => { + expect( + auditDetails({ + email: 'a@b.de', + passwort: 'geheim123', + passwortHash: '$2b$12$...', + access_token: 'ey...', + }), + ).toEqual({ + email: 'a@b.de', + passwort: ENTFERNT, + passwortHash: ENTFERNT, + access_token: ENTFERNT, + }) + }) + + it('entfernt Termin-Betreffs und Mailinhalte', () => { + expect( + auditDetails({ betreff: 'Personalgespräch', body: 'Text', dauer: 30 }), + ).toEqual({ betreff: ENTFERNT, body: ENTFERNT, dauer: 30 }) + }) + + it('achtet nicht auf Gross- und Kleinschreibung', () => { + expect(auditDetails({ ClientSecret: 'x', REFRESH_TOKEN: 'y' })).toEqual({ + ClientSecret: ENTFERNT, + REFRESH_TOKEN: ENTFERNT, + }) + }) +}) + +describe('detailsSaeubern', () => { + it('greift auch in verschachtelten Objekten', () => { + expect(detailsSaeubern({ konto: { anbieter: 'GRAPH', id_token: 'ey...' } })).toEqual({ + konto: { anbieter: 'GRAPH', id_token: ENTFERNT }, + }) + }) + + it('greift in Listen', () => { + expect(detailsSaeubern([{ name: 'a', token: 'x' }])).toEqual([ + { name: 'a', token: ENTFERNT }, + ]) + }) + + it('schreibt Datumsangaben als ISO-Zeichenkette', () => { + expect(detailsSaeubern({ am: new Date('2026-09-10T12:00:00Z') })).toEqual({ + am: '2026-09-10T12:00:00.000Z', + }) + }) + + it('schneidet zu tiefe Verschachtelung ab, statt endlos abzusteigen', () => { + expect(detailsSaeubern({ a: { b: { c: { d: { e: { f: 1 } } } } } })).toEqual({ + a: { b: { c: { d: { e: ENTFERNT } } } }, + }) + }) + + it('kommt mit einfachen Werten und null zurecht', () => { + expect(detailsSaeubern('text')).toBe('text') + expect(detailsSaeubern(42)).toBe(42) + expect(detailsSaeubern(null)).toBeNull() + }) +}) diff --git a/src/lib/audit.ts b/src/lib/audit.ts new file mode 100644 index 0000000..f1d4a5a --- /dev/null +++ b/src/lib/audit.ts @@ -0,0 +1,66 @@ +/** + * Aufbereitung der Audit-Details. Reine Logik, damit sie ohne Datenbank + * geprueft werden kann. + * + * In die Details gehoeren nur fachliche Felder – niemals Tokens, Passwoerter, + * Mailinhalte oder Termin-Betreffs. Was hier landet, steht spaeter im + * Klartext in der Datenbank und in jeder Sicherung. + */ + +/** Feldnamen, die auch versehentlich nie in einem Audit-Eintrag landen duerfen. */ +const VERBOTEN = [ + 'passwort', + 'password', + 'token', + 'secret', + 'geheim', + 'clientsecret', + 'betreff', + 'subject', + 'inhalt', + 'body', + 'nachricht', +] + +export const ENTFERNT = '[entfernt]' + +function istVerdaechtig(schluessel: string): boolean { + const klein = schluessel.toLowerCase() + return VERBOTEN.some((v) => klein.includes(v)) +} + +/** + * Zweiter Riegel gegen Unachtsamkeit: Felder mit verdaechtigem Namen werden + * ersetzt, statt gespeichert zu werden. Wirkt auch in verschachtelten + * Objekten und in Listen. + */ +export function detailsSaeubern(wert: unknown, tiefe = 0): unknown { + // Tief verschachtelte Strukturen gehoeren nicht in ein Audit-Detail; ab + // einer gewissen Tiefe wird abgeschnitten statt endlos abgestiegen. + if (tiefe > 4) return ENTFERNT + + if (Array.isArray(wert)) { + return wert.map((eintrag) => detailsSaeubern(eintrag, tiefe + 1)) + } + + if (wert !== null && typeof wert === 'object') { + if (wert instanceof Date) return wert.toISOString() + const sauber: Record = {} + for (const [schluessel, inhalt] of Object.entries(wert)) { + sauber[schluessel] = istVerdaechtig(schluessel) + ? ENTFERNT + : detailsSaeubern(inhalt, tiefe + 1) + } + return sauber + } + + return wert +} + +/** Bequemer Einstieg fuer den Normalfall: ein flaches Objekt. */ +export function auditDetails( + details: Record | undefined, +): Record | undefined { + if (!details) return undefined + return detailsSaeubern(details) as Record +} diff --git a/src/lib/bereich.test.ts b/src/lib/bereich.test.ts new file mode 100644 index 0000000..b6292a2 --- /dev/null +++ b/src/lib/bereich.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest' +import { + KAPAZITAET_FELDER, + bereichSchema, + farbeSchema, + kapazitaetAmTag, + wochenKapazitaet, +} from './bereich' + +const gueltig = { + name: 'Gewerbe', + farbe: '#2563EB', + reihenfolge: 1, + aktiv: true, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '17:00', + kapazitaetMo: 240, + kapazitaetDi: 240, + kapazitaetMi: 240, + kapazitaetDo: 240, + kapazitaetFr: 120, + kapazitaetSa: 0, + kapazitaetSo: 0, +} + +describe('farbeSchema', () => { + it('nimmt sechsstellige und dreistellige Hex-Farben', () => { + expect(farbeSchema.parse('#2563eb')).toBe('#2563eb') + expect(farbeSchema.parse('#abc')).toBe('#abc') + }) + + it('macht Grossbuchstaben klein, damit Vergleiche stimmen', () => { + expect(farbeSchema.parse('#2563EB')).toBe('#2563eb') + }) + + it('weist Farben ohne Raute und mit falscher Laenge ab', () => { + expect(farbeSchema.safeParse('2563eb').success).toBe(false) + expect(farbeSchema.safeParse('#12345').success).toBe(false) + expect(farbeSchema.safeParse('blau').success).toBe(false) + }) +}) + +describe('bereichSchema', () => { + it('nimmt einen vollstaendigen Bereich an und rechnet Uhrzeiten in Minuten um', () => { + const ergebnis = bereichSchema.parse(gueltig) + expect(ergebnis.arbeitsfensterVon).toBe(480) + expect(ergebnis.arbeitsfensterBis).toBe(1020) + expect(ergebnis.farbe).toBe('#2563eb') + }) + + it('verlangt einen Namen mit mindestens zwei Zeichen', () => { + const ergebnis = bereichSchema.safeParse({ ...gueltig, name: 'A' }) + expect(ergebnis.success).toBe(false) + }) + + it('weist ein Arbeitsfenster ab, das vor seinem Beginn endet', () => { + const ergebnis = bereichSchema.safeParse({ + ...gueltig, + arbeitsfensterVon: '17:00', + arbeitsfensterBis: '08:00', + }) + expect(ergebnis.success).toBe(false) + if (!ergebnis.success) { + expect(ergebnis.error.issues[0]?.message).toMatch(/nach seinem Beginn enden/) + } + }) + + it('weist ein Arbeitsfenster ohne Dauer ab', () => { + const ergebnis = bereichSchema.safeParse({ + ...gueltig, + arbeitsfensterVon: '08:00', + arbeitsfensterBis: '08:00', + }) + expect(ergebnis.success).toBe(false) + }) + + it('weist eine Kapazitaet ab, die laenger ist als das Arbeitsfenster', () => { + // Fenster 08:00–17:00 sind 540 Minuten. + const ergebnis = bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 600 }) + expect(ergebnis.success).toBe(false) + if (!ergebnis.success) { + expect(ergebnis.error.issues[0]?.message).toMatch(/Arbeitsfenster/) + } + }) + + it('laesst eine Kapazitaet genau in Fensterlaenge zu', () => { + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 540 }).success).toBe(true) + }) + + it('weist negative und uebergrosse Kapazitaeten ab', () => { + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: -1 }).success).toBe(false) + expect(bereichSchema.safeParse({ ...gueltig, kapazitaetMo: 1441 }).success).toBe( + false, + ) + }) + + it('weist eine unsinnige Uhrzeit ab', () => { + expect( + bereichSchema.safeParse({ ...gueltig, arbeitsfensterVon: '25:00' }).success, + ).toBe(false) + }) +}) + +describe('wochenKapazitaet', () => { + it('summiert alle sieben Tage', () => { + const bereich = bereichSchema.parse(gueltig) + expect(wochenKapazitaet(bereich)).toBe(1080) + }) + + it('ist null, wenn nichts eingeplant ist', () => { + const leer = Object.fromEntries(KAPAZITAET_FELDER.map((f) => [f, 0])) as Record< + (typeof KAPAZITAET_FELDER)[number], + number + > + expect(wochenKapazitaet(leer)).toBe(0) + }) +}) + +describe('kapazitaetAmTag', () => { + it('trifft den richtigen Wochentag, Montag ist null', () => { + const bereich = bereichSchema.parse(gueltig) + expect(kapazitaetAmTag(bereich, 0)).toBe(240) + expect(kapazitaetAmTag(bereich, 4)).toBe(120) + expect(kapazitaetAmTag(bereich, 6)).toBe(0) + }) + + it('gibt fuer einen Index ausserhalb der Woche null zurueck', () => { + const bereich = bereichSchema.parse(gueltig) + expect(kapazitaetAmTag(bereich, 7)).toBe(0) + expect(kapazitaetAmTag(bereich, -1)).toBe(0) + }) +}) diff --git a/src/lib/bereich.ts b/src/lib/bereich.ts new file mode 100644 index 0000000..cdc05b5 --- /dev/null +++ b/src/lib/bereich.ts @@ -0,0 +1,100 @@ +import { z } from 'zod' +import { ausUhrzeit } from '@/lib/zeit' + +/** + * Regeln fuer Bereiche. Reine Logik, damit Formular und Server Action + * dieselbe Antwort geben. + */ + +export const WOCHENTAGE = [ + 'Montag', + 'Dienstag', + 'Mittwoch', + 'Donnerstag', + 'Freitag', + 'Samstag', + 'Sonntag', +] as const + +export const KAPAZITAET_FELDER = [ + 'kapazitaetMo', + 'kapazitaetDi', + 'kapazitaetMi', + 'kapazitaetDo', + 'kapazitaetFr', + 'kapazitaetSa', + 'kapazitaetSo', +] as const + +export type KapazitaetFeld = (typeof KAPAZITAET_FELDER)[number] + +/** Hex-Farbe mit Raute, drei- oder sechsstellig. */ +export const farbeSchema = z + .string() + .trim() + .regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, 'Bitte eine Farbe wie #2563eb angeben.') + .transform((f) => f.toLowerCase()) + +const uhrzeitSchema = z + .string() + .trim() + .refine((w) => ausUhrzeit(w) !== null, 'Bitte eine Uhrzeit wie 08:00 angeben.') + .transform((w) => ausUhrzeit(w) as number) + +/** Eine Tageskapazitaet: 0 bis 24 Stunden, in Minuten. */ +const kapazitaetSchema = z.coerce + .number() + .int('Bitte ganze Minuten angeben.') + .min(0, 'Nicht weniger als 0 Minuten.') + .max(1440, 'Nicht mehr als 1440 Minuten, also 24 Stunden.') + +export const bereichSchema = z + .object({ + name: z + .string() + .trim() + .min(2, 'Mindestens zwei Zeichen.') + .max(40, 'Höchstens 40 Zeichen.'), + farbe: farbeSchema, + reihenfolge: z.coerce.number().int().min(0).max(999).default(0), + aktiv: z.coerce.boolean().default(true), + arbeitsfensterVon: uhrzeitSchema, + arbeitsfensterBis: uhrzeitSchema, + kapazitaetMo: kapazitaetSchema, + kapazitaetDi: kapazitaetSchema, + kapazitaetMi: kapazitaetSchema, + kapazitaetDo: kapazitaetSchema, + kapazitaetFr: kapazitaetSchema, + kapazitaetSa: kapazitaetSchema, + kapazitaetSo: kapazitaetSchema, + }) + .refine((b) => b.arbeitsfensterVon < b.arbeitsfensterBis, { + path: ['arbeitsfensterBis'], + message: 'Das Arbeitsfenster muss nach seinem Beginn enden.', + }) + .refine( + (b) => + KAPAZITAET_FELDER.every( + (feld) => b[feld] <= b.arbeitsfensterBis - b.arbeitsfensterVon, + ), + { + path: ['kapazitaetMo'], + message: 'Keine Tageskapazität darf länger sein als das Arbeitsfenster.', + }, + ) + +export type BereichEingabe = z.infer + +/** Summe der Wochenkapazitaet in Minuten. */ +export function wochenKapazitaet(bereich: Record): number { + return KAPAZITAET_FELDER.reduce((summe, feld) => summe + bereich[feld], 0) +} + +/** Kapazitaet des Wochentags, 0 = Montag. */ +export function kapazitaetAmTag( + bereich: Record, + wochentagIndex: number, +): number { + const feld = KAPAZITAET_FELDER[wochentagIndex] + return feld ? bereich[feld] : 0 +} diff --git a/src/server/aktionen/bereiche.ts b/src/server/aktionen/bereiche.ts new file mode 100644 index 0000000..f26c9d8 --- /dev/null +++ b/src/server/aktionen/bereiche.ts @@ -0,0 +1,169 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { z } from 'zod' +import { bereichSchema } from '@/lib/bereich' +import { auditSchreiben } from '@/server/audit' +import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung' +import { db } from '@/server/db' + +/** + * Server Actions fuer Bereiche. + * + * Jede prueft die Rolle selbst – die Seite tut es ebenfalls, aber eine Action + * ist ein eigener Einstiegspunkt und darf sich darauf nicht verlassen. Jede + * schreibende Aktion hinterlaesst einen Audit-Eintrag. + */ + +export type Rueckmeldung = + | { ok: true; meldung: string } + | { ok: false; fehler: string; felder?: Record } + +function feldFehler(fehler: z.ZodError): Record { + const felder: Record = {} + for (const problem of fehler.issues) { + const feld = problem.path.join('.') + felder[feld] ??= problem.message + } + return felder +} + +function alsFormularwerte(daten: FormData): Record { + const werte = Object.fromEntries(daten.entries()) + return { + ...werte, + // Ein nicht angehaktes Kaestchen taucht im FormData gar nicht auf. + aktiv: daten.get('aktiv') !== null, + } +} + +export async function bereichAnlegen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = bereichSchema.safeParse(alsFormularwerte(daten)) + if (!gelesen.success) { + return { + ok: false, + fehler: 'Bitte die markierten Felder prüfen.', + felder: feldFehler(gelesen.error), + } + } + + const vorhanden = await db.bereich.findUnique({ + where: { name: gelesen.data.name }, + select: { id: true }, + }) + if (vorhanden) { + return { + ok: false, + fehler: 'Diesen Namen gibt es schon.', + felder: { name: 'Diesen Namen gibt es schon.' }, + } + } + + const bereich = await db.bereich.create({ + data: gelesen.data, + select: { id: true, name: true }, + }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.angelegt', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name, farbe: gelesen.data.farbe }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${bereich.name}" angelegt.` } +} + +export async function bereichAendern(id: string, daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = bereichSchema.safeParse(alsFormularwerte(daten)) + if (!gelesen.success) { + return { + ok: false, + fehler: 'Bitte die markierten Felder prüfen.', + felder: feldFehler(gelesen.error), + } + } + + const vorher = await db.bereich.findUnique({ + where: { id }, + select: { id: true, name: true }, + }) + if (!vorher) return { ok: false, fehler: 'Diesen Bereich gibt es nicht mehr.' } + + const namensdublette = await db.bereich.findFirst({ + where: { name: gelesen.data.name, id: { not: id } }, + select: { id: true }, + }) + if (namensdublette) { + return { + ok: false, + fehler: 'Diesen Namen gibt es schon.', + felder: { name: 'Diesen Namen gibt es schon.' }, + } + } + + await db.bereich.update({ where: { id }, data: gelesen.data }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.geaendert', + objektTyp: 'Bereich', + objektId: id, + details: { vorher: vorher.name, nachher: gelesen.data.name }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${gelesen.data.name}" gespeichert.` } +} + +const loeschEingabe = z.object({ id: z.string().min(1) }) + +export async function bereichLoeschen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('ADMIN') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = loeschEingabe.safeParse({ id: daten.get('id') }) + if (!gelesen.success) return { ok: false, fehler: 'Ungültige Anfrage.' } + + const bereich = await db.bereich.findUnique({ + where: { id: gelesen.data.id }, + select: { id: true, name: true }, + }) + if (!bereich) return { ok: false, fehler: 'Diesen Bereich gibt es nicht mehr.' } + + await db.bereich.delete({ where: { id: bereich.id } }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'bereich.geloescht', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name }, + }) + + revalidatePath('/bereiche') + return { ok: true, meldung: `Bereich „${bereich.name}" gelöscht.` } +} diff --git a/src/server/aktionen/passkeys.ts b/src/server/aktionen/passkeys.ts new file mode 100644 index 0000000..1ede0b5 --- /dev/null +++ b/src/server/aktionen/passkeys.ts @@ -0,0 +1,48 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { z } from 'zod' +import { auditSchreiben } from '@/server/audit' +import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung' +import { db } from '@/server/db' + +export type Rueckmeldung = { ok: true; meldung: string } | { ok: false; fehler: string } + +const eingabe = z.object({ id: z.string().min(1) }) + +/** + * Entfernt einen Passkey. Jeder darf nur seine eigenen entfernen – deshalb + * geht die Benutzer-Kennung in die where-Bedingung und wird nicht nur + * nachträglich verglichen. + */ +export async function passkeyEntfernen(daten: FormData): Promise { + let akteur + try { + akteur = await pruefeRolle('BENUTZER') + } catch (fehler) { + if (fehler instanceof NichtErlaubt) return { ok: false, fehler: fehler.message } + throw fehler + } + + const gelesen = eingabe.safeParse({ id: daten.get('id') }) + if (!gelesen.success) return { ok: false, fehler: 'Ungültige Anfrage.' } + + const passkey = await db.passkey.findFirst({ + where: { id: gelesen.data.id, benutzerId: akteur.id }, + select: { id: true, bezeichnung: true }, + }) + if (!passkey) return { ok: false, fehler: 'Diesen Passkey gibt es nicht mehr.' } + + await db.passkey.delete({ where: { id: passkey.id } }) + + await auditSchreiben({ + akteur: { id: akteur.id, email: akteur.email }, + aktion: 'passkey.entfernt', + objektTyp: 'Passkey', + objektId: passkey.id, + details: { bezeichnung: passkey.bezeichnung }, + }) + + revalidatePath('/einstellungen') + return { ok: true, meldung: `Passkey „${passkey.bezeichnung}" entfernt.` } +} diff --git a/src/server/audit.ts b/src/server/audit.ts new file mode 100644 index 0000000..88bae52 --- /dev/null +++ b/src/server/audit.ts @@ -0,0 +1,44 @@ +import 'server-only' +import type { Prisma } from '@/generated/prisma/client' +import { auditDetails } from '@/lib/audit' +import { db } from '@/server/db' +import { aktuelleInstanz } from '@/server/instanz' + +/** + * Nachweis jeder schreibenden Aktion. Die Details werden vorher gesaeubert + * (siehe @/lib/audit), damit keine Geheimnisse in der Datenbank landen. + */ + +export type AuditAkteur = { + id: string | null + email: string +} + +export type AuditEingabe = { + akteur: AuditAkteur + /** Was geschah, in der Form "objekt.verb", z. B. "bereich.angelegt". */ + aktion: string + objektTyp: string + objektId?: string | null + details?: Record | undefined +} + +/** + * Schreibt einen Audit-Eintrag. Schlaegt das fehl, darf die eigentliche + * Aktion nicht als geglueckt gelten – deshalb wird der Fehler weitergereicht + * und nicht verschluckt. + */ +export async function auditSchreiben(eingabe: AuditEingabe): Promise { + const details = auditDetails(eingabe.details) + await db.auditEintrag.create({ + data: { + benutzerId: eingabe.akteur.id, + benutzerEmail: eingabe.akteur.email, + aktion: eingabe.aktion, + objektTyp: eingabe.objektTyp, + objektId: eingabe.objektId ?? null, + ...(details ? { details: details as Prisma.InputJsonObject } : {}), + instanz: aktuelleInstanz(), + }, + }) +} From 1876ae4c6b749af9267934f0977b42f4a6b81250 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:02:49 +0200 Subject: [PATCH 08/19] =?UTF-8?q?feat(betrieb):=20Health-Endpunkt=20und=20?= =?UTF-8?q?Seed=20f=C3=BCr=20eine=20frische=20Datenbank?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/health meldet Status, Version, Instanz und Erreichbarkeit der Datenbank – ohne Anmeldung, deshalb ohne Zählstände, Namen oder Adressen. update.sh wird sich später darauf stützen. Der Seed legt die vier Bereiche an und ein erstes Verwaltungskonto aus ADMIN_EMAIL und ADMIN_PASSWORT. Er läuft mehrfach ohne Schaden; ohne ADMIN_PASSWORT entsteht ein Konto, das sich nur über OIDC oder Passkey anmelden kann. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 6 ++ devlog/zeiten.csv | 1 + prisma/seed.ts | 156 ++++++++++++++++++++++++++++++++++++ src/app/api/health/route.ts | 36 +++++++++ 4 files changed, 199 insertions(+) create mode 100644 devlog/zeiten.csv create mode 100644 prisma/seed.ts create mode 100644 src/app/api/health/route.ts diff --git a/.env.example b/.env.example index 279a421..cf43fca 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,12 @@ AUTH_TRUST_HOST=true # false, damit nur ueber OIDC angemeldet wird. ANMELDUNG_PASSWORT=true +# Erstes Verwaltungskonto, wird vom Seed angelegt. Ohne ADMIN_PASSWORT +# entsteht ein Konto ohne Passwort, das sich nur per OIDC oder Passkey +# anmelden kann. Nach der Einrichtung dürfen beide Werte wieder raus. +ADMIN_EMAIL= +ADMIN_PASSWORT= + # --- OIDC ------------------------------------------------------------------ # firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab. # Der Rueckleit-URI beim Anbieter lautet: diff --git a/devlog/zeiten.csv b/devlog/zeiten.csv new file mode 100644 index 0000000..0de7710 --- /dev/null +++ b/devlog/zeiten.csv @@ -0,0 +1 @@ +datum;start;ende;minuten;meilenstein;art;notiz diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..4cfa0a9 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,156 @@ +/** + * Grundausstattung für eine frische Datenbank. + * + * Läuft mehrfach ohne Schaden: alles wird per upsert angelegt, vorhandene + * Einträge bleiben unangetastet. Es entstehen keine erfundenen Aufgaben oder + * Termine – dafür gibt es prisma/demo-seed.ts. + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + throw new Error('DATABASE_URL ist nicht gesetzt.') +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +const BEREICHE = [ + { + name: 'Firma', + farbe: '#2563eb', + reihenfolge: 1, + arbeitsfensterVon: 7 * 60 + 30, + arbeitsfensterBis: 16 * 60 + 30, + kapazitaetMo: 360, + kapazitaetDi: 360, + kapazitaetMi: 360, + kapazitaetDo: 360, + kapazitaetFr: 240, + kapazitaetSa: 0, + kapazitaetSo: 0, + }, + { + name: 'Gewerbe', + farbe: '#7c3aed', + reihenfolge: 2, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 21 * 60, + kapazitaetMo: 90, + kapazitaetDi: 90, + kapazitaetMi: 90, + kapazitaetDo: 90, + kapazitaetFr: 60, + kapazitaetSa: 180, + kapazitaetSo: 0, + }, + { + name: 'Privat', + farbe: '#16a34a', + reihenfolge: 3, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 22 * 60, + kapazitaetMo: 60, + kapazitaetDi: 60, + kapazitaetMi: 60, + kapazitaetDo: 60, + kapazitaetFr: 120, + kapazitaetSa: 240, + kapazitaetSo: 240, + }, + { + name: 'Homelab', + farbe: '#ea580c', + reihenfolge: 4, + arbeitsfensterVon: 19 * 60, + arbeitsfensterBis: 23 * 60, + kapazitaetMo: 0, + kapazitaetDi: 60, + kapazitaetMi: 0, + kapazitaetDo: 60, + kapazitaetFr: 0, + kapazitaetSa: 120, + kapazitaetSo: 60, + }, +] + +async function bereicheAnlegen(): Promise { + for (const bereich of BEREICHE) { + await db.bereich.upsert({ + where: { name: bereich.name }, + update: {}, + create: bereich, + }) + } + console.log(`Bereiche geprüft: ${BEREICHE.map((b) => b.name).join(', ')}`) +} + +async function verwaltungAnlegen(): Promise { + const email = process.env.ADMIN_EMAIL?.trim().toLowerCase() + const passwort = process.env.ADMIN_PASSWORT + + if (!email) { + console.log( + 'ADMIN_EMAIL ist nicht gesetzt – kein Verwaltungskonto angelegt. ' + + 'Bei reiner OIDC-Anmeldung ist das richtig: die erste Person, die sich ' + + 'anmeldet, muss danach von Hand hochgestuft werden.', + ) + return + } + + const vorhanden = await db.benutzer.findUnique({ + where: { email }, + select: { id: true, rolle: true }, + }) + + if (vorhanden) { + if (vorhanden.rolle !== 'ADMIN') { + await db.benutzer.update({ where: { id: vorhanden.id }, data: { rolle: 'ADMIN' } }) + console.log(`${email} zur Verwaltung hochgestuft.`) + } else { + console.log(`${email} ist bereits Verwaltung.`) + } + return + } + + if (!passwort) { + await db.benutzer.create({ + data: { email, rolle: 'ADMIN', name: null }, + }) + console.log( + `${email} als Verwaltung angelegt, ohne Passwort. ` + + 'Anmeldung ist damit nur über OIDC oder einen Passkey möglich.', + ) + return + } + + if (passwort.length < 12) { + throw new Error('ADMIN_PASSWORT ist zu kurz, mindestens 12 Zeichen.') + } + + await db.benutzer.create({ + data: { + email, + rolle: 'ADMIN', + passwortHash: await bcrypt.hash(passwort, 12), + }, + }) + console.log(`${email} als Verwaltung mit Passwort angelegt.`) +} + +async function main(): Promise { + await bereicheAnlegen() + await verwaltungAnlegen() +} + +main() + .then(async () => { + await db.$disconnect() + }) + .catch(async (fehler: unknown) => { + console.error(fehler) + await db.$disconnect() + process.exit(1) + }) diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..89e1881 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import { VERSION } from '@/lib/version' +import { db } from '@/server/db' +import { aktuelleInstanz } from '@/server/instanz' + +export const dynamic = 'force-dynamic' + +/** + * Auskunft fuer Betrieb und update.sh. Bewusst ohne Anmeldung, deshalb steht + * hier nichts, was nicht ohnehin oeffentlich sein darf – insbesondere keine + * Zaehlstaende, Namen oder Adressen. + */ +export async function GET() { + let datenbank: 'ok' | 'fehler' = 'ok' + try { + await db.$queryRaw`SELECT 1` + } catch { + datenbank = 'fehler' + } + + const status = datenbank === 'ok' ? 'ok' : 'fehler' + + return NextResponse.json( + { + status, + version: VERSION, + instanz: aktuelleInstanz(), + datenbank, + zeit: new Date().toISOString(), + }, + { + status: status === 'ok' ? 200 : 503, + headers: { 'Cache-Control': 'no-store' }, + }, + ) +} From 24a799d6bad63382758651583168c5f4c64ab00e Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:03:06 +0200 Subject: [PATCH 09/19] =?UTF-8?q?chore:=20tsconfig-Anpassungen=20von=20Nex?= =?UTF-8?q?t=2016=20=C3=BCbernehmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next setzt beim Bauen jsx auf react-jsx und ergänzt seinen Typpfad. Wird das zurückgedreht, schreibt es der nächste Build wieder hin. Co-Authored-By: Claude Opus 5 (1M context) --- tsconfig.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 07b5708..e59dc59 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "ES2022"], "module": "esnext", "moduleResolution": "bundler", - "jsx": "preserve", + "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -21,7 +21,11 @@ "exactOptionalPropertyTypes": true, "verbatimModuleSyntax": true, "forceConsistentCasingInFileNames": true, - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "paths": { "@/*": ["./src/*"] } @@ -32,7 +36,8 @@ "**/*.tsx", ".next/types/**/*.ts", "vitest.config.ts", - "playwright.config.ts" + "playwright.config.ts", + ".next/dev/types/**/*.ts" ], "exclude": ["node_modules", ".next", "out"] } From 126ad93301d0fe88c3303394b480b0ab4d93e964 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 13:10:54 +0200 Subject: [PATCH 10/19] =?UTF-8?q?test:=20Integrations-,=20Migrations-=20un?= =?UTF-8?q?d=20E2E-Tests=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Integrationstest prüft gegen eine echte Datenbank, was Vitest nicht sieht: Eindeutigkeit, Fremdschlüssel, Kaskaden und dass das Protokoll den gelöschten Benutzer überlebt. Er läuft bewusst ohne umschließende Transaktion — er prüft absichtlich Fälle, die scheitern müssen, und ein gescheiterter Befehl bricht in PostgreSQL die ganze Transaktion ab. Stattdessen tragen alle Testdaten dieselbe Kennung und werden am Ende wieder entfernt. Der Migrationstest legt eine Wegwerf-Datenbank an, spielt alle Migrationen ein und vergleicht das Ergebnis mit dem Schema. Dass er Abweichungen wirklich bemerkt, wurde mit einem probeweise ergänzten Feld nachgewiesen. Der E2E-Rauchtest deckt den Weg ab, der in M1 trägt: Umleitung ohne Anmeldung samt gemerktem Ziel, Abweisung falscher Zugangsdaten, erfolgreiche Anmeldung und die Verwaltungsseite. Dafür neu: eine Middleware, die nur den angefragten Pfad als Kopfzeile durchreicht. Ein Layout kennt seinen Pfad nicht, braucht ihn aber für die Rückkehr nach der Anmeldung. Anmeldelogik steht dort bewusst nicht — die Middleware läuft im Edge-Umfeld ohne Prisma. Co-Authored-By: Claude Opus 5 (1M context) --- e2e/anmeldung.spec.ts | 79 ++++++++++++++ playwright.config.ts | 46 ++++++++ scripts/integrationstest.ts | 208 ++++++++++++++++++++++++++++++++++++ scripts/migrationstest.ts | 121 +++++++++++++++++++++ src/app/(app)/layout.tsx | 6 +- src/middleware.ts | 25 +++++ src/server/berechtigung.ts | 12 +++ 7 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 e2e/anmeldung.spec.ts create mode 100644 playwright.config.ts create mode 100644 scripts/integrationstest.ts create mode 100644 scripts/migrationstest.ts create mode 100644 src/middleware.ts diff --git a/e2e/anmeldung.spec.ts b/e2e/anmeldung.spec.ts new file mode 100644 index 0000000..7003e8f --- /dev/null +++ b/e2e/anmeldung.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test' + +/** + * Rauchtest der Anmeldung. + * + * Prüft den Weg, der in M1 wirklich trägt: Wer nicht angemeldet ist, kommt + * nirgends hin; mit richtigem Passwort schon; und die Verwaltungsseite bleibt + * verschlossen, wenn die Rolle nicht reicht. + * + * Voraussetzung: "npm run db:seed" ist gelaufen und ADMIN_EMAIL sowie + * ADMIN_PASSWORT sind gesetzt. + */ + +const EMAIL = process.env.ADMIN_EMAIL ?? '' +const PASSWORT = process.env.ADMIN_PASSWORT ?? '' + +test.describe('Anmeldung', () => { + test.skip( + !EMAIL || !PASSWORT, + 'ADMIN_EMAIL und ADMIN_PASSWORT sind nicht gesetzt – ohne sie gibt es kein Testkonto.', + ) + + test('leitet nicht Angemeldete zur Anmeldeseite', async ({ page }) => { + await page.goto('/') + await expect(page).toHaveURL(/\/anmelden/) + await expect(page.getByRole('heading', { name: 'Planer' })).toBeVisible() + }) + + test('merkt sich das Ziel und kehrt nach der Anmeldung dorthin zurück', async ({ + page, + }) => { + await page.goto('/bereiche') + await expect(page).toHaveURL(/\/anmelden\?weiter=%2Fbereiche/) + }) + + test('weist falsche Zugangsdaten ab, ohne zu verraten warum', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill('ganz-sicher-falsch-123') + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + + await expect(page.getByText('Anmeldung nicht möglich')).toBeVisible() + await expect(page).toHaveURL(/\/anmelden/) + }) + + test('lässt mit richtigem Passwort hinein und wieder hinaus', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill(PASSWORT) + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + + await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + await expect(page.getByRole('link', { name: 'Bereiche' })).toBeVisible() + + await page.getByTitle('Abmelden').click() + await expect(page).toHaveURL(/\/anmelden/) + }) + + test('zeigt der Verwaltung das Protokoll', async ({ page }) => { + await page.goto('/anmelden') + await page.getByLabel('E-Mail-Adresse').fill(EMAIL) + await page.getByLabel('Passwort').fill(PASSWORT) + await page.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await expect(page.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + + await page.getByRole('link', { name: 'Protokoll' }).click() + await expect(page.getByRole('heading', { name: 'Protokoll' })).toBeVisible() + }) +}) + +test('der Health-Endpunkt antwortet ohne Anmeldung', async ({ request }) => { + const antwort = await request.get('/api/health') + expect(antwort.ok()).toBe(true) + + const inhalt = (await antwort.json()) as Record + expect(inhalt.status).toBe('ok') + expect(inhalt.datenbank).toBe('ok') + expect(typeof inhalt.version).toBe('string') +}) diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e7de6a3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test' +import 'dotenv/config' + +const PORT = Number(process.env.E2E_PORT ?? 3100) +const BASIS = `http://127.0.0.1:${PORT}` + +/** + * E2E-Tests gegen eine echte, gebaute Anwendung. + * + * Der Testserver läuft auf einem eigenen Port, damit er einer laufenden + * Entwicklungsinstanz nicht in die Quere kommt. Die Datenbank ist dieselbe wie + * im Entwicklungsbetrieb – die Tests legen nur Daten mit eigenem Präfix an und + * räumen hinter sich auf. + */ +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI ? 'github' : 'list', + timeout: 30_000, + + use: { + baseURL: BASIS, + locale: 'de-DE', + timezoneId: 'Europe/Berlin', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + + webServer: { + command: 'npm run start', + url: `${BASIS}/api/health`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + PORT: String(PORT), + APP_URL: BASIS, + WEBAUTHN_ORIGIN: BASIS, + WEBAUTHN_RP_ID: '127.0.0.1', + }, + }, +}) diff --git a/scripts/integrationstest.ts b/scripts/integrationstest.ts new file mode 100644 index 0000000..41f217c --- /dev/null +++ b/scripts/integrationstest.ts @@ -0,0 +1,208 @@ +/** + * Integrationstest gegen eine echte Datenbank. + * + * Prüft, was Vitest nicht abdeckt: Eindeutigkeit, Fremdschlüssel, Kaskaden + * und dass das Audit-Protokoll den Benutzer überlebt. + * + * Bewusst ohne umschließende Transaktion: Der Test prüft absichtlich Fälle, + * die scheitern müssen, und ein gescheiterter Befehl bricht in PostgreSQL die + * ganze Transaktion ab. Stattdessen tragen alle Testdaten dieselbe Kennung + * und werden am Ende wieder entfernt — auch wenn der Test unterwegs abbricht. + * + * Aufruf: npm run test:integration + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from '../src/generated/prisma/client.js' + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +type Pruefung = { name: string; ok: boolean; hinweis?: string } +const ergebnisse: Pruefung[] = [] + +function pruefe(name: string, bedingung: boolean, hinweis?: string): void { + ergebnisse.push(hinweis ? { name, ok: bedingung, hinweis } : { name, ok: bedingung }) +} + +/** Erwartet, dass die Aktion scheitert. */ +async function scheitert(name: string, tun: () => Promise): Promise { + try { + await tun() + pruefe(name, false, 'Die Aktion war erlaubt, obwohl sie scheitern sollte.') + } catch { + pruefe(name, true) + } +} + +const kennung = `zzz-test-${Date.now()}` +const testEmail = `${kennung}@example.invalid` + +async function aufraeumen(): Promise { + // Reihenfolge egal, die Kaskaden erledigen den Rest; der Audit-Eintrag + // hängt bewusst an keinem Benutzer mehr. + await db.auditEintrag.deleteMany({ where: { benutzerEmail: testEmail } }) + await db.benutzer.deleteMany({ where: { email: testEmail } }) + await db.bereich.deleteMany({ where: { name: { startsWith: kennung } } }) + await db.webauthnChallenge.deleteMany({ where: { kennung } }) +} + +async function main(): Promise { + // --- Benutzer und Eindeutigkeit der Adresse ------------------------------ + const benutzer = await db.benutzer.create({ + data: { email: testEmail, name: 'Testkonto' }, + }) + pruefe('Benutzer anlegen', benutzer.id.length > 0) + pruefe('Rolle ist im Zweifel BENUTZER', benutzer.rolle === 'BENUTZER') + pruefe('Konto ist im Zweifel aktiv', benutzer.aktiv === true) + + await scheitert('E-Mail-Adresse ist eindeutig', () => + db.benutzer.create({ data: { email: testEmail } }), + ) + + // --- Bereich und Eindeutigkeit des Namens -------------------------------- + const bereich = await db.bereich.create({ + data: { name: `${kennung}-bereich`, farbe: '#2563eb', kapazitaetMo: 120 }, + }) + pruefe('Bereich anlegen', bereich.name.endsWith('-bereich')) + pruefe( + 'Arbeitsfenster hat brauchbare Standardwerte', + bereich.arbeitsfensterVon === 480 && bereich.arbeitsfensterBis === 1020, + ) + + await scheitert('Bereichsname ist eindeutig', () => + db.bereich.create({ data: { name: bereich.name, farbe: '#000000' } }), + ) + + // --- Passkey, Eindeutigkeit und Fremdschlüssel --------------------------- + const passkey = await db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: `${kennung}-cred`, + publicKey: Buffer.from([1, 2, 3]), + bezeichnung: 'Testgerät', + }, + }) + pruefe('Passkey anlegen', passkey.counter === 0n) + pruefe( + 'Zähler ist ein BigInt und beginnt bei null', + typeof passkey.counter === 'bigint', + ) + + await scheitert('Credential-ID ist eindeutig', () => + db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: passkey.credentialId, + publicKey: Buffer.from([4]), + bezeichnung: 'Zweitgerät', + }, + }), + ) + + await scheitert('Passkey ohne gültigen Benutzer ist nicht möglich', () => + db.passkey.create({ + data: { + benutzerId: 'gibt-es-nicht', + credentialId: `${kennung}-waise`, + publicKey: Buffer.from([9]), + bezeichnung: 'Waise', + }, + }), + ) + + // --- Öffentlicher Schlüssel kommt unverändert zurück --------------------- + const gelesen = await db.passkey.findUnique({ where: { id: passkey.id } }) + pruefe( + 'Der öffentliche Schlüssel kommt Byte für Byte zurück', + Buffer.from(gelesen?.publicKey ?? []).equals(Buffer.from([1, 2, 3])), + ) + + // --- Konto und Eindeutigkeit je Anbieter --------------------------------- + await db.konto.create({ + data: { + benutzerId: benutzer.id, + type: 'oidc', + provider: 'oidc', + providerAccountId: kennung, + }, + }) + await scheitert('Ein Anbieterkonto ist nur einmal verknüpfbar', () => + db.konto.create({ + data: { + benutzerId: benutzer.id, + type: 'oidc', + provider: 'oidc', + providerAccountId: kennung, + }, + }), + ) + + // --- Audit überlebt den Benutzer ---------------------------------------- + const audit = await db.auditEintrag.create({ + data: { + benutzerId: benutzer.id, + benutzerEmail: benutzer.email, + aktion: 'test.durchgefuehrt', + objektTyp: 'Bereich', + objektId: bereich.id, + details: { name: bereich.name }, + instanz: 'daheim', + }, + }) + pruefe('Audit-Eintrag anlegen', audit.id.length > 0) + + await db.benutzer.delete({ where: { id: benutzer.id } }) + + const nachher = await db.auditEintrag.findUnique({ where: { id: audit.id } }) + pruefe('Audit-Eintrag überlebt das Löschen des Benutzers', nachher !== null) + pruefe( + 'Die Kennung wird gelöst, die Adresse bleibt als Nachweis stehen', + nachher?.benutzerId === null && nachher?.benutzerEmail === testEmail, + ) + + pruefe( + 'Passkeys verschwinden mit dem Benutzer', + (await db.passkey.count({ where: { credentialId: passkey.credentialId } })) === 0, + ) + pruefe( + 'Verknüpfte Konten verschwinden mit dem Benutzer', + (await db.konto.count({ where: { providerAccountId: kennung } })) === 0, + ) +} + +let abbruch: unknown = null + +main() + .catch((fehler: unknown) => { + abbruch = fehler + }) + .finally(async () => { + await aufraeumen() + + const uebrig = + (await db.benutzer.count({ where: { email: testEmail } })) + + (await db.bereich.count({ where: { name: { startsWith: kennung } } })) + pruefe('Aufräumen hat nichts übrig gelassen', uebrig === 0) + + await db.$disconnect() + + if (abbruch) { + console.error('Der Integrationstest ist abgebrochen:', abbruch) + process.exit(1) + } + + const gefallen = ergebnisse.filter((e) => !e.ok) + for (const e of ergebnisse) { + console.log(`${e.ok ? '✓' : '✗'} ${e.name}${e.hinweis ? ` – ${e.hinweis}` : ''}`) + } + console.log( + `\n${ergebnisse.length - gefallen.length} von ${ergebnisse.length} Prüfungen bestanden.`, + ) + process.exit(gefallen.length === 0 ? 0 : 1) + }) diff --git a/scripts/migrationstest.ts b/scripts/migrationstest.ts new file mode 100644 index 0000000..52fbe4f --- /dev/null +++ b/scripts/migrationstest.ts @@ -0,0 +1,121 @@ +/** + * Migrationstest. + * + * Legt eine Wegwerf-Datenbank an, spielt alle Migrationen der Reihe nach ein + * und prüft anschließend, ob das Ergebnis genau dem Schema entspricht. Damit + * fällt auf, wenn jemand das Schema ändert, ohne eine Migration zu erzeugen — + * beim Aufspielen im Betrieb wäre das sonst erst spät zu merken. + * + * Aufruf: npm run test:migration + */ +import 'dotenv/config' +import { spawnSync } from 'node:child_process' +import { join } from 'node:path' +import { Client } from 'pg' + +const WURZEL = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1') + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const testDatenbank = `planer_migrationstest_${Date.now()}` + +/** Dieselbe Verbindung, aber auf eine andere Datenbank gerichtet. */ +function mitDatenbank(url: string, name: string): string { + const zerlegt = new URL(url) + zerlegt.pathname = `/${name}` + return zerlegt.toString() +} + +function prisma(argumente: string[], zusatz: Record): number { + const ergebnis = spawnSync( + process.execPath, + [join(WURZEL, 'node_modules', 'prisma', 'build', 'index.js'), ...argumente], + { + cwd: WURZEL, + stdio: 'inherit', + env: { ...process.env, ...zusatz }, + }, + ) + return ergebnis.status ?? 1 +} + +async function aufVerwaltungsdatenbank( + tun: (klient: Client) => Promise, +): Promise { + // "postgres" ist auf jedem Server vorhanden und wird hier nur benutzt, um + // die Wegwerf-Datenbank anzulegen und wieder zu entfernen. + const klient = new Client({ connectionString: mitDatenbank(verbindung!, 'postgres') }) + await klient.connect() + try { + return await tun(klient) + } finally { + await klient.end() + } +} + +async function main(): Promise { + console.log(`▸ Wegwerf-Datenbank ${testDatenbank} anlegen`) + await aufVerwaltungsdatenbank((k) => k.query(`CREATE DATABASE "${testDatenbank}"`)) + + const testUrl = mitDatenbank(verbindung!, testDatenbank) + + console.log('▸ Alle Migrationen einspielen') + if (prisma(['migrate', 'deploy'], { DATABASE_URL: testUrl }) !== 0) { + console.error('✗ Die Migrationen ließen sich nicht einspielen.') + return 1 + } + + console.log('▸ Ergebnis mit dem Schema vergleichen') + // --exit-code liefert 2, wenn ein Unterschied bleibt. + const abweichung = prisma( + [ + 'migrate', + 'diff', + // Die Quelle ist die eben eingespielte Wegwerf-Datenbank, das Ziel das + // Schema. So braucht der Vergleich keine Schattendatenbank. + '--from-config-datasource', + '--to-schema', + 'prisma/schema.prisma', + '--exit-code', + ], + { DATABASE_URL: testUrl }, + ) + + if (abweichung === 2) { + console.error( + '\n✗ Nach dem Einspielen weicht die Datenbank vom Schema ab.\n' + + ' Vermutlich fehlt eine Migration: "npx prisma migrate dev --name ".', + ) + return 1 + } + if (abweichung !== 0) { + console.error('✗ Der Vergleich ließ sich nicht durchführen.') + return 1 + } + + console.log('\n✓ Die Migrationen ergeben genau das Schema.') + return 0 +} + +let code = 1 +try { + code = await main() +} catch (fehler) { + console.error('Der Migrationstest ist abgebrochen:', fehler) +} finally { + await aufVerwaltungsdatenbank(async (k) => { + // Offene Verbindungen trennen, sonst lässt sich die Datenbank nicht löschen. + await k.query( + 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1', + [testDatenbank], + ) + await k.query(`DROP DATABASE IF EXISTS "${testDatenbank}"`) + }) + console.log(`▸ Wegwerf-Datenbank ${testDatenbank} entfernt`) +} + +process.exit(code) diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 4f4161f..0ad2e81 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,7 +1,7 @@ import { Kopfleiste } from '@/components/kopfleiste' import { INSTANZ_NAME } from '@/lib/instanz' import { versionAnzeige } from '@/lib/version' -import { verlangeAnmeldung } from '@/server/berechtigung' +import { angefragterPfad, verlangeAnmeldung } from '@/server/berechtigung' import { aktuelleInstanz } from '@/server/instanz' /** @@ -14,7 +14,9 @@ export default async function AngemeldetesLayout({ }: { children: React.ReactNode }) { - const benutzer = await verlangeAnmeldung() + // Der Pfad kommt aus der Kopfzeile der Middleware, damit es nach der + // Anmeldung dort weitergeht, wo man hinwollte. + const benutzer = await verlangeAnmeldung(await angefragterPfad()) const instanz = aktuelleInstanz() return ( diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..5d982bc --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,25 @@ +import { NextResponse, type NextRequest } from 'next/server' + +export const PFAD_KOPFZEILE = 'x-planer-pfad' + +/** + * Reicht den angefragten Pfad als Kopfzeile weiter. + * + * Ein Layout kennt seinen eigenen Pfad nicht, braucht ihn aber, um nach der + * Anmeldung wieder dorthin zurückzuführen. Hier steht bewusst keine + * Anmeldelogik: die Middleware läuft im Edge-Umfeld, in dem weder Prisma noch + * der Adapter verfügbar sind. Geprüft wird weiterhin im Layout, in der Seite + * und in jeder Server Action. + */ +export function middleware(anfrage: NextRequest) { + const kopfzeilen = new Headers(anfrage.headers) + kopfzeilen.set(PFAD_KOPFZEILE, `${anfrage.nextUrl.pathname}${anfrage.nextUrl.search}`) + return NextResponse.next({ request: { headers: kopfzeilen } }) +} + +export const config = { + matcher: [ + // Alles außer den Auth-Routen, statischen Dateien und Bildern. + '/((?!api/auth|_next/static|_next/image|favicon.ico).*)', + ], +} diff --git a/src/server/berechtigung.ts b/src/server/berechtigung.ts index 5a2ef6d..a44e1a6 100644 --- a/src/server/berechtigung.ts +++ b/src/server/berechtigung.ts @@ -1,9 +1,11 @@ import 'server-only' +import { headers } from 'next/headers' import { redirect } from 'next/navigation' import type { Session } from 'next-auth' import type { Rolle } from '@/generated/prisma/enums' import { rolleReichtAus } from '@/lib/rollen' import { auth } from '@/server/auth' +import { PFAD_KOPFZEILE } from '@/middleware' /** * Rollenpruefung. @@ -72,3 +74,13 @@ export async function pruefeRolle(verlangt: Rolle): Promise { export async function verlangeAnmeldungFuerApi(): Promise { return angemeldet() } + +/** + * Der angefragte Pfad, den die Middleware durchgereicht hat. Wird gebraucht, + * um nach der Anmeldung wieder dorthin zurueckzufuehren. + */ +export async function angefragterPfad(): Promise { + const kopfzeilen = await headers() + const pfad = kopfzeilen.get(PFAD_KOPFZEILE) + return pfad && pfad.startsWith('/') ? pfad : undefined +} From f91d015cc46aef87c209509690b4b4078fa03f87 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:08:04 +0200 Subject: [PATCH 11/19] feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dreistufiges Dockerfile mit standalone-Ausgabe; im Laufzeitabbild liegen weder Quelltext noch Bauwerkzeuge, und es läuft unter einem eigenen Benutzer. Die Migrationen erledigt ein eigener Einmaldienst aus der Bau-Stufe. Der Versuch, nur die Prisma-CLI ins Laufzeitabbild zu kopieren, scheiterte an ihren transitiven Modulen; sie alle mitzunehmen hätte das Abbild aufgebläht. Der Dienst läuft vor der App und muss sauber durchlaufen. Zwei Stellen mussten faul werden, weil Next beim Bauen Seitendaten sammelt und dabei Module lädt, die es zur Bauzeit noch ohne Umgebungsvariablen gibt: - Der Prisma-Client entsteht erst beim ersten Zugriff, über einen Proxy. - Die Auth.js-Konfiguration wird als Funktion übergeben. Ebenso liest prisma.config.ts die URL über process.env statt über env(), das beim Bauen ohne Datenbank abbricht. Dazu backup.sh, update.sh mit Rückrollpfad und scripts/release.sh. Der Dump wurde nachweislich in eine Wegwerf-Datenbank zurückgespielt und stimmt zeilengenau mit dem Original überein. Alle Skripte sind shellcheck-sauber. Co-Authored-By: Claude Opus 5 (1M context) --- Dockerfile | 52 ++++++++++ backup.sh | 107 ++++++++++++++++++++ docker-compose.yml | 34 ++++--- prisma.config.ts | 8 +- public/.gitkeep | 1 + scripts/release.sh | 93 +++++++++++++++++ src/server/auth/index.ts | 11 ++- src/server/db.ts | 36 +++++-- update.sh | 209 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 523 insertions(+), 28 deletions(-) create mode 100644 Dockerfile create mode 100755 backup.sh create mode 100644 public/.gitkeep create mode 100755 scripts/release.sh create mode 100755 update.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6250b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# Planer – Abbild für den Betrieb. +# +# Die Stufe "bauen" dient zugleich als Abbild für den Migrationsdienst: die +# Prisma-CLI bringt eine ganze Reihe Module mit, die im schlanken +# Laufzeitabbild nichts zu suchen haben. Siehe docker-compose.yml. +# --------------------------------------------------------------------------- + +FROM node:24-alpine AS abhaengigkeiten +WORKDIR /app +RUN apk add --no-cache libc6-compat +COPY package.json package-lock.json ./ +# Die Bauwerkzeuge werden gebraucht und bleiben in dieser Stufe. +RUN npm ci --ignore-scripts && npm rebuild prisma @prisma/engines esbuild unrs-resolver + + +FROM node:24-alpine AS bauen +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=abhaengigkeiten /app/node_modules ./node_modules +COPY . . +# Der Prisma-Client wird erzeugt, bevor Next baut. +RUN npx prisma generate && npm run build + + +FROM node:24-alpine AS betrieb +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + TZ=Europe/Berlin + +RUN apk add --no-cache tzdata curl \ + && addgroup --system --gid 1001 planer \ + && adduser --system --uid 1001 --ingroup planer planer + +# Die standalone-Ausgabe bringt nur die wirklich benutzten Module mit. +COPY --from=bauen --chown=planer:planer /app/.next/standalone ./ +COPY --from=bauen --chown=planer:planer /app/.next/static ./.next/static +COPY --from=bauen --chown=planer:planer /app/public ./public + +USER planer +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=5 \ + CMD curl -fsS http://127.0.0.1:3000/api/health || exit 1 + +CMD ["node", "server.js"] diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..ac20caf --- /dev/null +++ b/backup.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Sichert Datenbank, .env und Uploads des Planers. +# Wiederherstellung: siehe README.md, Abschnitt "Sicherung und Wiederherstellung". +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$WURZEL" + +ZIEL="${WURZEL}/sicherungen" +AUFBEWAHRUNG_TAGE=30 +NUR_DATENBANK=0 + +hilfe() { + cat <<'ENDE' +Verwendung: backup.sh [Optionen] + + --ziel Ablageort der Sicherung (Standard: ./sicherungen) + --tage Aufbewahrung in Tagen (Standard: 30, 0 = unbegrenzt) + --nur-datenbank Nur den Datenbank-Dump erzeugen + --hilfe Diese Übersicht + +Erzeugt je Lauf zwei Dateien: + planer-db-.dump Datenbank im Custom-Format von pg_dump + planer-dateien-.tar.gz .env und der Ordner uploads +ENDE +} + +meldung() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"; } +fehler() { + printf '%s Fehler: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --ziel) + ZIEL="${2:-}" + [ -n "$ZIEL" ] || fehler '--ziel braucht einen Pfad.' + shift 2 + ;; + --tage) + AUFBEWAHRUNG_TAGE="${2:-}" + case "$AUFBEWAHRUNG_TAGE" in + ''|*[!0-9]*) fehler '--tage braucht eine ganze Zahl.' ;; + esac + shift 2 + ;; + --nur-datenbank) + NUR_DATENBANK=1 + shift + ;; + --hilfe|-h) + hilfe + exit 0 + ;; + *) fehler "Unbekannte Option '$1'. 'backup.sh --hilfe' zeigt die Übersicht." ;; + esac +done + +command -v docker >/dev/null 2>&1 || fehler 'docker fehlt.' +[ -f "$WURZEL/.env" ] || fehler '.env fehlt – im Projektverzeichnis aufrufen.' + +# Nur die benötigten Werte lesen, nicht die ganze Datei ausführen. +lies_env() { + local schluessel="$1" standard="${2:-}" + local wert + wert="$(grep -E "^${schluessel}=" "$WURZEL/.env" | tail -1 | cut -d= -f2- || true)" + wert="${wert%\"}" + wert="${wert#\"}" + printf '%s' "${wert:-$standard}" +} + +PG_BENUTZER="$(lies_env POSTGRES_USER planer)" +PG_DATENBANK="$(lies_env POSTGRES_DB planer)" + +DB_CONTAINER="$(docker compose ps -q db)" +[ -n "$DB_CONTAINER" ] || fehler 'Der Datenbank-Container läuft nicht. Erst "docker compose up -d db".' + +ZEITSTEMPEL="$(date '+%Y-%m-%d_%H-%M-%S')" +mkdir -p "$ZIEL" + +DUMP="$ZIEL/planer-db-${ZEITSTEMPEL}.dump" +meldung "Datenbank sichern nach $DUMP" +# -Fc ist das Custom-Format: komprimiert und mit pg_restore selektiv einspielbar. +docker exec "$DB_CONTAINER" pg_dump -U "$PG_BENUTZER" -d "$PG_DATENBANK" -Fc > "$DUMP" + +if [ ! -s "$DUMP" ]; then + rm -f "$DUMP" + fehler 'Der Dump ist leer geblieben.' +fi +meldung "Datenbank gesichert ($(du -h "$DUMP" | cut -f1))." + +if [ "$NUR_DATENBANK" -eq 0 ]; then + DATEIEN="$ZIEL/planer-dateien-${ZEITSTEMPEL}.tar.gz" + meldung "Dateien sichern nach $DATEIEN" + ZU_SICHERN=(.env) + [ -d "$WURZEL/uploads" ] && ZU_SICHERN+=(uploads) + tar -czf "$DATEIEN" -C "$WURZEL" "${ZU_SICHERN[@]}" + meldung "Dateien gesichert ($(du -h "$DATEIEN" | cut -f1))." +fi + +if [ "$AUFBEWAHRUNG_TAGE" -gt 0 ]; then + meldung "Sicherungen älter als $AUFBEWAHRUNG_TAGE Tage entfernen." + find "$ZIEL" -maxdepth 1 -type f -name 'planer-*' -mtime "+$AUFBEWAHRUNG_TAGE" -print -delete +fi + +meldung 'Sicherung abgeschlossen.' diff --git a/docker-compose.yml b/docker-compose.yml index 34b0ecd..765b507 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,15 +20,36 @@ services: retries: 10 start_period: 20s + # Einmaldienst: spielt die Migrationen ein und beendet sich. Läuft aus der + # Bau-Stufe, weil die Prisma-CLI eine Reihe Module braucht, die im schlanken + # Laufzeitabbild nichts zu suchen haben. + migration: + build: + context: . + dockerfile: Dockerfile + target: bauen + container_name: planer-migration + restart: 'no' + depends_on: + db: + condition: service_healthy + environment: + TZ: ${TZ:-Europe/Berlin} + DATABASE_URL: postgresql://${POSTGRES_USER:-planer}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-planer}?schema=public + command: node node_modules/prisma/build/index.js migrate deploy + app: build: context: . dockerfile: Dockerfile + target: betrieb container_name: planer-app restart: unless-stopped depends_on: db: condition: service_healthy + migration: + condition: service_completed_successfully environment: NODE_ENV: production TZ: ${TZ:-Europe/Berlin} @@ -45,21 +66,10 @@ services: WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:?WEBAUTHN_RP_ID fehlt in .env} WEBAUTHN_RP_NAME: ${WEBAUTHN_RP_NAME:-Planer} WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:?WEBAUTHN_ORIGIN fehlt in .env} + # Der Demo-Modus ist im Betrieb nie erlaubt. DEMO_MODUS: 'false' ports: - '${APP_PORT:-3000}:3000' - healthcheck: - test: - [ - 'CMD', - 'node', - '-e', - "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", - ] - interval: 30s - timeout: 5s - retries: 5 - start_period: 40s volumes: db-daten: diff --git a/prisma.config.ts b/prisma.config.ts index 2774414..8ccc8cf 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,5 +1,5 @@ import 'dotenv/config' -import { defineConfig, env } from 'prisma/config' +import { defineConfig } from 'prisma/config' export default defineConfig({ schema: 'prisma/schema.prisma', @@ -8,6 +8,10 @@ export default defineConfig({ seed: 'tsx prisma/seed.ts', }, datasource: { - url: env('DATABASE_URL'), + // Bewusst über process.env statt über den env()-Helfer: "prisma generate" + // läuft auch beim Bauen des Abbilds, wo es noch gar keine Datenbank gibt. + // env() bricht dort ab. Fehlt die URL für migrate oder studio, meldet + // Prisma das selbst. + url: process.env.DATABASE_URL ?? '', }, }) diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..fc68be5 --- /dev/null +++ b/public/.gitkeep @@ -0,0 +1 @@ +# Der Ordner bleibt im Git, damit der Docker-Bau ihn kopieren kann. diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..e0a60a8 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Setzt eine neue Version: package.json, CHANGELOG, Commit und Tag. +# Pusht bewusst nicht – das geschieht erst nach ausdrücklicher Freigabe. +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$WURZEL" + +meldung() { printf '▸ %s\n' "$1"; } +fehler() { + printf 'Fehler: %s\n' "$1" >&2 + exit 1 +} + +hilfe() { + cat <<'ENDE' +Verwendung: scripts/release.sh + + Semantische Version ohne führendes v, z. B. 0.1.0 + +Ablauf: + 1. Sauberen Arbeitsbaum und grüne Prüfungen sicherstellen + 2. Version in package.json setzen + 3. "Unveröffentlicht" im CHANGELOG in einen datierten Abschnitt überführen + 4. chore(release): committen und Tag v anlegen + +Es wird nicht gepusht. +ENDE +} + +VERSION="${1:-}" +case "$VERSION" in + ''|--hilfe|-h) hilfe; [ -z "$VERSION" ] && exit 1; exit 0 ;; +esac + +printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \ + || fehler "\"$VERSION\" sieht nicht nach einer semantischen Version aus (z. B. 0.1.0)." + +TAG="v$VERSION" + +# --- 1. Voraussetzungen ---------------------------------------------------- +meldung 'Arbeitsbaum prüfen.' +[ -d .git ] || fehler 'Kein Git-Arbeitsbaum.' +[ -z "$(git status --porcelain)" ] || fehler 'Der Arbeitsbaum ist nicht sauber.' + +git rev-parse -q --verify "refs/tags/$TAG" >/dev/null \ + && fehler "Den Tag $TAG gibt es schon." + +[ -f CHANGELOG.md ] || fehler 'CHANGELOG.md fehlt.' +grep -q '^## \[Unveröffentlicht\]' CHANGELOG.md \ + || fehler 'Im CHANGELOG fehlt der Abschnitt "## [Unveröffentlicht]".' + +INHALT="$(awk ' + /^## \[Unveröffentlicht\]/ { sammeln = 1; next } + sammeln && /^## / { exit } + sammeln { print } +' CHANGELOG.md | grep -v '^[[:space:]]*$' || true)" +[ -n "$INHALT" ] || fehler 'Der Abschnitt "Unveröffentlicht" ist leer.' + +meldung 'Prüfungen laufen lassen.' +npm run pruefen || fehler 'Die Prüfungen sind nicht grün.' + +# --- 2. Version setzen ----------------------------------------------------- +meldung "Version auf $VERSION setzen." +npm version "$VERSION" --no-git-tag-version --allow-same-version >/dev/null + +# --- 3. CHANGELOG ---------------------------------------------------------- +meldung 'CHANGELOG fortschreiben.' +HEUTE="$(date '+%Y-%m-%d')" +TEMP="$(mktemp)" +awk -v version="$VERSION" -v heute="$HEUTE" ' + /^## \[Unveröffentlicht\]/ { + print "## [Unveröffentlicht]" + print "" + print "## [" version "] – " heute + next + } + { print } +' CHANGELOG.md > "$TEMP" +mv "$TEMP" CHANGELOG.md + +npx prettier --write package.json CHANGELOG.md >/dev/null 2>&1 || true + +# --- 4. Commit und Tag ----------------------------------------------------- +meldung "Commit und Tag $TAG anlegen." +git add package.json package-lock.json CHANGELOG.md +git commit -m "chore(release): $VERSION" +git tag -a "$TAG" -m "Planer $VERSION" + +meldung "Fertig. $TAG ist angelegt, aber nicht gepusht." +printf '\nNach der Freigabe:\n' +printf ' git push origin %s\n' "$(git rev-parse --abbrev-ref HEAD)" +printf ' git push origin %s\n' "$TAG" diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts index b36beeb..6690451 100644 --- a/src/server/auth/index.ts +++ b/src/server/auth/index.ts @@ -3,8 +3,11 @@ import NextAuth from 'next-auth' import { authKonfiguration } from '@/server/auth/config' /** - * Einstiegspunkt fuer Auth.js. Die Konfiguration wird erst hier ausgewertet, - * damit fehlende Umgebungsvariablen beim Start auffallen und nicht beim - * Einlesen des Moduls. + * Einstiegspunkt für Auth.js. + * + * Die Konfiguration wird als Funktion übergeben und dadurch erst bei der + * ersten Anfrage ausgewertet. Sie prüft Umgebungsvariablen, die es beim Bauen + * des Abbilds noch nicht gibt – bei sofortiger Auswertung bräche der Bau ab, + * sobald Next die Seitendaten der Auth-Routen sammelt. */ -export const { handlers, auth, signIn, signOut } = NextAuth(authKonfiguration()) +export const { handlers, auth, signIn, signOut } = NextAuth(() => authKonfiguration()) diff --git a/src/server/db.ts b/src/server/db.ts index b5170ec..bd22821 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -3,13 +3,19 @@ import { PrismaPg } from '@prisma/adapter-pg' import { PrismaClient } from '@/generated/prisma/client' /** - * Ein einziger Prisma-Client je Prozess. Im Entwicklungsbetrieb ueberlebt er - * das Neuladen der Module, damit nicht bei jedem Speichern ein neuer - * Verbindungspool entsteht. + * Ein einziger Prisma-Client je Prozess. + * + * Der Client wird erst beim ersten Zugriff erzeugt, nicht beim Laden des + * Moduls. Beim Bauen des Abbilds gibt es noch keine DATABASE_URL – Next sammelt + * dabei aber Seitendaten und lädt dieses Modul. Würde hier sofort verbunden, + * bräche der Bau ab. + * + * Im Entwicklungsbetrieb überlebt der Client das Neuladen der Module, damit + * nicht bei jedem Speichern ein neuer Verbindungspool entsteht. */ const globalerBereich = globalThis as unknown as { - prisma: PrismaClient | undefined + planerPrisma: PrismaClient | undefined } function erzeugen(): PrismaClient { @@ -17,15 +23,25 @@ function erzeugen(): PrismaClient { if (!verbindung) { throw new Error('DATABASE_URL ist nicht gesetzt.') } - const adapter = new PrismaPg({ connectionString: verbindung }) return new PrismaClient({ - adapter, + adapter: new PrismaPg({ connectionString: verbindung }), log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], }) } -export const db: PrismaClient = globalerBereich.prisma ?? erzeugen() - -if (process.env.NODE_ENV !== 'production') { - globalerBereich.prisma = db +function klient(): PrismaClient { + globalerBereich.planerPrisma ??= erzeugen() + return globalerBereich.planerPrisma } + +export const db: PrismaClient = new Proxy({} as PrismaClient, { + get(_ziel, eigenschaft) { + const echt = klient() as unknown as Record + const wert = echt[eigenschaft] + // Methoden an den echten Client binden, sonst zeigt "this" auf den Proxy. + if (typeof wert === 'function') { + return (wert as (...argumente: unknown[]) => unknown).bind(echt) + } + return wert + }, +}) diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..35d577a --- /dev/null +++ b/update.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Aktualisiert den Planer auf eine neue Version. +# Für Linux mit Docker Compose. Läuft im Projektverzeichnis. +set -euo pipefail + +WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$WURZEL" + +ZIELVERSION="" +OHNE_RUECKFRAGE=0 +OHNE_BACKUP=0 +NUR_PRUEFEN=0 +WARTEZEIT=180 + +LOGVERZEICHNIS="$WURZEL/logs" +LOGDATEI="$LOGVERZEICHNIS/update-$(date '+%Y-%m-%d_%H-%M-%S').log" + +hilfe() { + cat <<'ENDE' +Verwendung: update.sh [Optionen] + + --version vX.Y.Z Auf diese Version aktualisieren statt auf die neueste + --ja Ohne Rückfrage durchlaufen + --ohne-backup Keine Sicherung anlegen (nicht empfohlen) + --nur-pruefen Nur zeigen, was passieren würde + --wartezeit Sekunden, die auf einen gesunden Start gewartet wird + (Standard: 180) + --hilfe Diese Übersicht + +Ablauf: Voraussetzungen prüfen, Versionen ermitteln, Änderungen zeigen, +sichern, Ziel-Tag auschecken, bauen, starten, auf /api/health warten. +Schlägt etwas fehl, wird auf die vorherige Version zurückgerollt. +ENDE +} + +meldung() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI"; } +fehler() { + printf '%s Fehler: %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --version) + ZIELVERSION="${2:-}" + [ -n "$ZIELVERSION" ] || fehler '--version braucht eine Angabe wie v0.1.0.' + shift 2 + ;; + --ja) OHNE_RUECKFRAGE=1; shift ;; + --ohne-backup) OHNE_BACKUP=1; shift ;; + --nur-pruefen) NUR_PRUEFEN=1; shift ;; + --wartezeit) + WARTEZEIT="${2:-}" + case "$WARTEZEIT" in + ''|*[!0-9]*) fehler '--wartezeit braucht eine ganze Zahl.' ;; + esac + shift 2 + ;; + --hilfe|-h) hilfe; exit 0 ;; + *) fehler "Unbekannte Option '$1'. 'update.sh --hilfe' zeigt die Übersicht." ;; + esac +done + +mkdir -p "$LOGVERZEICHNIS" + +# --- 1. Voraussetzungen ---------------------------------------------------- +meldung 'Voraussetzungen prüfen.' +for werkzeug in git docker curl jq; do + command -v "$werkzeug" >/dev/null 2>&1 || fehler "$werkzeug fehlt." +done +docker compose version >/dev/null 2>&1 || fehler 'docker compose fehlt.' +[ -d "$WURZEL/.git" ] || fehler 'Kein Git-Arbeitsbaum – im Projektverzeichnis aufrufen.' +[ -f "$WURZEL/.env" ] || fehler '.env fehlt.' + +if [ -n "$(git status --porcelain)" ]; then + fehler 'Der Arbeitsbaum ist nicht sauber. Erst aufräumen oder committen.' +fi + +APP_PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- || true)" +APP_PORT="${APP_PORT:-3000}" +HEALTH_URL="http://127.0.0.1:${APP_PORT}/api/health" + +# --- 2. Versionen ermitteln ------------------------------------------------ +meldung 'Versionen ermitteln.' +INSTALLIERT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null | jq -r '.version // empty' || true)" +if [ -z "$INSTALLIERT" ]; then + INSTALLIERT="$(jq -r '.version' package.json)" + meldung "Die laufende Instanz war nicht erreichbar; nehme package.json: $INSTALLIERT" +else + meldung "Installiert: $INSTALLIERT" +fi + +VORHER_REF="$(git rev-parse --abbrev-ref HEAD)" +[ "$VORHER_REF" != 'HEAD' ] || VORHER_REF="$(git rev-parse HEAD)" + +git fetch --tags --quiet origin || fehler 'git fetch ist fehlgeschlagen.' + +if [ -z "$ZIELVERSION" ]; then + ZIELVERSION="$(git tag --list 'v*' --sort=-v:refname | head -1)" + [ -n "$ZIELVERSION" ] || fehler 'Es gibt keinen Versions-Tag auf origin.' +fi +git rev-parse -q --verify "refs/tags/${ZIELVERSION}" >/dev/null \ + || fehler "Den Tag $ZIELVERSION gibt es nicht." +meldung "Ziel: $ZIELVERSION" + +if [ "v$INSTALLIERT" = "$ZIELVERSION" ]; then + meldung 'Die Zielversion läuft bereits. Nichts zu tun.' + exit 0 +fi + +# --- 3. Änderungen zeigen -------------------------------------------------- +if [ -f CHANGELOG.md ]; then + meldung "Änderungen bis $ZIELVERSION laut CHANGELOG.md:" + # Vom Abschnitt der Zielversion bis zum Abschnitt der installierten Version. + awk -v ziel="${ZIELVERSION#v}" -v alt="$INSTALLIERT" ' + $0 ~ "^## \\[" ziel "\\]" { zeigen = 1 } + zeigen && $0 ~ "^## \\[" alt "\\]" { exit } + zeigen { print " " $0 } + ' CHANGELOG.md | tee -a "$LOGDATEI" +fi + +if [ "$NUR_PRUEFEN" -eq 1 ]; then + meldung "Nur geprüft: $INSTALLIERT würde auf ${ZIELVERSION#v} aktualisiert." + exit 0 +fi + +if [ "$OHNE_RUECKFRAGE" -eq 0 ]; then + printf 'Jetzt auf %s aktualisieren? [j/N] ' "$ZIELVERSION" + read -r antwort + case "$antwort" in + [jJ]|[jJ][aA]) ;; + *) meldung 'Abgebrochen.'; exit 0 ;; + esac +fi + +# --- 4. Sicherung ---------------------------------------------------------- +SICHERUNG='' +if [ "$OHNE_BACKUP" -eq 0 ]; then + meldung 'Sicherung anlegen.' + ./backup.sh --tage 30 | tee -a "$LOGDATEI" + SICHERUNG="$(find "$WURZEL/sicherungen" -maxdepth 1 -name 'planer-db-*.dump' -print0 2>/dev/null \ + | xargs -0 ls -t 2>/dev/null | head -1 || true)" +else + meldung 'Sicherung übersprungen (--ohne-backup).' +fi + +zurueckrollen() { + meldung "Zurückrollen auf $VORHER_REF." + git checkout --quiet "$VORHER_REF" || meldung 'Das Auschecken der alten Version schlug fehl.' + docker compose build >>"$LOGDATEI" 2>&1 || meldung 'Der Neubau der alten Version schlug fehl.' + docker compose up -d >>"$LOGDATEI" 2>&1 || meldung 'Der Start der alten Version schlug fehl.' + + if [ -n "$SICHERUNG" ]; then + meldung "Die Datenbank wurde NICHT zurückgespielt. Sicherung: $SICHERUNG" + meldung 'Wiederherstellen mit:' + meldung " docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists < \"$SICHERUNG\"" + fi + meldung "Vollständiges Protokoll: $LOGDATEI" +} + +# --- 5. Auschecken, bauen, starten ---------------------------------------- +meldung "Auf $ZIELVERSION wechseln." +if ! git checkout --quiet "$ZIELVERSION"; then + fehler "Der Wechsel auf $ZIELVERSION schlug fehl." +fi + +meldung 'Abbild bauen.' +if ! docker compose build >>"$LOGDATEI" 2>&1; then + meldung 'Der Bau ist fehlgeschlagen.' + zurueckrollen + exit 1 +fi + +meldung 'Dienste starten.' +if ! docker compose up -d >>"$LOGDATEI" 2>&1; then + meldung 'Der Start ist fehlgeschlagen.' + zurueckrollen + exit 1 +fi + +# --- 6. Auf Gesundheit warten --------------------------------------------- +meldung "Auf $HEALTH_URL warten (bis zu ${WARTEZEIT}s)." +ERWARTET="${ZIELVERSION#v}" +VERGANGEN=0 +GESUND=0 +while [ "$VERGANGEN" -lt "$WARTEZEIT" ]; do + ANTWORT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null || true)" + if [ -n "$ANTWORT" ]; then + STATUS="$(printf '%s' "$ANTWORT" | jq -r '.status // empty')" + LAEUFT="$(printf '%s' "$ANTWORT" | jq -r '.version // empty')" + if [ "$STATUS" = 'ok' ] && [ "$LAEUFT" = "$ERWARTET" ]; then + GESUND=1 + break + fi + fi + sleep 5 + VERGANGEN=$((VERGANGEN + 5)) +done + +# --- 7. Ergebnis ----------------------------------------------------------- +if [ "$GESUND" -eq 0 ]; then + meldung "Die neue Version meldet sich nicht gesund mit Version $ERWARTET." + docker compose logs --tail 50 app >>"$LOGDATEI" 2>&1 || true + zurueckrollen + exit 1 +fi + +meldung "Fertig. Der Planer läuft in Version $ERWARTET." +meldung "Protokoll: $LOGDATEI" From 56b5356708cdee270e32f8b7f3ed471277a6815f Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:29:51 +0200 Subject: [PATCH 12/19] docs: README, Schnellstart, Handbuch, Fahrplan und CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README mit Funktionsumfang, Bildergalerie, allen Umgebungsvariablen als Tabelle und dem Betrieb inklusive Sicherung und Wiederherstellung. Der Schnellstart führt in acht Schritten von git clone zur ersten Anmeldung, jeder Schritt mit erwartetem Ergebnis. Er endet mit dem Hinterlegen eines Passkeys statt beim ersten Kalenderkonto oder der ersten Aufgabe — beides gibt es erst mit M3 und M2. Das ist in ROADMAP und Handbuch so vermerkt, damit niemand danach sucht. Handbuch mit Seiten zu Anmelden, Bereichen und Protokoll. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 1 + CHANGELOG.md | 49 +++++++ README.md | 267 +++++++++++++++++++++++++++++++++++++ ROADMAP.md | 124 +++++++++++++++++ docs/SCHNELLSTART.md | 155 +++++++++++++++++++++ docs/handbuch/README.md | 36 +++++ docs/handbuch/anmelden.md | 83 ++++++++++++ docs/handbuch/bereiche.md | 80 +++++++++++ docs/handbuch/protokoll.md | 57 ++++++++ 9 files changed, 852 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 docs/SCHNELLSTART.md create mode 100644 docs/handbuch/README.md create mode 100644 docs/handbuch/anmelden.md create mode 100644 docs/handbuch/bereiche.md create mode 100644 docs/handbuch/protokoll.md diff --git a/.gitignore b/.gitignore index 42e9a5c..253c276 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ Thumbs.db .vscode/* !.vscode/extensions.json .idea/ +screenshot-lauf.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b662902 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Änderungen + +Alle nennenswerten Änderungen an diesem Projekt stehen hier. +Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), +die Versionierung [Semantic Versioning](https://semver.org/lang/de/). + +## [Unveröffentlicht] + +### Hinzugefügt + +- Projektgerüst mit Next 16 (App Router, `output: "standalone"`), React 19, + TypeScript strict, Tailwind 4 und shadcn-Primitiven. +- `npm run pruefen` bündelt Prettier, ESLint, `tsc --noEmit`, Vitest und + ShellCheck. ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt. +- Datenmodell für Benutzer mit Rolle, OIDC-Konten, Passkeys, Bereiche und + Audit-Einträge; PostgreSQL 16 über Prisma 7 mit pg-Treiberadapter. +- Anmeldung über einen generischen OIDC-Anbieter, über Passkeys + (SimpleWebAuthn) und wahlweise über E-Mail und Passwort. +- Rollen `BENUTZER` und `ADMIN`. Jede schreibende Aktion prüft die Rolle + serverseitig in der Seite **und** noch einmal in der Server Action. +- Audit-Protokoll für jede schreibende Aktion, mit Protokollseite für die + Verwaltung. Details werden vor dem Speichern gesäubert. +- Instanzsteuerung über `INSTANZ=firma|daheim`. Was eine Instanz nicht darf, + liefert dort 404 und lädt seine Konfiguration gar nicht erst. +- Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag. +- Zeitrechnung durchgehend in Europe/Berlin, mit Tagesgrenzen, Wochenbeginn + am Montag und deutschen Anzeigeformaten. +- `/api/health` mit Status, Version, Instanz und Erreichbarkeit der Datenbank. +- Hell und Dunkel, Navigation mit Rollenfilter, Oberfläche auf Deutsch. +- Dockerfile in drei Stufen und Compose-Stapel mit eigenem Migrationsdienst. +- `backup.sh` (pg_dump im Custom-Format, `.env` und Uploads), + `update.sh` (mit Rückrollpfad), `scripts/release.sh` und `scripts/zeit.sh`. +- Tests: Unit mit Vitest, Integrationstest und Migrationstest gegen eine + echte Datenbank, E2E-Rauchtest mit Playwright. +- `npm run screenshots` nimmt die Handbuchbilder in einer Wegwerf-Instanz mit + ausschließlich erfundenen Daten auf. +- README, Schnellstart, Handbuch, ROADMAP und DEVLOG. + +### Sicherheit + +- Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, + damit sich nicht abfragen lässt, wer ein Konto hat. +- Sitzungen als JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage + frisch aus der Datenbank gelesen — eine Sperre wirkt sofort. +- Die WebAuthn-Herausforderung wird nach einem Versuch verworfen, gleich ob + er geglückt ist. +- Der Demo-Seed verweigert den Dienst ohne `DEMO_MODUS=true`. +- `allowDangerousEmailAccountLinking` ist bewusst aktiv: je Instanz gibt es + genau einen vertrauenswürdigen OIDC-Anbieter, der die Adresse selbst prüft. diff --git a/README.md b/README.md new file mode 100644 index 0000000..71244c4 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# Planer + +Aufgaben, Ideen, Projekte und Zeitblöcke an einem Ort. + +Die Kernidee: Kalender aus Microsoft 365 (Firma, Gewerbe) und Google (privat) +lesen und Aufgaben per Ziehen und Ablegen als Zeitblöcke direkt in diese +Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben +wird. + +> **Stand:** Meilenstein 1 von 8. Die Grundlage steht: Anmeldung, Rollen, +> Protokoll, Bereiche und der Betrieb. Aufgaben kommen mit M2, das Lesen der +> Kalender mit M3, die Wochenplanung mit M4. Der [Fahrplan](ROADMAP.md) sagt, +> was wann folgt. + +- **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone` + zur ersten Anmeldung +- **[Handbuch](docs/handbuch/)** – Anleitung für den Alltag +- **[Fahrplan](ROADMAP.md)** – was geplant ist und was bewusst nicht +- **[Änderungen](CHANGELOG.md)** – was sich je Version getan hat + +## Funktionsumfang + +### Heute + +- Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und + Passwort — je Instanz einzeln zuschaltbar +- Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird + serverseitig geprüft und hinterlässt einen Protokolleintrag +- Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag +- Hell und Dunkel, Oberfläche durchgehend auf Deutsch +- Betrieb über Docker Compose, mit Sicherung, Aktualisierung und Healthcheck + +### Geplant + +Aufgaben und Projekte, Kalender lesen und schreiben, Wochenplanung per Ziehen +und Ablegen, Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und +ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). + +## Bildergalerie + +| | Hell | Dunkel | +| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) | +| Start | ![Startseite, hell](docs/bilder/start-hell-schreibtisch.png) | ![Startseite, dunkel](docs/bilder/start-dunkel-schreibtisch.png) | +| Bereiche | ![Bereiche, hell](docs/bilder/bereiche-hell-schreibtisch.png) | ![Bereiche, dunkel](docs/bilder/bereiche-dunkel-schreibtisch.png) | +| Einstellungen | ![Einstellungen, hell](docs/bilder/einstellungen-hell-schreibtisch.png) | ![Einstellungen, dunkel](docs/bilder/einstellungen-dunkel-schreibtisch.png) | +| Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) | + +Alle Bilder entstehen mit `npm run screenshots` in einer Wegwerf-Instanz mit +ausschließlich erfundenen Daten — niemals aus einer Instanz mit echten Daten. + +## Die zwei Instanzen + +Ein Abbild, zwei Deployments, gesteuert über `INSTANZ`. Die Umschaltung wird +**serverseitig erzwungen**: Was eine Instanz nicht darf, liefert dort 404 und +lädt seine Konfiguration gar nicht erst. + +| | `firma` | `daheim` | +| -------------- | -------------------------------------- | ------------------------------ | +| Anmeldung | OIDC Entra ID | OIDC authentik | +| Kalenderkonten | Graph (Firmen-Tenant) | Graph (Gewerbe-Tenant), Google | +| Mail | Graph Firmenpostfach | Graph Gewerbepostfach, Gmail | +| Integrationen | Zammad, Wartungsplaner, Spiegel senden | Telegram, Spiegel empfangen | + +Ein falsch geschriebener Wert bricht den Start ab, statt still auf einen +Standard zu fallen — in der Firma dürfen keine Provider anlaufen, die dort +nichts zu suchen haben. + +## Umgebungsvariablen + +Alle Werte stehen in `.env`. Vorlage: [`.env.example`](.env.example). +`.env` gehört niemals ins Git. + +### Pflicht + +| Variable | Beispiel | Bedeutung | +| ----------------- | ---------------------------------------------------- | ---------------------------------------------- | +| `INSTANZ` | `daheim` | `firma` oder `daheim` | +| `APP_URL` | `https://planer.example.de` | Öffentliche Adresse, ohne Schrägstrich am Ende | +| `DATABASE_URL` | `postgresql://planer:…@db:5432/planer?schema=public` | Verbindung zur Datenbank | +| `AUTH_SECRET` | _(32 Byte Base64)_ | Signiert Sitzungen. `openssl rand -base64 32` | +| `WEBAUTHN_RP_ID` | `planer.example.de` | Nackte Domain, ohne Schema und Port | +| `WEBAUTHN_ORIGIN` | `https://planer.example.de` | Exakt die Herkunft im Browser | + +### Datenbank im Compose-Stapel + +| Variable | Standard | Bedeutung | +| ------------------- | ---------- | ---------------------------------------------- | +| `POSTGRES_USER` | `planer` | Benutzer der Datenbank | +| `POSTGRES_PASSWORD` | _(keiner)_ | Pflicht. Muss zu `DATABASE_URL` passen | +| `POSTGRES_DB` | `planer` | Name der Datenbank | +| `POSTGRES_PORT` | `5432` | Nach außen veröffentlichter Port | +| `APP_PORT` | `3000` | Nach außen veröffentlichter Port der Anwendung | + +### Anmeldung + +| Variable | Standard | Bedeutung | +| -------------------- | -------- | ------------------------------------------ | +| `AUTH_TRUST_HOST` | `false` | Hinter einem Reverse Proxy auf `true` | +| `ANMELDUNG_PASSWORT` | `false` | Anmeldung mit E-Mail und Passwort erlauben | +| `OIDC_NAME` | _(leer)_ | Beschriftung des Anmeldeknopfs | +| `OIDC_ISSUER` | _(leer)_ | Issuer-URL des Anbieters | +| `OIDC_CLIENT_ID` | _(leer)_ | Client-ID | +| `OIDC_CLIENT_SECRET` | _(leer)_ | Client-Secret | +| `WEBAUTHN_RP_NAME` | `Planer` | Name, den das Gerät beim Passkey anzeigt | + +OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der +Rückleit-URI beim Anbieter lautet `/api/auth/callback/oidc`. + +### Einrichtung und Sonstiges + +| Variable | Standard | Bedeutung | +| ---------------- | --------------- | ---------------------------------------------------------------------------------------- | +| `ADMIN_EMAIL` | _(leer)_ | Erstes Verwaltungskonto, wird vom Seed angelegt | +| `ADMIN_PASSWORT` | _(leer)_ | Mindestens 12 Zeichen. Ohne den Wert entsteht ein Konto, das nur OIDC oder Passkey kennt | +| `TZ` | `Europe/Berlin` | Zeitzone für Anzeigen und Tagesgrenzen | +| `DEMO_MODUS` | `false` | Nur für die Screenshot-Instanz. Im Betrieb niemals `true` | + +## Betrieb + +### Starten und stoppen + +```bash +docker compose up -d # starten +docker compose logs -f app # Protokoll mitlesen +docker compose down # stoppen, Daten bleiben +``` + +Der Stapel besteht aus drei Diensten: `db`, ein Einmaldienst `migration`, der +die Migrationen einspielt und sich beendet, und `app`. + +### Healthcheck + +```bash +curl https://planer.example.de/api/health +``` + +```json +{ + "status": "ok", + "version": "0.1.0", + "instanz": "daheim", + "datenbank": "ok", + "zeit": "2026-03-17T08:20:00.000Z" +} +``` + +Die Auskunft ist bewusst ohne Anmeldung erreichbar und enthält deshalb keine +Zählstände, Namen oder Adressen. Bei `"status": "fehler"` antwortet der +Endpunkt mit HTTP 503. + +### Aktualisieren + +```bash +./update.sh # auf den neuesten Tag +./update.sh --nur-pruefen # nur zeigen, was passieren würde +./update.sh --version v0.2.0 # auf eine bestimmte Version +./update.sh --ja # ohne Rückfrage +``` + +Das Skript prüft die Voraussetzungen, zeigt die Änderungen aus dem CHANGELOG, +legt eine Sicherung an, wechselt auf den Ziel-Tag, baut, startet und wartet +auf einen gesunden `/api/health` mit der erwarteten Version. Klappt das nicht, +rollt es auf die vorherige Version zurück und nennt den Pfad der Sicherung. +Jeder Lauf wird nach `logs/update-.log` protokolliert. + +Die **Datenbank wird beim Zurückrollen nicht automatisch zurückgespielt** — +das wäre bei einem Teilfehler gefährlicher als hilfreich. Das Skript nennt den +Befehl dafür. + +### Sicherung und Wiederherstellung + +```bash +./backup.sh # Datenbank, .env und Uploads +./backup.sh --tage 90 # längere Aufbewahrung +./backup.sh --nur-datenbank # nur der Dump +``` + +Je Lauf entstehen zwei Dateien in `sicherungen/`: +ein `pg_dump` im Custom-Format und ein tar.gz mit `.env` und `uploads`. + +Wiederherstellen: + +```bash +# Datenbank +docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists \ + < sicherungen/planer-db-2026-03-17_02-00-00.dump + +# Dateien +tar -xzf sicherungen/planer-dateien-2026-03-17_02-00-00.tar.gz +``` + +Die Sicherung enthält `.env` und damit alle Geheimnisse. Sie gehört so +geschützt wie die Datenbank selbst. + +Nächtlich per cron: + +```cron +0 2 * * * cd /opt/planer && ./backup.sh --tage 30 >> logs/backup.log 2>&1 +``` + +## Entwicklung + +```bash +npm install +docker compose up -d db # nur die Datenbank +npm run db:migrieren # Migrationen einspielen +npm run db:seed # Bereiche und Verwaltungskonto +npm run dev # Entwicklungsserver +``` + +### Prüfungen + +```bash +npm run pruefen # Prettier, ESLint, tsc, Vitest, ShellCheck +npm run test:integration # gegen eine echte Datenbank +npm run test:migration # Migrationen gegen eine Wegwerf-Datenbank +npm run test:e2e # Playwright gegen die gebaute Anwendung +npm run screenshots # Handbuchbilder neu aufnehmen +``` + +`npm run pruefen` muss vor jedem Commit grün sein. Fehlt `shellcheck` lokal, +wird das Docker-Abbild `koalaman/shellcheck` benutzt. + +### Aufbau + +``` +prisma/ Schema, Migrationen, Seed, Demo-Seed +scripts/ Prüf-, Test- und Betriebsskripte +src/app/ Seiten und API-Routen; (app) = angemeldeter Bereich +src/components/ Bausteine; ui/ enthält die Primitiven +src/lib/ Reine Logik ohne DB und ohne Systemzeit, je Datei ein Test +src/server/ DB-nahe Dienste, Server Actions, Provider +e2e/ Playwright-Rauchtests +screenshots/ Aufnahme der Handbuchbilder +docs/ Schnellstart, Handbuch, Bilder +devlog/ Entwicklungstagebuch und Zeiterfassung +``` + +Reine Funktionen bekommen die aktuelle Zeit als Parameter, damit sie ohne +Tricks prüfbar bleiben. Die Tests laufen bewusst in UTC — so fällt sofort auf, +wenn irgendwo die Ortszeit des Servers durchschlägt statt Europe/Berlin. + +### Bekannte Meldungen + +`npm audit` meldet vier Funde in transitiven Abhängigkeiten der +**`prisma`-CLI** (`deepmerge-ts` über `@prisma/config`, `mysql2`). Beide sind +Entwicklungsabhängigkeiten und erreichen das Laufzeitabbild nicht; MySQL wird +ohnehin nie geladen. Der angebotene Fix wäre ein Rückschritt auf Prisma 6. + +## Sicherheit + +- Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, damit + sich nicht abfragen lässt, wer hier ein Konto hat. +- Sitzungen sind JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage + frisch aus der Datenbank gelesen — eine Sperre wirkt sofort. +- Jede schreibende Aktion prüft die Rolle in der Seite **und** noch einmal in + der Server Action. Die Oberfläche blendet Knöpfe nur zusätzlich aus. +- Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name nach + Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt. +- Keine Geheimnisse, Tokens, Mailinhalte oder Termin-Betreffs in Protokollen. + +Gefundene Schwachstellen bitte nicht als Issue anlegen, sondern direkt melden. + +## Lizenz + +Privates Projekt, keine Lizenz zur Weiterverwendung. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..4a3bbd7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,124 @@ +# Fahrplan + +Acht Meilensteine. Jeder endet mit einer lauffähigen Version auf `main`, +grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG. + +| Meilenstein | Ziel | Status | +| --------------------------------------- | ---------------------------- | --------- | +| [M1](#m1--fundament) | Fundament | in Arbeit | +| [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | geplant | +| [M3](#m3--kalender-lesen) | Kalender lesen | geplant | +| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | +| [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | +| [M6](#m6--integrationen) | Integrationen | geplant | +| [M7](#m7--schnellerfassung-und-api) | Schnellerfassung und API | geplant | +| [M8](#m8--mcp) | MCP | geplant | + +--- + +## M1 – Fundament + +**Ziel:** Ein angemeldeter, betreibbarer Rumpf. Noch keine Aufgaben, kein +Kalender — aber alles, worauf die späteren Meilensteine aufsetzen. + +- [x] Projektgerüst: Next 16, React 19, TypeScript strict, Tailwind 4 +- [x] `npm run pruefen`: Prettier, ESLint, tsc, Vitest, ShellCheck +- [x] Datenmodell für Anmeldung, Passkeys, Bereiche und Audit +- [x] Anmeldung über OIDC, Passkey und wahlweise Passwort +- [x] Rollen `BENUTZER` und `ADMIN`, serverseitig in Seite **und** Server Action +- [x] Audit-Eintrag je schreibender Aktion, Details werden gesäubert +- [x] Instanzsteuerung `INSTANZ=firma|daheim` mit 404 statt Hinweis +- [x] Bereiche verwalten: Farbe, Arbeitsfenster, Tageskapazität +- [x] Protokollseite für die Verwaltung +- [x] `/api/health` mit Status, Version und Instanz +- [x] Dockerfile, Compose-Stapel mit eigenem Migrationsdienst +- [x] `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh` +- [x] Unit-, Integrations-, Migrations- und E2E-Tests +- [x] Screenshot-Kette mit eigener Wegwerf-Instanz +- [x] README, Schnellstart, Handbuch, CHANGELOG, DEVLOG + +**Bewusst verschoben:** `Aufgabe` und `Zeitblock` kommen mit M2 und M4, +`Kalenderkonto` mit M3 — jeweils mit den Feldern, die der Provider dann +wirklich braucht, statt heute geraten. + +## M2 – Aufgaben und Projekte + +**Ziel:** Alles erfassen und wiederfinden, noch ohne Kalender. + +- [ ] `Aufgabe` mit Typ, Status, Priorität, Aufwand, Fälligkeit, Quelle +- [ ] Eindeutigkeit `[quelle, quelleRef]`, Integrationen upserten +- [ ] Inbox: schnell erfassen, später einsortieren +- [ ] Ideen getrennt von Aufgaben +- [ ] Projekte mit Arbeitspaketen und Meilensteinen +- [ ] Listen mit Filtern nach Bereich, Status und Fälligkeit +- [ ] Handbuchseiten und Bilder + +## M3 – Kalender lesen + +**Ziel:** Termine aus Microsoft 365 und Google sehen. + +- [ ] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis +- [ ] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim +- [ ] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft +- [ ] Ganztägige Termine zählen nicht zur Belegung +- [ ] Anonymisierte Beispielantworten in `test/fixtures/` +- [ ] Hintergrundjob mit node-cron + +## M4 – Wochenplanung und Zeitblöcke + +**Ziel:** Aufgaben per Ziehen und Ablegen in den Kalender schreiben — und +Verschiebungen von dort zurückholen. + +- [ ] `Zeitblock` an genau einer Aufgabe und einem Kalenderkonto +- [ ] Wochenansicht mit `@dnd-kit/core` +- [ ] Schreiben in den Kalender, Rückweg beim Verschieben +- [ ] Schleifenschutz über `hash = sha256(startISO|endeISO)` +- [ ] Konflikte: der Kalender gewinnt (HTTP 412 auf If-Match) +- [ ] Belegung gegen die Tageskapazität des Bereichs + +## M5 – Benachrichtigungen + +- [ ] Web-Push mit `web-push` +- [ ] Tagesüberblick per E-Mail über nodemailer +- [ ] Erinnerungen an Fälligkeiten, Jobs über node-cron +- [ ] Je Benutzer einstellbar, mit Ruhezeiten + +## M6 – Integrationen + +- [ ] Zammad und Wartungsplaner-API (nur `firma`) +- [ ] Telegram (nur `daheim`) +- [ ] Spiegel: `firma` sendet, `daheim` empfängt +- [ ] Jede Integration upsertet über `[quelle, quelleRef]` + +## M7 – Schnellerfassung und API + +- [ ] Erfassung vom Handy, auch offline +- [ ] Öffentliche API mit Token je Anwendung +- [ ] Rechte je Token, jede Nutzung im Protokoll + +## M8 – MCP + +- [ ] MCP-Server über `@modelcontextprotocol/sdk` +- [ ] Lesen und Anlegen von Aufgaben und Zeitblöcken +- [ ] Rechte wie bei der API, jede Nutzung im Protokoll + +--- + +## Ideen für später + +- Auswertung: wie viel Zeit ging tatsächlich in welchen Bereich +- Wiederkehrende Aufgaben mit eigenem Rhythmus +- Vorlagen für wiederkehrende Projekte +- Verknüpfung von Aufgaben untereinander (blockiert, gehört zu) +- Export der eigenen Daten als JSON + +## Bewusst nicht geplant + +- **Wiederkehrende Wartungsaufgaben mit Nachweispflicht.** Die bleiben im + Wartungsplaner. Der Planer liest nur Fälligkeiten über dessen API. +- **Mehrbenutzerbetrieb mit geteilten Aufgaben.** Der Planer ist für eine + Person gedacht; Kollegen melden sich in der Firmeninstanz nur an ihren + eigenen Daten an. +- **Eigene Kalenderdarstellung als Ersatz für Outlook.** Der Planer schreibt + in die vorhandenen Kalender, er ersetzt sie nicht. +- **Zeiterfassung zur Abrechnung.** Dafür gibt es andere Werkzeuge. diff --git a/docs/SCHNELLSTART.md b/docs/SCHNELLSTART.md new file mode 100644 index 0000000..4651e79 --- /dev/null +++ b/docs/SCHNELLSTART.md @@ -0,0 +1,155 @@ +# Schnellstart + +Von `git clone` bis zur ersten Anmeldung in höchstens zehn Minuten. +Jeder Befehl ist zum Kopieren, jeder Schritt nennt das erwartete Ergebnis. + +## Voraussetzungen + +| Werkzeug | Fassung | Prüfen mit | +| ------------------ | ---------------- | ------------------------ | +| Docker mit Compose | 25 oder neuer | `docker compose version` | +| Git | 2.40 oder neuer | `git --version` | +| Node.js | 22.12 oder neuer | `node -v` | + +Node wird nur für die Entwicklung gebraucht. Für den reinen Betrieb genügen +Docker und Git. + +## 1. Holen und Verzeichnis wechseln + +```bash +git clone planer +cd planer +``` + +**Erwartet:** Das Verzeichnis `planer` enthält unter anderem `package.json`, +`docker-compose.yml` und `.env.example`. + +## 2. Konfiguration anlegen + +```bash +cp .env.example .env +``` + +Jetzt die Pflichtwerte setzen. Die Geheimnisse werden erzeugt, nicht erfunden: + +```bash +# Signiert die Sitzungen. Ohne diesen Wert startet die Anwendung nicht. +openssl rand -base64 32 + +# Ein Passwort für die Datenbank. +openssl rand -hex 16 +``` + +Unter Windows ohne OpenSSL geht auch: + +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" +``` + +Diese Werte in `.env` eintragen: + +| Variable | Beispiel | Bedeutung | +| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | +| `INSTANZ` | `daheim` | `firma` oder `daheim` – bestimmt, was es überhaupt gibt | +| `APP_URL` | `http://localhost:3000` | Adresse ohne Schrägstrich am Ende | +| `AUTH_SECRET` | _(erzeugt)_ | Signatur der Sitzungen | +| `POSTGRES_PASSWORD` | _(erzeugt)_ | Passwort der Datenbank | +| `DATABASE_URL` | `postgresql://planer:@localhost:5432/planer?schema=public` | muss dasselbe Passwort enthalten | +| `WEBAUTHN_RP_ID` | `localhost` | nackte Domain, ohne Schema und Port | +| `WEBAUTHN_ORIGIN` | `http://localhost:3000` | exakt wie im Browser, mit Port | +| `ADMIN_EMAIL` | `du@example.de` | erstes Verwaltungskonto | +| `ADMIN_PASSWORT` | _(mindestens 12 Zeichen)_ | nur nötig, wenn du dich ohne OIDC anmelden willst | + +> **Sind die Ports 5432 oder 3000 auf deinem Rechner belegt?** +> Dann `POSTGRES_PORT` beziehungsweise `APP_PORT` in `.env` auf einen freien +> Wert setzen. `DATABASE_URL`, `APP_URL` und `WEBAUTHN_ORIGIN` müssen denselben +> Port nennen. + +**Erwartet:** `.env` liegt im Projektverzeichnis. Sie gehört niemals ins Git — +`.gitignore` sorgt dafür. + +## 3. Starten + +```bash +docker compose up -d --build +``` + +Beim ersten Mal dauert der Bau einige Minuten. Der Ablauf ist: Datenbank +starten, Migrationen einspielen, Anwendung starten. + +**Erwartet:** + +```bash +docker compose ps +``` + +zeigt `db` als `healthy` und `app` als `Up`. Der Dienst `migration` taucht +nicht mehr auf — er hat seine Arbeit getan und sich beendet. + +## 4. Prüfen, ob alles läuft + +```bash +curl http://localhost:3000/api/health +``` + +**Erwartet:** + +```json +{ "status": "ok", "version": "0.1.0", "instanz": "daheim", "datenbank": "ok" } +``` + +Steht dort `"datenbank": "fehler"`, stimmt `DATABASE_URL` nicht mit +`POSTGRES_PASSWORD` überein. + +## 5. Erste Bereiche und das Verwaltungskonto anlegen + +```bash +docker compose run --rm migration node node_modules/prisma/build/index.js db seed +``` + +**Erwartet:** Die Ausgabe nennt die vier angelegten Bereiche (Firma, Gewerbe, +Privat, Homelab) und bestätigt das Verwaltungskonto. + +## 6. Anmelden + +Im Browser öffnen. + +**Erwartet:** Die Anmeldeseite. Mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` +anmelden — danach steht oben „Guten Tag" und darunter die vier Bereiche. + +> Ist OIDC eingerichtet, erscheint zusätzlich ein Knopf mit dem Namen aus +> `OIDC_NAME`. Der Rückleit-URI, den du beim Anbieter hinterlegen musst, +> lautet `/api/auth/callback/oidc`. + +## 7. Einen Passkey hinterlegen + +Auf **Einstellungen** gehen, unter _Passkeys_ einen Namen für das Gerät +eintragen (etwa „Arbeitsrechner") und auf **Anlegen** klicken. Das Gerät +fragt nach Fingerabdruck, Gesicht oder PIN. + +**Erwartet:** Der Passkey steht in der Liste. Ab jetzt genügt zum Anmelden die +E-Mail-Adresse und **Mit Passkey anmelden**. + +> Hinterlege am besten zwei Passkeys auf verschiedenen Geräten. Sonst sperrst +> du dich aus, wenn ein Gerät verlorengeht. + +## 8. Aufräumen + +Wenn `ADMIN_PASSWORT` nur zur Einrichtung gedient hat, kannst du es jetzt +zusammen mit `ADMIN_EMAIL` aus `.env` entfernen. Das Konto bleibt bestehen. + +## Wie es weitergeht + +- [Handbuch](handbuch/) – Anleitung für den Alltag +- [README](../README.md) – alle Umgebungsvariablen, Betrieb, Sicherung +- [ROADMAP](../ROADMAP.md) – was in den nächsten Meilensteinen kommt + +## Wenn etwas klemmt + +| Symptom | Ursache und Abhilfe | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `INSTANZ fehlt in .env` beim Start | `INSTANZ` ist leer. Erlaubt sind nur `firma` und `daheim`. | +| `Die Konfiguration ist unvollständig` | Die Meldung nennt die fehlende Variable. Sie nennt nie den Wert. | +| `port is already allocated` | Ein anderer Dienst hält den Port. `APP_PORT` oder `POSTGRES_PORT` ändern. | +| Passkey lässt sich nicht anlegen | `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. `WEBAUTHN_RP_ID` ist die nackte Domain, ohne `https://`. | +| Anmeldung schlägt immer fehl | Bewusst gibt es nur eine Meldung für alle Fälle. Prüfe im Protokoll der Anwendung: `docker compose logs app`. | diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md new file mode 100644 index 0000000..cdbc4f8 --- /dev/null +++ b/docs/handbuch/README.md @@ -0,0 +1,36 @@ +# Handbuch + +Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. + +## Erste Schritte + +1. **[Anmelden](anmelden.md)** – die drei Wege hinein, Passkeys einrichten, + was tun, wenn nichts mehr geht +2. **[Bereiche einrichten](bereiche.md)** – Farbe, Arbeitsfenster und + Tageskapazität festlegen + +## Verwaltung + +3. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat + +## Was noch kommt + +Diese Seiten entstehen mit den nächsten Meilensteinen: + +| Thema | Meilenstein | +| ------------------------------- | ----------- | +| Aufgaben erfassen, Inbox, Ideen | M2 | +| Projekte und Arbeitspakete | M2 | +| Kalenderkonten verbinden | M3 | +| Wochenplanung und Zeitblöcke | M4 | +| Benachrichtigungen | M5 | +| Integrationen | M6 | +| Schnellerfassung vom Handy, API | M7 | +| MCP | M8 | + +Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt. + +--- + +Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md). +Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). diff --git a/docs/handbuch/anmelden.md b/docs/handbuch/anmelden.md new file mode 100644 index 0000000..acef851 --- /dev/null +++ b/docs/handbuch/anmelden.md @@ -0,0 +1,83 @@ +# Anmelden + +Es gibt drei Wege in den Planer. Welche davon zu sehen sind, hängt davon ab, +was für deine Instanz eingerichtet wurde. + +![Die Anmeldeseite](../bilder/anmelden-hell-schreibtisch.png) + +| Weg | Wann sinnvoll | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | +| **Unternehmensanmeldung** (OIDC) | In der Firma der Normalfall. Ein Klick, die Anmeldung übernimmt Entra ID beziehungsweise authentik. | +| **Passkey** | Der bequemste Weg am eigenen Gerät: Fingerabdruck, Gesicht oder Geräte-PIN, kein Passwort. | +| **E-Mail und Passwort** | Der Rückfallweg. Oft nur daheim freigeschaltet. | + +## Mit Passkey anmelden + +1. E-Mail-Adresse eintragen. +2. Auf **Mit Passkey anmelden** klicken. +3. Das Gerät fragt nach Fingerabdruck, Gesicht oder PIN. + +Fertig — du landest auf der Startseite. + +> **Warum erst die E-Mail-Adresse?** Damit der Planer weiß, welche Passkeys er +> anbieten soll. Er antwortet dabei für unbekannte Adressen genauso wie für +> bekannte, damit sich von außen nicht abfragen lässt, wer hier ein Konto hat. + +## Einen Passkey einrichten + +Das geht erst, wenn du angemeldet bist — also beim ersten Mal über OIDC oder +über E-Mail und Passwort. + +1. Oben rechts auf **Einstellungen**. +2. Im Abschnitt _Passkeys_ einen Namen für das Gerät eintragen, etwa + „Arbeitsrechner" oder „Diensthandy". +3. Auf **Anlegen** klicken und die Abfrage des Geräts bestätigen. + +![Passkeys in den Einstellungen](../bilder/einstellungen-hell-schreibtisch.png) + +Der Passkey steht danach in der Liste, mit Anlegedatum und letzter Benutzung. + +> **Lege zwei Passkeys an, auf zwei verschiedenen Geräten.** Sonst sperrst du +> dich aus, sobald ein Gerät verlorengeht oder zurückgesetzt wird. + +Unter jedem Eintrag steht, ob der Passkey **geräteübergreifend** ist — dann +liegt er im Schlüsselbund von Apple, Google oder deinem Passwortmanager und ist +auf deinen anderen Geräten ebenfalls verfügbar. Steht dort **nur dieses +Gerät**, ist er an genau dieses Gerät gebunden. + +## Einen Passkey entfernen + +In den Einstellungen auf das Papierkorb-Symbol neben dem Eintrag. Der Passkey +ist sofort ungültig; auf dem Gerät selbst bleibt ein Rest zurück, den du dort +im Schlüsselbund löschen kannst. + +## Abmelden + +Oben rechts auf das Symbol mit dem Pfeil aus der Tür. + +## Wenn es klemmt + +| Was du siehst | Was dahintersteckt | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| „Anmeldung nicht möglich. Bitte Angaben prüfen." | Diese eine Meldung gilt für alle Fälle: falsches Passwort, unbekannte Adresse, gesperrtes Konto. Das ist Absicht — sonst ließe sich abfragen, wer hier ein Konto hat. | +| Es passiert nichts nach dem Klick auf **Mit Passkey anmelden** | Du hast den Systemdialog abgebrochen. Einfach noch einmal klicken. | +| „Dieses Gerät ist hier schon hinterlegt." | Für dieses Gerät gibt es bereits einen Passkey. Ein zweiter bringt nichts. | +| Der Passkey-Knopf tut gar nichts | Der Browser unterstützt WebAuthn nicht, oder die Seite läuft nicht über HTTPS. Passkeys brauchen eine sichere Verbindung — `localhost` ausgenommen. | +| Passkey lässt sich nicht anlegen | Meist stimmt die Konfiguration nicht: `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. Siehe [README](../../README.md). | + +## Ausgesperrt? + +Wenn kein Weg mehr hineinführt, hilft nur jemand mit Zugriff auf den Server. +Dort lässt sich mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` in der `.env` und einem +erneuten Seed-Lauf wieder ein Passwort setzen — siehe +[Schnellstart, Schritt 5](../SCHNELLSTART.md). + +## Rollen + +Unten in der Fußzeile und in den Einstellungen steht, welche Rolle du hast. + +- **Benutzer** – sieht und plant die eigenen Sachen. +- **Verwaltung** – darf zusätzlich Bereiche ändern und das + [Protokoll](protokoll.md) einsehen. + +Die Rolle vergibt die Verwaltung; sie lässt sich nicht selbst hochstufen. diff --git a/docs/handbuch/bereiche.md b/docs/handbuch/bereiche.md new file mode 100644 index 0000000..136af55 --- /dev/null +++ b/docs/handbuch/bereiche.md @@ -0,0 +1,80 @@ +# Bereiche einrichten + +Ein **Bereich** ist ein Lebensbereich: Firma, Gewerbe, Privat, Homelab. Jede +Aufgabe und jeder Zeitblock gehört später zu genau einem davon. + +Der Bereich bestimmt drei Dinge: + +- **die Farbe**, an der du im Kalender sofort erkennst, worum es geht, +- **das Arbeitsfenster** — die Tageszeit, in der für diesen Bereich überhaupt + geplant wird, +- **die Tageskapazität** — wie viele Minuten je Wochentag höchstens + hineingehen. + +![Die Bereichsseite](../bilder/bereiche-hell-schreibtisch.png) + +> Bereiche ändern darf nur die **Verwaltung**. Als Benutzer siehst du sie, +> aber ohne die Formulare. + +## Einen Bereich anlegen + +1. Im Menü auf **Bereiche**. +2. Ganz unten unter _Neuen Bereich anlegen_ die Felder ausfüllen. +3. Auf **Bereich anlegen** klicken. + +| Feld | Bedeutung | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | +| **Name** | Zwei bis 40 Zeichen, muss eindeutig sein | +| **Farbe** | Wird zum Farbpunkt in Listen und später im Kalender | +| **Arbeitsfenster von / bis** | Zwischen diesen Uhrzeiten wird geplant. Muss nach dem Beginn enden | +| **Tageskapazität** | Minuten je Wochentag. 0 heißt: an diesem Tag gar nicht | +| **In Benutzung** | Abgehakt lassen. Ohne Haken bleibt der Bereich erhalten, taucht aber nicht mehr zur Auswahl auf | +| **Reihenfolge** | Kleinere Zahlen stehen weiter oben | + +## Was eine sinnvolle Kapazität ist + +Nicht die Zeit, die der Tag hergibt — die Zeit, die **wirklich für geplante +Arbeit** übrig bleibt, nach Besprechungen, Störungen und Pausen. + +Ein brauchbarer Anfang: + +| Bereich | Montag bis Donnerstag | Freitag | Wochenende | +| ------- | --------------------- | ------- | --------------- | +| Firma | 360 min (6 h) | 240 min | 0 | +| Gewerbe | 90 min | 60 min | Samstag 180 min | +| Privat | 60 min | 120 min | je 240 min | +| Homelab | 0 bis 60 min | 0 | Samstag 120 min | + +Wenn am Ende der Woche regelmäßig etwas liegenbleibt, ist die Kapazität zu +hoch angesetzt, nicht der Tag zu kurz. + +> Die Tageskapazität darf nie länger sein als das Arbeitsfenster. Ein Fenster +> von 08:00 bis 17:00 fasst höchstens 540 Minuten; trägst du mehr ein, weist +> das Formular es ab. + +## Einen Bereich ändern + +Auf **Bearbeiten** unter dem Bereich klicken, ändern, **Speichern**. Mit +**Schließen** verwirfst du, was du noch nicht gespeichert hast. + +## Einen Bereich loswerden + +Zwei Wege, und meistens ist der erste der richtige: + +1. **Stilllegen** – den Haken bei _In Benutzung_ entfernen und speichern. Alles + Bisherige bleibt erhalten, der Bereich taucht nur nicht mehr zur Auswahl auf. +2. **Löschen** – der rote Knopf. Endgültig. + +> Ab M2 hängen Aufgaben an Bereichen. Dann ist Stilllegen fast immer die +> richtige Wahl, weil beim Löschen auch die Zuordnung verlorenginge. + +Jede Änderung landet im [Protokoll](protokoll.md). + +## Typische Fehler + +| Meldung | Was zu tun ist | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| „Diesen Namen gibt es schon." | Namen sind eindeutig. Vielleicht existiert der Bereich stillgelegt schon. | +| „Das Arbeitsfenster muss nach seinem Beginn enden." | Von-Zeit liegt hinter der Bis-Zeit. Über Mitternacht hinweg geht nicht — dafür lieber zwei Bereiche. | +| „Keine Tageskapazität darf länger sein als das Arbeitsfenster." | Entweder die Kapazität senken oder das Fenster weiten. | +| „Dazu fehlt dir die Berechtigung." | Du bist als Benutzer angemeldet, nicht als Verwaltung. | diff --git a/docs/handbuch/protokoll.md b/docs/handbuch/protokoll.md new file mode 100644 index 0000000..65be422 --- /dev/null +++ b/docs/handbuch/protokoll.md @@ -0,0 +1,57 @@ +# Protokoll + +Jede schreibende Aktion im Planer hinterlässt einen Eintrag: wer, wann, was, +und woran. Einträge lassen sich weder ändern noch löschen. + +Die Seite steht nur der **Verwaltung** offen. Als Benutzer siehst du den +Menüpunkt gar nicht erst; wer die Adresse direkt aufruft, landet auf einem +Hinweis. + +![Die Protokollseite](../bilder/protokoll-hell-schreibtisch.png) + +## Was dort steht + +| Spalte | Bedeutung | +| ------------- | --------------------------------------------------------------------- | +| **Zeitpunkt** | Datum und Uhrzeit in Europe/Berlin | +| **Wer** | Die E-Mail-Adresse zum Zeitpunkt der Aktion | +| **Was** | Die Aktion, in der Form `objekt.verb` | +| **Objekt** | Die Art des betroffenen Objekts, dahinter die fachlichen Einzelheiten | + +Angezeigt werden die letzten 100 Einträge, die neuesten zuerst. + +## Die Aktionen + +| Aktion | Bedeutung | +| ------------------- | --------------------------------------------------- | +| `bereich.angelegt` | Ein neuer Bereich ist entstanden | +| `bereich.geaendert` | Name, Farbe, Fenster oder Kapazität wurden geändert | +| `bereich.geloescht` | Ein Bereich wurde entfernt | +| `passkey.angelegt` | Jemand hat ein Gerät hinterlegt | +| `passkey.entfernt` | Ein Gerät wurde entfernt | + +Mit jedem Meilenstein kommen Aktionen hinzu. + +## Was dort bewusst **nicht** steht + +- Keine Passwörter, Hashes oder Tokens. +- Keine Termin-Betreffs und keine Mailinhalte. +- Keine Zugangsdaten der Kalenderkonten. + +Das ist doppelt abgesichert: Solche Felder werden gar nicht erst übergeben, +und vor dem Speichern ersetzt der Planer zusätzlich jedes Feld, dessen Name +nach einem Geheimnis aussieht, durch `[entfernt]`. Wenn du dort `[entfernt]` +liest, hat dieser zweite Riegel gegriffen. + +## Warum die E-Mail-Adresse und nicht nur die Kennung + +Wird ein Benutzer gelöscht, verschwinden seine Passkeys und verknüpften +Konten — der Protokolleintrag bleibt. Die Verknüpfung zum Konto wird gelöst, +die Adresse bleibt als Nachweis stehen. Sonst wäre das Protokoll nach einer +Löschung wertlos. + +## Aufbewahrung + +Einträge werden nicht automatisch entfernt. Die Tabelle wächst langsam — ein +Eintrag je Änderung, nicht je Seitenaufruf. Sollte sie einmal stören, gehört +das Aufräumen in die Sicherungsroutine und nicht in die Anwendung. From e3390c88f41b35cf46b71950362beb2112cdf8ad Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:29:52 +0200 Subject: [PATCH 13/19] refactor: middleware in proxy umbenennen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next 16 hat die Dateikonvention "middleware" abgekündigt und in "proxy" umbenannt; der Bau warnte bei jedem Lauf. Umbenannt sind Datei und Funktion, die Aufgabe bleibt dieselbe. Die E2E-Tests laufen unverändert durch: die Umleitung samt gemerktem Ziel funktioniert weiter. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/(app)/layout.tsx | 2 +- src/{middleware.ts => proxy.ts} | 4 ++-- src/server/berechtigung.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/{middleware.ts => proxy.ts} (86%) diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 0ad2e81..b59e22c 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -14,7 +14,7 @@ export default async function AngemeldetesLayout({ }: { children: React.ReactNode }) { - // Der Pfad kommt aus der Kopfzeile der Middleware, damit es nach der + // Der Pfad kommt aus der Kopfzeile des Proxys, damit es nach der // Anmeldung dort weitergeht, wo man hinwollte. const benutzer = await verlangeAnmeldung(await angefragterPfad()) const instanz = aktuelleInstanz() diff --git a/src/middleware.ts b/src/proxy.ts similarity index 86% rename from src/middleware.ts rename to src/proxy.ts index 5d982bc..a67268b 100644 --- a/src/middleware.ts +++ b/src/proxy.ts @@ -7,11 +7,11 @@ export const PFAD_KOPFZEILE = 'x-planer-pfad' * * Ein Layout kennt seinen eigenen Pfad nicht, braucht ihn aber, um nach der * Anmeldung wieder dorthin zurückzuführen. Hier steht bewusst keine - * Anmeldelogik: die Middleware läuft im Edge-Umfeld, in dem weder Prisma noch + * Anmeldelogik: der Proxy läuft im Edge-Umfeld, in dem weder Prisma noch * der Adapter verfügbar sind. Geprüft wird weiterhin im Layout, in der Seite * und in jeder Server Action. */ -export function middleware(anfrage: NextRequest) { +export function proxy(anfrage: NextRequest) { const kopfzeilen = new Headers(anfrage.headers) kopfzeilen.set(PFAD_KOPFZEILE, `${anfrage.nextUrl.pathname}${anfrage.nextUrl.search}`) return NextResponse.next({ request: { headers: kopfzeilen } }) diff --git a/src/server/berechtigung.ts b/src/server/berechtigung.ts index a44e1a6..5458a8a 100644 --- a/src/server/berechtigung.ts +++ b/src/server/berechtigung.ts @@ -5,7 +5,7 @@ import type { Session } from 'next-auth' import type { Rolle } from '@/generated/prisma/enums' import { rolleReichtAus } from '@/lib/rollen' import { auth } from '@/server/auth' -import { PFAD_KOPFZEILE } from '@/middleware' +import { PFAD_KOPFZEILE } from '@/proxy' /** * Rollenpruefung. @@ -76,7 +76,7 @@ export async function verlangeAnmeldungFuerApi(): Promise { } /** - * Der angefragte Pfad, den die Middleware durchgereicht hat. Wird gebraucht, + * Der angefragte Pfad, den der Proxy durchgereicht hat. Wird gebraucht, * um nach der Anmeldung wieder dorthin zurueckzufuehren. */ export async function angefragterPfad(): Promise { From a88273117cc3e73d689e86ef5164c5c627c8a89f Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:29:52 +0200 Subject: [PATCH 14/19] chore: Screenshot-Kette und Demo-Seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm run screenshots startet eine Wegwerf-Instanz in einem eigenen Compose-Projekt mit eigener Datenbank, spielt ausschließlich erfundene Daten ein, fährt die Seiten mit Playwright ab und räumt am Ende alles wieder weg. Je Seite entstehen vier Bilder: hell und dunkel, Schreibtisch und Handy. Die Uhr steht fest, damit sich zwischen zwei Läufen nur das ändert, was sich wirklich geändert hat. Der Demo-Seed verweigert den Dienst ohne DEMO_MODUS=true — er würde sonst erfundene Daten in eine echte Datenbank schreiben. Test- und Werkzeugordner sind jetzt aus dem Docker-Kontext ausgenommen. Ein Typfehler in scripts/screenshots.ts hatte den Bau des Abbilds blockiert, obwohl die Datei dort nichts zu suchen hat. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 17 ++- docker-compose.screenshots.yml | 61 +++++++++++ package.json | 3 +- playwright.screenshots.config.ts | 20 ++++ prisma/demo-seed.ts | 172 +++++++++++++++++++++++++++++++ screenshots/bilder.spec.ts | 88 ++++++++++++++++ scripts/screenshots.ts | 142 +++++++++++++++++++++++++ tsconfig.json | 3 +- 8 files changed, 503 insertions(+), 3 deletions(-) create mode 100644 docker-compose.screenshots.yml create mode 100644 playwright.screenshots.config.ts create mode 100644 prisma/demo-seed.ts create mode 100644 screenshots/bilder.spec.ts create mode 100644 scripts/screenshots.ts diff --git a/.dockerignore b/.dockerignore index 9561351..0dafacd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,22 @@ node_modules logs sicherungen coverage + +# Nur für die Entwicklung. Halten den Kontext klein und sorgen dafür, dass ein +# defektes Werkzeugskript nicht den Bau des Abbilds blockiert – geprüft werden +# sie ohnehin von "npm run pruefen". +e2e +screenshots +test test-results playwright-report -docs/bilder +playwright.config.ts +playwright.screenshots.config.ts +vitest.config.ts +docs devlog +*.sh +scripts/pruefen.mjs +scripts/integrationstest.ts +scripts/migrationstest.ts +scripts/screenshots.ts diff --git a/docker-compose.screenshots.yml b/docker-compose.screenshots.yml new file mode 100644 index 0000000..b92667c --- /dev/null +++ b/docker-compose.screenshots.yml @@ -0,0 +1,61 @@ +# Wegwerf-Instanz nur für "npm run screenshots". +# +# Eigenes Compose-Projekt (planer-screenshots), eigenes Volume, eigene Ports. +# Sie kommt der Entwicklungs- oder Betriebsinstanz damit nicht in die Quere und +# wird nach dem Lauf samt Volume wieder entfernt. +services: + db: + image: postgres:16-alpine + restart: 'no' + environment: + POSTGRES_USER: demo + POSTGRES_PASSWORD: demo + POSTGRES_DB: demo + TZ: Europe/Berlin + ports: + - '${SCREENSHOT_DB_PORT:-5455}:5432' + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U demo -d demo'] + interval: 3s + timeout: 3s + retries: 20 + start_period: 10s + + migration: + build: + context: . + dockerfile: Dockerfile + target: bauen + restart: 'no' + depends_on: + db: + condition: service_healthy + environment: + TZ: Europe/Berlin + DATABASE_URL: postgresql://demo:demo@db:5432/demo?schema=public + command: node node_modules/prisma/build/index.js migrate deploy + + app: + build: + context: . + dockerfile: Dockerfile + target: betrieb + restart: 'no' + depends_on: + migration: + condition: service_completed_successfully + environment: + NODE_ENV: production + TZ: Europe/Berlin + INSTANZ: daheim + APP_URL: http://127.0.0.1:${SCREENSHOT_APP_PORT:-3200} + DATABASE_URL: postgresql://demo:demo@db:5432/demo?schema=public + AUTH_SECRET: nur-fuer-die-screenshot-instanz-nicht-geheim + AUTH_TRUST_HOST: 'true' + ANMELDUNG_PASSWORT: 'true' + WEBAUTHN_RP_ID: 127.0.0.1 + WEBAUTHN_RP_NAME: Planer + WEBAUTHN_ORIGIN: http://127.0.0.1:${SCREENSHOT_APP_PORT:-3200} + DEMO_MODUS: 'true' + ports: + - '${SCREENSHOT_APP_PORT:-3200}:3000' diff --git a/package.json b/package.json index 5190880..66aa128 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "db:migrieren": "prisma migrate dev", "db:einspielen": "prisma migrate deploy", "db:seed": "tsx prisma/seed.ts", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "db:demo-seed": "tsx prisma/demo-seed.ts" }, "prisma": { "seed": "tsx prisma/seed.ts" diff --git a/playwright.screenshots.config.ts b/playwright.screenshots.config.ts new file mode 100644 index 0000000..1c0fcda --- /dev/null +++ b/playwright.screenshots.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@playwright/test' + +const PORT = process.env.SCREENSHOT_APP_PORT ?? '3200' + +/** + * Eigene Konfiguration für die Screenshot-Instanz. Sie startet keinen Server — + * darum kümmert sich scripts/screenshots.ts über Docker Compose. + */ +export default defineConfig({ + testDir: './screenshots', + fullyParallel: false, + workers: 1, + reporter: 'list', + timeout: 60_000, + use: { + baseURL: `http://127.0.0.1:${PORT}`, + locale: 'de-DE', + timezoneId: 'Europe/Berlin', + }, +}) diff --git a/prisma/demo-seed.ts b/prisma/demo-seed.ts new file mode 100644 index 0000000..2b5592c --- /dev/null +++ b/prisma/demo-seed.ts @@ -0,0 +1,172 @@ +/** + * Daten für die Screenshot-Instanz. + * + * Alles hier ist frei erfunden. Diese Datei darf niemals gegen eine + * Datenbank mit echten Daten laufen — deshalb bricht sie ab, wenn + * DEMO_MODUS nicht ausdrücklich auf true steht. + * + * Aufruf: über npm run screenshots, nicht von Hand. + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +if (process.env.DEMO_MODUS !== 'true') { + console.error( + 'Der Demo-Seed läuft nur mit DEMO_MODUS=true. Das ist Absicht: er würde ' + + 'sonst erfundene Daten in eine echte Datenbank schreiben.', + ) + process.exit(1) +} + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + console.error('DATABASE_URL ist nicht gesetzt.') + process.exit(1) +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +export const DEMO_EMAIL = 'martina.beispiel@planer.invalid' +export const DEMO_PASSWORT = 'demo-instanz-nur-fuer-bilder' + +const BEREICHE = [ + { + name: 'Firma', + farbe: '#2563eb', + reihenfolge: 1, + arbeitsfensterVon: 450, + arbeitsfensterBis: 990, + kapazitaetMo: 360, + kapazitaetDi: 360, + kapazitaetMi: 360, + kapazitaetDo: 360, + kapazitaetFr: 240, + kapazitaetSa: 0, + kapazitaetSo: 0, + }, + { + name: 'Gewerbe', + farbe: '#7c3aed', + reihenfolge: 2, + arbeitsfensterVon: 1020, + arbeitsfensterBis: 1260, + kapazitaetMo: 90, + kapazitaetDi: 90, + kapazitaetMi: 90, + kapazitaetDo: 90, + kapazitaetFr: 60, + kapazitaetSa: 180, + kapazitaetSo: 0, + }, + { + name: 'Privat', + farbe: '#16a34a', + reihenfolge: 3, + arbeitsfensterVon: 1020, + arbeitsfensterBis: 1320, + kapazitaetMo: 60, + kapazitaetDi: 60, + kapazitaetMi: 60, + kapazitaetDo: 60, + kapazitaetFr: 120, + kapazitaetSa: 240, + kapazitaetSo: 240, + }, + { + name: 'Homelab', + farbe: '#ea580c', + reihenfolge: 4, + arbeitsfensterVon: 1140, + arbeitsfensterBis: 1380, + kapazitaetMo: 0, + kapazitaetDi: 60, + kapazitaetMi: 0, + kapazitaetDo: 60, + kapazitaetFr: 0, + kapazitaetSa: 120, + kapazitaetSo: 60, + }, +] + +/** Erfundene Protokolleinträge, damit die Protokollseite nicht leer wirkt. */ +const PROTOKOLL = [ + { aktion: 'bereich.angelegt', objektTyp: 'Bereich', details: { name: 'Homelab' } }, + { + aktion: 'bereich.geaendert', + objektTyp: 'Bereich', + details: { vorher: 'Nebenerwerb', nachher: 'Gewerbe' }, + }, + { + aktion: 'passkey.angelegt', + objektTyp: 'Passkey', + details: { bezeichnung: 'Diensthandy' }, + }, +] + +async function main(): Promise { + console.log('Demo-Daten einspielen.') + + for (const bereich of BEREICHE) { + await db.bereich.upsert({ + where: { name: bereich.name }, + update: bereich, + create: bereich, + }) + } + + const benutzer = await db.benutzer.upsert({ + where: { email: DEMO_EMAIL }, + update: { rolle: 'ADMIN', name: 'Martina Beispiel' }, + create: { + email: DEMO_EMAIL, + name: 'Martina Beispiel', + rolle: 'ADMIN', + passwortHash: await bcrypt.hash(DEMO_PASSWORT, 10), + }, + }) + + await db.auditEintrag.deleteMany({}) + // Feste Zeitpunkte, damit die Bilder zwischen zwei Läufen gleich bleiben. + let versatz = 0 + for (const eintrag of PROTOKOLL) { + versatz += 37 + await db.auditEintrag.create({ + data: { + zeitpunkt: new Date(Date.UTC(2026, 2, 17, 9, versatz % 60)), + benutzerId: benutzer.id, + benutzerEmail: benutzer.email, + aktion: eintrag.aktion, + objektTyp: eintrag.objektTyp, + details: eintrag.details, + instanz: 'daheim', + }, + }) + } + + await db.passkey.deleteMany({ where: { benutzerId: benutzer.id } }) + await db.passkey.create({ + data: { + benutzerId: benutzer.id, + credentialId: 'demo-passkey-diensthandy', + publicKey: Buffer.from([0]), + bezeichnung: 'Diensthandy', + gesichert: true, + erstelltAm: new Date(Date.UTC(2026, 1, 3, 8, 15)), + zuletztBenutzt: new Date(Date.UTC(2026, 2, 17, 7, 42)), + }, + }) + + console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`) +} + +main() + .then(async () => { + await db.$disconnect() + }) + .catch(async (fehler: unknown) => { + console.error(fehler) + await db.$disconnect() + process.exit(1) + }) diff --git a/screenshots/bilder.spec.ts b/screenshots/bilder.spec.ts new file mode 100644 index 0000000..067f83f --- /dev/null +++ b/screenshots/bilder.spec.ts @@ -0,0 +1,88 @@ +import { mkdir } from 'node:fs/promises' +import { test, type Page } from '@playwright/test' + +/** + * Nimmt die Bilder für das Handbuch auf. + * + * Je Seite vier Bilder: hell und dunkel, Schreibtisch und Handy. Die Uhr steht + * auf einem festen Zeitpunkt, damit sich zwischen zwei Läufen nur das ändert, + * was sich wirklich geändert hat. + */ + +const ZIEL = 'docs/bilder' +const SCHREIBTISCH = { width: 1440, height: 900 } +const HANDY = { width: 390, height: 844 } + +// Dienstag, 17. März 2026, 09:20 Uhr in Berlin. +const FESTE_ZEIT = new Date('2026-03-17T08:20:00.000Z') + +const EMAIL = 'martina.beispiel@planer.invalid' +const PASSWORT = 'demo-instanz-nur-fuer-bilder' + +type Aufnahme = { + name: string + pfad: string + /** Wird vor der Aufnahme ausgeführt, z. B. um ein Formular aufzuklappen. */ + vorbereiten?: (seite: Page) => Promise +} + +const AUFNAHMEN: Aufnahme[] = [ + { name: 'anmelden', pfad: '/anmelden' }, + { name: 'start', pfad: '/' }, + { name: 'bereiche', pfad: '/bereiche' }, + { name: 'einstellungen', pfad: '/einstellungen' }, + { name: 'protokoll', pfad: '/verwaltung/protokoll' }, +] + +async function anmelden(seite: Page): Promise { + await seite.goto('/anmelden') + await seite.getByLabel('E-Mail-Adresse').fill(EMAIL) + await seite.getByLabel('Passwort').fill(PASSWORT) + await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await seite.waitForURL('/') +} + +test.beforeAll(async () => { + await mkdir(ZIEL, { recursive: true }) +}) + +for (const schema of ['hell', 'dunkel'] as const) { + for (const [geraet, groesse] of [ + ['schreibtisch', SCHREIBTISCH], + ['handy', HANDY], + ] as const) { + test(`Bilder: ${schema}, ${geraet}`, async ({ browser }) => { + const kontext = await browser.newContext({ + viewport: groesse, + colorScheme: schema === 'dunkel' ? 'dark' : 'light', + deviceScaleFactor: 2, + }) + const seite = await kontext.newPage() + await seite.clock.setFixedTime(FESTE_ZEIT) + + for (const aufnahme of AUFNAHMEN) { + if (aufnahme.pfad !== '/anmelden') { + // Für alles außer der Anmeldeseite braucht es eine Sitzung. + if ( + !(await seite.context().cookies()).some((k) => k.name.includes('session')) + ) { + await anmelden(seite) + } + } + + await seite.goto(aufnahme.pfad) + await seite.waitForLoadState('networkidle') + await aufnahme.vorbereiten?.(seite) + // Der Farbschema-Umschalter und Schriften sollen fertig sein. + await seite.waitForTimeout(300) + + await seite.screenshot({ + path: `${ZIEL}/${aufnahme.name}-${schema}-${geraet}.png`, + fullPage: true, + }) + } + + await kontext.close() + }) + } +} diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..ede8135 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,142 @@ +/** + * Nimmt die Bilder für das Handbuch auf. + * + * Startet eine Wegwerf-Instanz in einem eigenen Compose-Projekt mit eigener + * Datenbank, spielt ausschließlich erfundene Daten ein, fährt die Seiten mit + * Playwright ab und räumt am Ende alles wieder weg — auch wenn unterwegs etwas + * schiefgeht. + * + * An eine Instanz mit echten Daten kommt dieser Ablauf nicht heran: die + * Datenbank entsteht neu, wird nur über ihren eigenen Port angesprochen und + * samt Volume wieder entfernt. + * + * Aufruf: npm run screenshots + */ +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const WURZEL = fileURLToPath(new URL('..', import.meta.url)) +const PROJEKT = 'planer-screenshots' +const COMPOSE_DATEI = 'docker-compose.screenshots.yml' + +const APP_PORT = process.env.SCREENSHOT_APP_PORT ?? '3200' +const DB_PORT = process.env.SCREENSHOT_DB_PORT ?? '5455' + +const UMGEBUNG = { + ...process.env, + SCREENSHOT_APP_PORT: APP_PORT, + SCREENSHOT_DB_PORT: DB_PORT, +} + +function meldung(text: string): void { + console.log(`▸ ${text}`) +} + +function laufen( + befehl: string, + argumente: string[], + zusatz: Record = {}, +): number { + const ergebnis = spawnSync(befehl, argumente, { + cwd: WURZEL, + stdio: 'inherit', + env: { ...UMGEBUNG, ...zusatz }, + }) + return ergebnis.status ?? 1 +} + +function compose(argumente: string[]): number { + return laufen('docker', ['compose', '-p', PROJEKT, '-f', COMPOSE_DATEI, ...argumente]) +} + +function node(skript: string, zusatz: Record): number { + return laufen( + process.execPath, + [`${WURZEL}node_modules/tsx/dist/cli.mjs`, skript], + zusatz, + ) +} + +async function wartenAufGesundheit(sekunden: number): Promise { + const ziel = `http://127.0.0.1:${APP_PORT}/api/health` + const frist = Date.now() + sekunden * 1000 + + while (Date.now() < frist) { + try { + const antwort = await fetch(ziel) + if (antwort.ok) { + const inhalt = (await antwort.json()) as { status?: string } + if (inhalt.status === 'ok') return true + } + } catch { + // Noch nicht da, gleich noch einmal. + } + await new Promise((weiter) => setTimeout(weiter, 2000)) + } + return false +} + +function aufraeumen(): void { + meldung('Wegwerf-Instanz entfernen.') + compose(['down', '-v', '--remove-orphans']) +} + +async function main(): Promise { + if (laufen('docker', ['info']) !== 0) { + console.error('Der Docker-Daemon läuft nicht.') + return 1 + } + + // Reste eines abgebrochenen Laufs zuerst wegräumen. + compose(['down', '-v', '--remove-orphans']) + + meldung(`Wegwerf-Instanz bauen und starten (App auf ${APP_PORT}, DB auf ${DB_PORT}).`) + if (compose(['up', '-d', '--build']) !== 0) { + console.error('Die Wegwerf-Instanz ließ sich nicht starten.') + return 1 + } + + meldung('Auf die Anwendung warten.') + if (!(await wartenAufGesundheit(180))) { + console.error('Die Anwendung ist nicht gesund geworden.') + compose(['logs', '--tail', '50', 'app']) + return 1 + } + + meldung('Erfundene Daten einspielen.') + const demoUrl = `postgresql://demo:demo@127.0.0.1:${DB_PORT}/demo?schema=public` + if (node('prisma/demo-seed.ts', { DATABASE_URL: demoUrl, DEMO_MODUS: 'true' }) !== 0) { + console.error('Der Demo-Seed ist fehlgeschlagen.') + return 1 + } + + meldung('Bilder aufnehmen.') + const playwright = laufen( + process.execPath, + [ + `${WURZEL}node_modules/@playwright/test/cli.js`, + 'test', + '--config', + 'playwright.screenshots.config.ts', + ], + { SCREENSHOT_APP_PORT: APP_PORT }, + ) + if (playwright !== 0) { + console.error('Die Aufnahme ist fehlgeschlagen.') + return 1 + } + + meldung('Fertig. Die Bilder liegen in docs/bilder/.') + return 0 +} + +let code = 1 +try { + code = await main() +} catch (fehler) { + console.error('Der Screenshot-Lauf ist abgebrochen:', fehler) +} finally { + aufraeumen() +} + +process.exit(code) diff --git a/tsconfig.json b/tsconfig.json index e59dc59..519efb5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,8 +36,9 @@ "**/*.tsx", ".next/types/**/*.ts", "vitest.config.ts", + ".next/dev/types/**/*.ts", "playwright.config.ts", - ".next/dev/types/**/*.ts" + "playwright.screenshots.config.ts" ], "exclude": ["node_modules", ".next", "out"] } From cf28ec19f597fe91ab67bed7f4a3d2d7f6c92338 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:32:52 +0200 Subject: [PATCH 15/19] test: Rollentrennung end-to-end absichern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legt einen Benutzer ohne Verwaltungsrechte an und prüft, dass der direkte Aufruf der Protokollseite auf den Hinweis führt — nicht nur, dass der Menüpunkt fehlt. Sich darauf zu verlassen, dass die Oberfläche Knöpfe ausblendet, wäre kein Nachweis. Co-Authored-By: Claude Opus 5 (1M context) --- e2e/rollen.spec.ts | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 e2e/rollen.spec.ts diff --git a/e2e/rollen.spec.ts b/e2e/rollen.spec.ts new file mode 100644 index 0000000..db89306 --- /dev/null +++ b/e2e/rollen.spec.ts @@ -0,0 +1,74 @@ +import { PrismaPg } from '@prisma/adapter-pg' +import { expect, test, type Page } from '@playwright/test' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +/** + * Prüft, dass die Rollentrennung serverseitig hält. + * + * Die Oberfläche blendet Knöpfe aus — darauf allein darf man sich nicht + * verlassen. Deshalb ruft dieser Test die Verwaltungsseite direkt auf, statt + * nach einem Menüpunkt zu suchen. + */ + +const EMAIL = 'zzz-e2e-benutzer@example.invalid' +const PASSWORT = 'nur-fuer-den-test-1234' + +const verbindung = process.env.DATABASE_URL ?? '' +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +test.describe('Rollentrennung', () => { + test.skip(!verbindung, 'DATABASE_URL ist nicht gesetzt.') + + test.beforeAll(async () => { + await db.benutzer.deleteMany({ where: { email: EMAIL } }) + await db.benutzer.create({ + data: { + email: EMAIL, + name: 'Testbenutzer ohne Rechte', + rolle: 'BENUTZER', + passwortHash: await bcrypt.hash(PASSWORT, 10), + }, + }) + }) + + test.afterAll(async () => { + await db.auditEintrag.deleteMany({ where: { benutzerEmail: EMAIL } }) + await db.benutzer.deleteMany({ where: { email: EMAIL } }) + await db.$disconnect() + }) + + async function anmelden(seite: Page): Promise { + await seite.goto('/anmelden') + await seite.getByLabel('E-Mail-Adresse').fill(EMAIL) + await seite.getByLabel('Passwort').fill(PASSWORT) + await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click() + await expect(seite.getByRole('heading', { name: /Guten Tag/ })).toBeVisible() + } + + test('zeigt einem Benutzer den Menüpunkt Protokoll gar nicht', async ({ page }) => { + await anmelden(page) + await expect(page.getByRole('link', { name: 'Protokoll' })).toHaveCount(0) + }) + + test('lässt einen Benutzer auch beim direkten Aufruf nicht ins Protokoll', async ({ + page, + }) => { + await anmelden(page) + await page.goto('/verwaltung/protokoll') + + await expect(page).toHaveURL(/\/keine-berechtigung/) + await expect( + page.getByRole('heading', { name: /Dazu fehlt dir die Berechtigung/ }), + ).toBeVisible() + }) + + test('zeigt einem Benutzer die Bereiche, aber ohne Formulare', async ({ page }) => { + await anmelden(page) + await page.goto('/bereiche') + + await expect(page.getByRole('heading', { name: 'Bereiche' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Bearbeiten' })).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Bereich anlegen' })).toHaveCount(0) + }) +}) From 6d04bc2c1ce7017379124eb2efdabc81254ca94e Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Thu, 10 Sep 2026 14:36:35 +0200 Subject: [PATCH 16/19] fix(oberflaeche): Kopfleiste einzeilig halten, Demo-Uhr feststellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Mängel, die erst in den aufgenommenen Bildern auffielen. Auf Handybreite brach die Kopfleiste um und drängte den Namen der Anwendung in die zweite Zeile, mitten zwischen die Menüpunkte. Sie bleibt jetzt auf allen Breiten einzeilig; wird es eng, scrollt die Navigation waagerecht. Das Tagesdatum auf der Startseite wird serverseitig gerendert, weshalb Playwrights Uhr nicht griff — die Bilder hätten sich täglich geändert. In der Screenshot-Instanz (DEMO_MODUS=true) steht die Uhr jetzt fest. Reine Funktionen bekommen den Zeitpunkt weiterhin als Parameter; src/server/ zeitgeber.ts ist die eine Stelle, an der er entsteht. Co-Authored-By: Claude Opus 5 (1M context) --- devlog/DEVLOG.md | 118 ++++++++++++++++++++++++++++++++++ src/app/(app)/page.tsx | 5 +- src/components/kopfleiste.tsx | 18 ++++-- src/server/zeitgeber.ts | 20 ++++++ 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 devlog/DEVLOG.md create mode 100644 src/server/zeitgeber.ts diff --git a/devlog/DEVLOG.md b/devlog/DEVLOG.md new file mode 100644 index 0000000..f488341 --- /dev/null +++ b/devlog/DEVLOG.md @@ -0,0 +1,118 @@ +# Entwicklungstagebuch + +Neuester Eintrag oben. Die Dauer je Sitzung stammt aus `devlog/zeiten.csv`, +die Übersicht aus `scripts/zeit.sh summe`. + +## Zeit je Meilenstein + +| Meilenstein | Art | Minuten | Stunden | +| ----------- | ------ | ----------------------------------- | ------- | +| M1 | claude | _(wird beim Abschluss eingetragen)_ | | + +--- + +## 2026-09-10 – M1: Fundament + +**Ziel der Sitzung:** Das Projekt von Grund auf aufsetzen: Gerüst, +Datenbank, Anmeldung, Rollen, Audit, Instanzsteuerung, Betrieb und +Dokumentation. + +### Erledigt + +- Projektgerüst mit Next 16, React 19, TypeScript strict, Tailwind 4, + alle Versionen exakt gepinnt +- `npm run pruefen` als Sammelprüfung: Prettier, ESLint, `tsc`, Vitest, + ShellCheck +- Datenmodell und erste Migration: Benutzer mit Rolle, OIDC-Konten, + Passkeys, WebAuthn-Herausforderungen, Bereiche, Audit +- Anmeldung über OIDC, Passkey und Passwort, je Instanz zuschaltbar +- Rollenprüfung in Seite **und** Server Action, Audit je Schreibaktion +- Instanzsteuerung mit 404 statt Hinweis +- Bereiche verwalten, Protokollseite, Startseite, Einstellungen +- `/api/health`, Dockerfile, Compose-Stapel mit Migrationsdienst +- `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh` +- Unit-, Integrations-, Migrations- und E2E-Tests +- Screenshot-Kette mit eigener Wegwerf-Instanz +- README, Schnellstart, Handbuch, ROADMAP, CHANGELOG + +### Entscheidungen mit Begründung + +**Prisma 7.10.0 statt der notierten Fassung 6.** Fassung 6 ist überholt, +Fassung 8 liegt nur als RC auf `latest`. Prisma 7 nimmt die Verbindung +nicht mehr im Schema entgegen — sie steht in `prisma.config.ts`, der +Client bekommt den pg-Treiberadapter. + +**TypeScript 6.0.3 statt 7.0.2.** TS 7 ist zwar `latest`, aber +`typescript-eslint@8.70` verlangt `typescript >=4.8.4 <6.1.0`. Mit TS 7 +wäre die Lint-Kette kaputt gewesen. + +**ESLint 9.39.5 statt 10.** Das in `eslint-config-next@16` gebündelte +`eslint-plugin-react` ruft noch `context.getFilename()`, das ESLint 10 +entfernt hat. Der Lint-Lauf brach reproduzierbar ab. + +**Eigener Auth.js-Adapter.** Der mitgelieferte `@auth/prisma-adapter` +greift fest auf `prisma.user` und `prisma.account` zu; unsere Modelle +heißen `Benutzer` und `Konto`. Der Adapter ist schmal und ersparte es, +die Fachbegriffe der Konvention zu opfern. + +**Passkeys direkt über SimpleWebAuthn 14.** Der WebAuthn-Provider von +Auth.js ist als experimentell markiert und auf SimpleWebAuthn 9 +festgelegt. Direkt angebunden bleibt die Fassung aktuell und der Ablauf +nachvollziehbar. Für die npm-Auflösung brauchte es `overrides`, weil +next-auth die älteren Fassungen als optionale Peers deklariert. + +**Sitzungen als JWT, Rolle trotzdem aus der Datenbank.** Credentials- +Provider verlangen JWT. Damit eine Sperre nicht bis zum Ablauf des Tokens +wirkungslos bleibt, liest der `session`-Callback Rolle und Status bei +jeder Anfrage frisch. Die Tabellen `sitzung` und `verifizierungs_token` +wären damit tot und wurden wieder entfernt, solange es nur eine einzige +Migration gab. + +**Migrationen als eigener Compose-Dienst.** Der Versuch, nur die +Prisma-CLI ins schlanke Laufzeitabbild zu kopieren, scheiterte an ihren +transitiven Modulen (`effect` über `@prisma/config`). Alle mitzunehmen +hätte das Abbild aufgebläht. Ein Einmaldienst aus der Bau-Stufe läuft vor +der App und muss sauber durchlaufen. + +**Aufgabe, Zeitblock und Kalenderkonto bewusst nicht modelliert.** Sie +kommen mit M2, M4 und M3 — dann mit den Feldern, die der jeweilige +Provider wirklich braucht, statt heute geraten. + +### Probleme und Lösungen + +| Problem | Lösung | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `prisma validate` lehnte `url` im Schema ab | Prisma 7 erwartet die Verbindung in `prisma.config.ts` und einen Treiberadapter im Client. | +| Integrationstest brach nach der ersten erwarteten Verletzung ab | In PostgreSQL bricht ein fehlgeschlagener Befehl die ganze Transaktion ab. Der Test läuft jetzt ohne Sammeltransaktion; alle Testdaten tragen eine Kennung und werden am Ende entfernt. | +| `prisma migrate diff` kannte `--from-url` nicht | In Prisma 7 heißen die Flaggen anders. Der Test vergleicht jetzt die eingespielte Wegwerf-Datenbank über `--from-config-datasource` gegen das Schema — und braucht damit keine Schattendatenbank. | +| Nach der Anmeldung ging es nicht zum ursprünglichen Ziel zurück | Das Layout leitet um, bevor die Seite drankommt, und kennt seinen Pfad nicht. Ein schlanker Proxy reicht ihn als Kopfzeile durch — ohne Anmeldelogik, weil er im Edge-Umfeld läuft. | +| Der Docker-Bau brach beim Sammeln der Seitendaten ab | Prisma-Client und Auth.js-Konfiguration wurden beim Laden des Moduls ausgewertet und brauchten Umgebungsvariablen, die es zur Bauzeit nicht gibt. Beide entstehen jetzt faul. | +| Ein Typfehler in `scripts/screenshots.ts` blockierte den Abbild-Bau | Entwicklungsskripte gehören nicht in den Docker-Kontext. `.dockerignore` schließt sie aus; geprüft werden sie von `npm run pruefen`. | +| ESLint beanstandete `setState` im Effekt | Der Hydratations-Trick im Farbschema-Knopf war unnötig: `next-themes` setzt die `dark`-Klasse vor der Hydratation, das erledigt CSS ohne Zustand. | + +### Eigene Fehler, die ich korrigiert habe + +- Die ersten vier Commits lagen direkt auf `main` statt auf einem + Meilenstein-Zweig und ohne Attributionszeile. Historie umgebaut: + `main` trägt nur noch einen leeren Wurzel-Commit, die Arbeit liegt auf + `m1-fundament`. +- Ein `git add -A` zog die Proxy-Umbenennung in den Doku-Commit. Wieder + getrennt. +- `scripts/screenshots.ts` wurde geschrieben, ohne danach `npm run +pruefen` laufen zu lassen; der Typfehler fiel erst im Docker-Bau auf. + +### Offene Punkte + +- Die `origin`-URL fehlt noch. Push und Merge stehen ohnehin erst nach + der Freigabe an. +- Der Schnellstart endet beim Passkey statt beim ersten Kalenderkonto und + der ersten Aufgabe — beides gibt es erst mit M3 und M2. +- Der Demo-Seed enthält noch keine erfundenen Termine; einen + Demo-Kalenderprovider gibt es erst mit M3. +- Lokal weichen die Ports ab (Datenbank 5435, App 3001), weil 5432 und + 3000 auf diesem Rechner belegt sind. In `.env.example` stehen die + üblichen Werte. + +### Commits + +Siehe `git log --oneline main..m1-fundament`. diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx index 5d1b92a..5ca34da 100644 --- a/src/app/(app)/page.tsx +++ b/src/app/(app)/page.tsx @@ -10,10 +10,11 @@ import { wochenKapazitaet } from '@/lib/bereich' import { alsDatum, alsDauer } from '@/lib/zeit' import { verlangeAnmeldung } from '@/server/berechtigung' import { db } from '@/server/db' +import { jetzt } from '@/server/zeitgeber' export default async function StartSeite() { const benutzer = await verlangeAnmeldung('/') - const jetzt = new Date() + const heute = jetzt() const bereiche = await db.bereich.findMany({ where: { aktiv: true }, @@ -26,7 +27,7 @@ export default async function StartSeite() {

Guten Tag, {anrede}

-

{alsDatum(jetzt)}

+

{alsDatum(heute)}

diff --git a/src/components/kopfleiste.tsx b/src/components/kopfleiste.tsx index c863e1a..7cff907 100644 --- a/src/components/kopfleiste.tsx +++ b/src/components/kopfleiste.tsx @@ -35,12 +35,20 @@ export function Kopfleiste({ return (
-
- + {/* + Bewusst eine einzige Zeile auf allen Breiten: Wird es eng, scrollt die + Navigation waagerecht, statt umzubrechen und den Namen der Anwendung in + die zweite Zeile zu drängen. + */} +
+ Planer -