From 59bb5a38a338b2cd421850dc793c86506efdf409 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sun, 27 Sep 2026 06:06:56 +0200 Subject: [PATCH] first commit --- .dockerignore | 19 + .env.example | 55 + .gitea/workflows/docker-build.yml | 105 + .gitignore | 22 + .impeccable/config.json | 14 + Dockerfile | 62 + README.md | 59 + Ziel | 437 + docker-compose.yml | 55 + docs/ARCHITEKTUR.md | 86 + docs/BETRIEB.md | 117 + docs/OFFENE-PUNKTE.md | 41 + docs/TESTS.md | 53 + e2e/onepage.spec.ts | 77 + eslint.config.mjs | 9 + k8s/base/app.yaml | 129 + k8s/base/config.yaml | 61 + k8s/base/jobs.yaml | 159 + k8s/base/kustomization.yaml | 14 + k8s/base/postgres.yaml | 82 + k8s/overlays/production/ingress.yaml | 64 + k8s/overlays/production/kustomization.yaml | 27 + k8s/secret.example.yaml | 28 + next.config.ts | 56 + package.json | 59 + playwright.config.ts | 24 + pnpm-lock.yaml | 7177 +++++++++++++++++ pnpm-workspace.yaml | 9 + postcss.config.mjs | 7 + prisma.config.ts | 15 + .../20260926231703_init/migration.sql | 528 ++ .../migration.sql | 4 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 529 ++ scripts/create-admin.ts | 54 + scripts/dev-db.mjs | 49 + scripts/seed-dev.ts | 40 + scripts/worker.ts | 34 + src/app/admin/(bereich)/SignOutButton.tsx | 21 + .../admin/(bereich)/buchungen/[id]/page.tsx | 206 + src/app/admin/(bereich)/buchungen/page.tsx | 89 + .../admin/(bereich)/einstellungen/page.tsx | 131 + .../admin/(bereich)/gutscheine/[id]/page.tsx | 42 + src/app/admin/(bereich)/gutscheine/page.tsx | 74 + src/app/admin/(bereich)/layout.tsx | 44 + src/app/admin/(bereich)/page.tsx | 138 + src/app/admin/actions.ts | 152 + src/app/admin/anmelden/LoginForm.tsx | 87 + src/app/admin/anmelden/page.tsx | 14 + src/app/admin/export/route.ts | 55 + src/app/admin/layout.tsx | 10 + src/app/admin/sicherheit/TwoFactorSetup.tsx | 92 + src/app/admin/sicherheit/page.tsx | 29 + src/app/api/auth/[...all]/route.ts | 10 + src/app/api/booking/checkout/route.ts | 55 + src/app/api/booking/quote/route.ts | 33 + src/app/api/booking/status-action/route.ts | 57 + src/app/api/health/ready/route.ts | 11 + src/app/api/health/route.ts | 4 + src/app/api/stripe/webhook/route.ts | 63 + .../buchung/status/[token]/StatusActions.tsx | 119 + src/app/buchung/status/[token]/page.tsx | 149 + src/app/buchungsbedingungen/page.tsx | 24 + src/app/datenschutz/page.tsx | 33 + src/app/globals.css | 251 + src/app/impressum/page.tsx | 38 + src/app/layout.tsx | 59 + src/app/media/[...path]/route.ts | 70 + src/app/page.tsx | 39 + src/app/robots.ts | 13 + src/components/EventJsonLd.tsx | 35 + src/components/Gallery.tsx | 116 + src/components/Hero.tsx | 88 + src/components/LegalPage.tsx | 22 + src/components/MediaFrame.tsx | 87 + src/components/Reveal.tsx | 26 + src/components/SiteFooter.tsx | 47 + src/components/SiteHeader.tsx | 114 + src/components/VideoSlot.tsx | 88 + src/components/admin/AdminUi.tsx | 81 + src/components/admin/VoucherForm.tsx | 28 + src/components/booking/BookingDialog.tsx | 574 ++ src/components/booking/BookingProvider.tsx | 61 + src/components/booking/MobileBookingBar.tsx | 69 + src/components/sections/CampSections.tsx | 139 + src/components/sections/FaqSection.tsx | 108 + src/components/sections/PeopleSections.tsx | 93 + src/components/sections/PricingSection.tsx | 104 + src/content/site.ts | 142 + src/lib/admin-auth.ts | 43 + src/lib/admin-queries.ts | 57 + src/lib/admin-service.ts | 247 + src/lib/api.ts | 81 + src/lib/audit.ts | 26 + src/lib/auth-client.ts | 9 + src/lib/auth.ts | 76 + src/lib/booking-service.ts | 261 + src/lib/camp.ts | 87 + src/lib/checkout.ts | 218 + src/lib/db.ts | 23 + src/lib/email-templates.ts | 165 + src/lib/email.ts | 135 + src/lib/env.ts | 92 + src/lib/labels.ts | 41 + src/lib/money.ts | 29 + src/lib/payment-events.ts | 379 + src/lib/payment-gateway.ts | 264 + src/lib/payment-plan.ts | 98 + src/lib/pricing.ts | 81 + src/lib/public-page.ts | 73 + src/lib/quote.ts | 98 + src/lib/rate-limit.ts | 63 + src/lib/sales-gate.ts | 133 + src/lib/time.ts | 132 + src/lib/tokens.ts | 35 + src/lib/vouchers.ts | 121 + src/lib/webhook.ts | 252 + src/lib/worker-jobs.ts | 298 + src/lib/zod-de.ts | 11 + src/proxy.ts | 79 + tests/admin.integration.test.ts | 102 + tests/booking.integration.test.ts | 173 + tests/payment-plan.test.ts | 65 + tests/payments.integration.test.ts | 333 + tests/pricing.test.ts | 47 + tests/sales-gate.test.ts | 101 + tests/support/db.ts | 56 + tests/support/empty.ts | 1 + tests/support/fake-gateway.ts | 143 + tests/support/global-setup.ts | 57 + tsconfig.json | 30 + vitest.config.ts | 23 + 132 files changed, 19435 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/docker-build.yml create mode 100644 .gitignore create mode 100644 .impeccable/config.json create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 Ziel create mode 100644 docker-compose.yml create mode 100644 docs/ARCHITEKTUR.md create mode 100644 docs/BETRIEB.md create mode 100644 docs/OFFENE-PUNKTE.md create mode 100644 docs/TESTS.md create mode 100644 e2e/onepage.spec.ts create mode 100644 eslint.config.mjs create mode 100644 k8s/base/app.yaml create mode 100644 k8s/base/config.yaml create mode 100644 k8s/base/jobs.yaml create mode 100644 k8s/base/kustomization.yaml create mode 100644 k8s/base/postgres.yaml create mode 100644 k8s/overlays/production/ingress.yaml create mode 100644 k8s/overlays/production/kustomization.yaml create mode 100644 k8s/secret.example.yaml create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 postcss.config.mjs create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20260926231703_init/migration.sql create mode 100644 prisma/migrations/20260927025745_better_auth_two_factor/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 scripts/create-admin.ts create mode 100644 scripts/dev-db.mjs create mode 100644 scripts/seed-dev.ts create mode 100644 scripts/worker.ts create mode 100644 src/app/admin/(bereich)/SignOutButton.tsx create mode 100644 src/app/admin/(bereich)/buchungen/[id]/page.tsx create mode 100644 src/app/admin/(bereich)/buchungen/page.tsx create mode 100644 src/app/admin/(bereich)/einstellungen/page.tsx create mode 100644 src/app/admin/(bereich)/gutscheine/[id]/page.tsx create mode 100644 src/app/admin/(bereich)/gutscheine/page.tsx create mode 100644 src/app/admin/(bereich)/layout.tsx create mode 100644 src/app/admin/(bereich)/page.tsx create mode 100644 src/app/admin/actions.ts create mode 100644 src/app/admin/anmelden/LoginForm.tsx create mode 100644 src/app/admin/anmelden/page.tsx create mode 100644 src/app/admin/export/route.ts create mode 100644 src/app/admin/layout.tsx create mode 100644 src/app/admin/sicherheit/TwoFactorSetup.tsx create mode 100644 src/app/admin/sicherheit/page.tsx create mode 100644 src/app/api/auth/[...all]/route.ts create mode 100644 src/app/api/booking/checkout/route.ts create mode 100644 src/app/api/booking/quote/route.ts create mode 100644 src/app/api/booking/status-action/route.ts create mode 100644 src/app/api/health/ready/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/stripe/webhook/route.ts create mode 100644 src/app/buchung/status/[token]/StatusActions.tsx create mode 100644 src/app/buchung/status/[token]/page.tsx create mode 100644 src/app/buchungsbedingungen/page.tsx create mode 100644 src/app/datenschutz/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/impressum/page.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/media/[...path]/route.ts create mode 100644 src/app/page.tsx create mode 100644 src/app/robots.ts create mode 100644 src/components/EventJsonLd.tsx create mode 100644 src/components/Gallery.tsx create mode 100644 src/components/Hero.tsx create mode 100644 src/components/LegalPage.tsx create mode 100644 src/components/MediaFrame.tsx create mode 100644 src/components/Reveal.tsx create mode 100644 src/components/SiteFooter.tsx create mode 100644 src/components/SiteHeader.tsx create mode 100644 src/components/VideoSlot.tsx create mode 100644 src/components/admin/AdminUi.tsx create mode 100644 src/components/admin/VoucherForm.tsx create mode 100644 src/components/booking/BookingDialog.tsx create mode 100644 src/components/booking/BookingProvider.tsx create mode 100644 src/components/booking/MobileBookingBar.tsx create mode 100644 src/components/sections/CampSections.tsx create mode 100644 src/components/sections/FaqSection.tsx create mode 100644 src/components/sections/PeopleSections.tsx create mode 100644 src/components/sections/PricingSection.tsx create mode 100644 src/content/site.ts create mode 100644 src/lib/admin-auth.ts create mode 100644 src/lib/admin-queries.ts create mode 100644 src/lib/admin-service.ts create mode 100644 src/lib/api.ts create mode 100644 src/lib/audit.ts create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/booking-service.ts create mode 100644 src/lib/camp.ts create mode 100644 src/lib/checkout.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/email-templates.ts create mode 100644 src/lib/email.ts create mode 100644 src/lib/env.ts create mode 100644 src/lib/labels.ts create mode 100644 src/lib/money.ts create mode 100644 src/lib/payment-events.ts create mode 100644 src/lib/payment-gateway.ts create mode 100644 src/lib/payment-plan.ts create mode 100644 src/lib/pricing.ts create mode 100644 src/lib/public-page.ts create mode 100644 src/lib/quote.ts create mode 100644 src/lib/rate-limit.ts create mode 100644 src/lib/sales-gate.ts create mode 100644 src/lib/time.ts create mode 100644 src/lib/tokens.ts create mode 100644 src/lib/vouchers.ts create mode 100644 src/lib/webhook.ts create mode 100644 src/lib/worker-jobs.ts create mode 100644 src/lib/zod-de.ts create mode 100644 src/proxy.ts create mode 100644 tests/admin.integration.test.ts create mode 100644 tests/booking.integration.test.ts create mode 100644 tests/payment-plan.test.ts create mode 100644 tests/payments.integration.test.ts create mode 100644 tests/pricing.test.ts create mode 100644 tests/sales-gate.test.ts create mode 100644 tests/support/db.ts create mode 100644 tests/support/empty.ts create mode 100644 tests/support/fake-gateway.ts create mode 100644 tests/support/global-setup.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a47e41b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.next +node_modules +src/generated +test-results +playwright-report +coverage +media +.mail-outbox +.env +.env.* +!.env.example +*.tsbuildinfo +npm-debug.log* +Ziel +k8s +docs +e2e +tests diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5486ac1 --- /dev/null +++ b/.env.example @@ -0,0 +1,55 @@ +# ARTISTA MODELCAMP 2027 — Konfigurationsvorlage +# Kopieren nach .env (lokal) bzw. in ConfigMap/Secret (Kubernetes, k8s/). +# Hier stehen KEINE echten Geheimnisse. Werte in <…> ersetzen. + +# --- Umgebung ---------------------------------------------------------------- +# development | staging | production +# Nur "production" wird indexiert, blendet Entwicklungs-Platzhalter aus und +# erlaubt einen Stripe-Live-Schluessel. +APP_ENV=development +# Oeffentliche Adresse ohne abschliessenden Schraegstrich. +APP_URL=http://localhost:3000 + +# --- Datenbank --------------------------------------------------------------- +# Lokal: pnpm db:dev (Port 54329) oder docker compose up -d db (Port 5432). +DATABASE_URL=postgresql://artista:artista-dev@localhost:54329/artista +DATABASE_POOL_SIZE=10 + +# --- Geheimnisse (je mind. 32 Zeichen, z. B. `openssl rand -base64 48`) ------- +# Leitet die Zugangstokens der Buchungsstatusseiten ab und hasht IPs fuer +# Rate Limits. Wechsel macht alle bisherigen Statuslinks ungueltig. +APP_SECRET= +# Signiert die Admin-Sitzungen (Better Auth). +BETTER_AUTH_SECRET= + +# --- Stripe ------------------------------------------------------------------ +# sk_test_… = Testmodus (Checkout ohne Live-Freigabe moeglich) +# sk_live_… = Livemodus (nur mit APP_ENV=production UND allen Freigaben) +# Empfohlen: eingeschraenkter Schluessel (rk_…) mit Schreibrechten fuer +# Customers, Checkout Sessions, PaymentIntents, PaymentMethods, Refunds. +STRIPE_SECRET_KEY= +# Signaturgeheimnis des Webhook-Endpunkts (whsec_…), siehe docs/BETRIEB.md. +STRIPE_WEBHOOK_SECRET= + +# --- E-Mail ------------------------------------------------------------------ +# file = sicherer Testausgang: Mails landen als .eml in MAIL_OUTBOX_DIR, +# nichts wird verschickt (in production gesperrt). +# smtp = Versand ueber den SMTP-Anbieter unten. +MAIL_TRANSPORT=file +MAIL_OUTBOX_DIR=.mail-outbox +MAIL_FROM=ARTISTA MODELCAMP +# Empfaenger fuer "Pruefung noetig"-Hinweise (optional, empfohlen). +ADMIN_NOTIFY_EMAIL= +SMTP_HOST= +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER= +SMTP_PASSWORD= + +# --- Medien ------------------------------------------------------------------ +# Persistentes Verzeichnis fuer freigegebene Bilder/Videos, ausgeliefert +# unter /media/… (Kubernetes: eigenes PersistentVolume). +MEDIA_DIR=./media +# Datenschutzkonzept fuer externe Videos: none | youtube-nocookie | vimeo +# Externe Player laden nur nach Klick und ausdruecklicher Zustimmung. +EXTERNAL_VIDEO_PROVIDER=none diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..79a191e --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,105 @@ +name: Docker-Images bauen und veroeffentlichen + +# Baut aus einem Dockerfile ZWEI Images mit identischen Tags: +# Target "runner": der Next.js-Server +# -tools Target "tools": Migration (Kubernetes-Job), Worker (CronJob), admin:create +# Beide muessen immer mit demselben Tag ausgerollt werden (gleiches Datenbankschema). +# +# Ausloeser: Push auf main (":latest" + ":sha-"), Versions-Tags "v1.2.3" +# (zusaetzlich ":1.2.3"), manuell ueber "Run workflow". +on: + push: + branches: [main] + tags: ["v*"] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: docker-builder + env: + # Oeffentlicher Registry-Host (Pull durch Kubernetes, OCI-Labels). + REGISTRY: git.encry.de + # Push ueber den Cluster-internen Gitea-Service statt ueber git.encry.de: + # Blob-Uploads scheitern am Cloudflare-Proxy ("413 Payload Too Large"). + # Gleiches Muster wie im Wabenhain-Repo; der Hostname ist nicht Teil des + # gespeicherten Images, gezogen wird weiterhin ueber git.encry.de. + REGISTRY_PUSH: gitea:80 + steps: + - name: Code auschecken + uses: actions/checkout@v4 + + - name: Image-Namen in Kleinbuchstaben ermitteln + # OCI-Registries erlauben nur Kleinbuchstaben + # ("Nick.Rosenloecher/Artista-Filmcamp" -> "nick.rosenloecher/artista-filmcamp"). + id: image + run: echo "name=$(echo '${{ gitea.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITEA_OUTPUT" + + - name: Docker Buildx einrichten + uses: docker/setup-buildx-action@v3 + with: + # Der interne Gitea-Service spricht HTTP ohne TLS. + config-inline: | + [registry."gitea:80"] + http = true + + - name: Bei der Gitea-Container-Registry anmelden + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY_PUSH }} + username: ${{ gitea.actor }} + # GITEA_TOKEN darf keine Pakete schreiben: Personal Access Token mit + # Scope "package" als Repo-Secret REGISTRY_TOKEN hinterlegen + # (Repo-Einstellungen -> Actions -> Secrets). + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Metadaten Anwendung + id: meta-app + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_PUSH }}/${{ steps.image.outputs.name }} + # Gleiche Liste wie bei den Werkzeugen (keine YAML-Anker: nicht + # jeder Gitea-Runner unterstuetzt sie). + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=short + type=semver,pattern={{version}} + + - name: Metadaten Werkzeuge + id: meta-tools + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_PUSH }}/${{ steps.image.outputs.name }}-tools + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=short + type=semver,pattern={{version}} + + # Der Build braucht Internet (next/font laedt die Schriften), aber weder + # Datenbank noch Secrets. + - name: Anwendung bauen und pushen + uses: docker/build-push-action@v6 + with: + context: . + target: runner + push: true + tags: ${{ steps.meta-app.outputs.tags }} + labels: ${{ steps.meta-app.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Nutzt die gemeinsamen Build-Stufen aus dem Cache des ersten Builds. + - name: Werkzeuge bauen und pushen + uses: docker/build-push-action@v6 + with: + context: . + target: tools + push: true + tags: ${{ steps.meta-tools.outputs.tags }} + labels: ${{ steps.meta-tools.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Zusammenfassung + run: | + echo "Gebaut fuer ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}(-tools):" + echo "${{ steps.meta-app.outputs.tags }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec6b7d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +/node_modules +/.next/ +/out/ +/build +/coverage +/test-results +/playwright-report +/src/generated/ +/.dev-db/ +/.test-db/ +/.mail-outbox/ +/media/ +*.tsbuildinfo +next-env.d.ts +.DS_Store +*.pem +npm-debug.log* +.pnpm-debug.log* + +# Umgebungsdateien: nur die Vorlage ohne Geheimnisse ist versioniert. +.env* +!.env.example diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..875563e --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,14 @@ +{ + "detector": { + "ignoreRules": [], + "ignoreFiles": [], + "ignoreValues": [ + { + "rule": "overused-font", + "value": "arial", + "createdAt": "2026-09-27T03:25:29.714Z", + "reason": "Claude: E-Mail-Clients laden keine Webfonts; Arial ist dort bewusster websicherer Fallback (src/lib/email-templates.ts)" + } + ] + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f970096 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1 +# +# Zwei Images aus einem Build: +# docker build --target runner -t /artista-modelcamp: . +# docker build --target tools -t /artista-modelcamp-tools: . +# +# runner: der Next.js-Server (standalone), sonst nichts. +# tools: Prisma-CLI + tsx fuer den kontrollierten Migrationsschritt +# (Kubernetes-Job), den Worker (CronJob) und `admin:create`. +# Beide IMMER mit demselben Tag bauen und ausrollen — sie teilen das +# Datenbankschema. + +FROM node:22.23.2-alpine3.24 AS base +RUN corepack enable +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 + +# --- Abhaengigkeiten -------------------------------------------------------- +FROM base AS deps +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +# postinstall (prisma generate) braucht das Schema, das erst im builder +# vorliegt; generiert wird dort ausdruecklich. +RUN pnpm install --frozen-lockfile --ignore-scripts + +# --- Build ------------------------------------------------------------------ +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Zur Build-Zeit wird keine Datenbank und kein Secret benoetigt. +RUN mkdir -p public && pnpm prisma generate && pnpm next build + +# --- Laufzeit: Anwendung ------------------------------------------------------ +FROM node:22.23.2-alpine3.24 AS runner +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + MEDIA_DIR=/app/media +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 --ingroup nodejs nextjs +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +# Schreibbar fuer den Bild-Cache; im Cluster ein emptyDir (reiner Cache). +RUN mkdir -p /app/.next/cache /app/media && chown -R nextjs:nodejs /app/.next/cache /app/media +USER 1001 +EXPOSE 3000 +CMD ["node", "server.js"] + +# --- Laufzeit: Werkzeuge (Migration, Worker, Admin-Anlage) -------------------- +FROM base AS tools +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 --ingroup nodejs nextjs +COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nodejs /app/src ./src +COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts /app/package.json /app/tsconfig.json ./ +ENV PATH="/app/node_modules/.bin:${PATH}" +USER 1001 +# Standard: Migrationen anwenden. Worker: ["tsx", "scripts/worker.ts"]. +CMD ["prisma", "migrate", "deploy"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..17c3056 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# ARTISTA MODELCAMP 2027 – Buchungswebsite + +Deutschsprachige Onepage mit echter Buchungsstrecke: Stripe-Zahlung (einmalig oder in bis zu drei Raten), persistente Bestell- und Gutscheinverwaltung, geschützter Adminbereich mit Zwei-Faktor-Anmeldung, Docker- und RKE2-Deployment. + +**Stand:** technisch vollständig, im Stripe-Testmodus buchbar. Der Live-Verkauf ist serverseitig gesperrt, bis alle Inhalte und Freigaben aus [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) im Adminbereich bestätigt sind. + +| Dokument | Inhalt | +|---|---| +| [docs/ARCHITEKTUR.md](docs/ARCHITEKTUR.md) | Entscheidungen: Stack, Preisbindung, Ratenzahlung, Kapazität, Webhooks, Design | +| [docs/BETRIEB.md](docs/BETRIEB.md) | RKE2-Deployment, Secrets, Stripe-Webhooks, E-Mail, Sicherung, Storno/Erstattung | +| [docs/OFFENE-PUNKTE.md](docs/OFFENE-PUNKTE.md) | Noch benötigte Inhalte und Freigaben | +| [docs/TESTS.md](docs/TESTS.md) | Ausgeführte Tests und verbleibende Einschränkungen | + +## Stack + +TypeScript 5.9 (strict) · Next.js 16.3 (App Router, standalone) · React 19.3 · Tailwind CSS 4.3 · Motion 13 · PostgreSQL 18 · Prisma 7.10 (versionierte Migrationen) · Stripe 22 (Checkout + PaymentIntents) · Zod 4 · Better Auth 1.7 (Admin, TOTP) · Nodemailer 10 · Vitest 4 · Playwright 1.63. Versionen sind exakt fixiert, Lockdatei: `pnpm-lock.yaml`. + +## Lokal starten + +Voraussetzungen: Node.js 22+, pnpm 12. Docker ist optional. + +```bash +pnpm install +cp .env.example .env # APP_SECRET, BETTER_AUTH_SECRET setzen, Stripe-Testschlüssel eintragen +pnpm db:dev # eingebettetes PostgreSQL (eigenes Terminal) – oder: docker compose up -d db mailpit +pnpm db:deploy # Migrationen anwenden +pnpm db:seed # nur Entwicklung: Early Bird simuliert, Test-Anreisetag, Gutschein TEST-STAMMMODEL +pnpm admin:create --email du@example.org --name "Dein Name" +pnpm dev # http://localhost:3000, Verwaltung unter /admin +``` + +Stripe-Testbuchung mit Webhooks lokal: + +```bash +stripe listen --forward-to localhost:3000/api/stripe/webhook # gibt das whsec_… für STRIPE_WEBHOOK_SECRET aus +pnpm worker # Reservierungen, Raten, E-Mails (im Cluster: CronJob) +``` + +Testkarte `4242 4242 4242 4242`, für 3-D-Secure-Fälle `4000 0027 6000 3184`. E-Mails landen mit `MAIL_TRANSPORT=file` als `.eml` in `.mail-outbox/` (oder in Mailpit unter http://localhost:8025). + +## Befehle + +| Befehl | Zweck | +|---|---| +| `pnpm dev` / `pnpm build` / `pnpm start` | Entwicklung, Produktionsbuild, Produktionsserver | +| `pnpm typecheck` / `pnpm lint` | Typprüfung, ESLint | +| `pnpm test` | Unit- und Integrationstests (startet eine eigene PostgreSQL-Instanz) | +| `pnpm test:e2e` | Playwright: Smartphone, Tablet, Desktop, axe-Barrierefreiheit (gegen laufenden Server) | +| `pnpm db:migrate` / `pnpm db:deploy` | Migration erstellen / anwenden | +| `pnpm worker` | Einmaliger Worker-Lauf | +| `pnpm admin:create` | Admin-Konto anlegen (kein Standardpasswort) | + +## Wo was gepflegt wird + +- **Texte, Bilder, Dozentinnen, Videos, FAQ-Grundlagen:** [src/content/site.ts](src/content/site.ts). Einträge mit `confirmed: false` gelten als Entwurf. +- **Termin, Ort, Zimmerbeschreibung, Veranstalter, Steuerhinweis, Bedingungen, Kapazität, Freigaben:** Adminbereich → „Einstellungen und Freigaben“. +- **Preise und Preisphasen:** fest in [src/lib/pricing.ts](src/lib/pricing.ts), weil sie verbindlich vorgegeben sind. +- **Gutscheine (Stammmodel-Codes):** Adminbereich → „Gutscheine“. +- **Medien:** Dateien ins Medienverzeichnis (`MEDIA_DIR`) legen und in `site.ts` als `{ kind: "image", src: "/media/…" }` eintragen. diff --git a/Ziel b/Ziel new file mode 100644 index 0000000..46dff57 --- /dev/null +++ b/Ziel @@ -0,0 +1,437 @@ + +Du bist ein erfahrener Creative Director, UX-Designer und Senior Fullstack-Entwickler. Entwickle eine hochwertige, produktionsfähige Buchungswebsite für das **ARTISTA MODELCAMP 2027**. + +Erstelle eine tatsächlich funktionierende Anwendung mit vollständigem Quellcode, Stripe-Anbindung, Bestellverwaltung und Deployment für mein selbst gehostetes RKE2-Kubernetes-Cluster. Eine rein visuelle Demo reicht nicht. + +## 1. Ziel und Arbeitsweise + +Die Website soll eine deutschsprachige Onepage sein, die das Modelcamp emotional und hochwertig präsentiert und Besucherinnen verständlich bis zur verbindlichen Buchung führt. + +Die öffentliche Verkaufsseite bleibt eine Onepage mit Ankernavigation. Separate technische Seiten für Zahlungsabschluss, Buchungsstatus, rechtliche Inhalte und einen geschützten Verwaltungsbereich sind erlaubt. + +Arbeite selbstständig und setze sinnvolle technische Entscheidungen direkt um. Fehlende Inhalte sollen die Entwicklung nicht aufhalten. Verwende dafür zentral gepflegte, klar erkennbare Platzhalter. + +Erfinde keine Veranstaltungsdaten, Leistungen, Qualifikationen, Kundenbewertungen oder rechtlichen Zusicherungen. Fehlende Angaben, die für einen echten Verkauf erforderlich sind, müssen den Live-Verkauf blockieren, aber nicht die Entwicklung oder Stripe-Testbuchungen. + +Prüfe vor der Implementierung die aktuellen offiziellen Dokumentationen der eingesetzten Frameworks und Stripe. Dokumentiere relevante Architekturentscheidungen knapp. + +## 2. Referenz und gestalterische Richtung + +Orientiere dich am Aufbau und an der Besucherführung dieser Seite: + +https://www.freedomfilmmaker.de/filmcamp + +Analysiere, soweit erreichbar: + +- Reihenfolge und Dramaturgie der Inhalte. +- Hero, Bildwirkung und typografische Hierarchie. +- Darstellung von Unterkunft und Dozenten. +- Video-Feedback und Vertrauenselemente. +- Preispräsentation und Buchungseinstieg. +- Mobile Nutzerführung. + +Übertrage geeignete Prinzipien in eine eigenständige Gestaltung für ARTISTA. Kopiere keine fremden Texte, Markenbestandteile oder nicht freigegebenen Medien. + +Wenn die Referenz nicht erreichbar ist, benenne diese Einschränkung und entwickle anhand dieses Briefings weiter. Behaupte nicht, die Seite analysiert zu haben. + +Gestalterische Richtung: + +- Hochwertiges, modernes Editorial-Design mit Mode- und Fotografiecharakter. +- Großzügige Bilder, ausdrucksstarke Typografie und bewusst gesetzter Weißraum. +- Warme, mediterrane Atmosphäre passend zu einer Villa in Italien. +- Als vorläufige Farbwelt: warmes Off-White, dunkles Anthrazit und ein zurückhaltender warmer Akzent. +- Markante, individuelle Kompositionen statt einer austauschbaren Ansammlung identischer Karten. +- Gute Lesbarkeit und klare Buchungsführung haben Vorrang vor Effekten. +- Dezente Animationen für Hero, Bildwechsel, Scroll-Reveal, Navigation und Buchungsdialog. +- Keine Scroll-Übernahme und keine Animationen, die Inhalte verzögern oder Bedienung erschweren. +- `prefers-reduced-motion` respektieren. + +Die Seite muss auf Smartphones ebenso sorgfältig gestaltet sein wie auf großen Bildschirmen. + +## 3. Technischer Rahmen + +Bevorzugter Stack: + +- TypeScript mit strikter Typprüfung. +- Next.js 16 mit App Router. +- React 19. +- Tailwind CSS v4. +- Motion for React. +- PostgreSQL für persistente Geschäftsdaten. +- Ein geeignetes ORM mit versionierten Migrationen. +- Stripe für Zahlungen. +- Serverseitige Eingabevalidierung, beispielsweise mit Zod. + +Prüfe die Versionskompatibilität. Passe den Stack nur bei einem nachvollziehbaren Vorteil an. Fixiere konkrete kompatible Versionen und liefere eine Lockdatei. + +Nutze Server Components, wo sinnvoll. Beschränke Client Components auf tatsächlich interaktive Bereiche. + +Die Anwendung muss ohne Vercel und ohne proprietäre Hosting-Abhängigkeiten als Docker-Container laufen. + +Für ein einzelnes Camp ist kein klassischer Warenkorb erforderlich. Bevorzuge eine direkte, verständliche Buchungsstrecke. + +## 4. Seitenstruktur + +Entwickle eine schlüssige Dramaturgie mit folgenden Inhalten: + +### Hero + +- ARTISTA MODELCAMP 2027 als eindeutige Identität. +- Prägnante deutsche Headline und verständliche Subline. +- Hochwertiges Leitmotiv mit austauschbarem Bild oder Video. +- Termin und Ort, sobald bestätigt. +- Klarer Buchungs-CTA. +- Preis abhängig von der tatsächlich gültigen Verkaufsphase. + +### Das Camp + +Erkläre Atmosphäre, Zielgruppe und bestätigte Inhalte. Formuliere überzeugend, aber ohne erfundene Versprechen. + +Keine Aussagen über garantierte Agenturvermittlung, Karriereerfolg, Aufträge oder Verdienst. + +### Erlebnisse und Leistungen + +Zeige bestätigte Programmpunkte und enthaltene Leistungen übersichtlich. Nicht bestätigte Workshops, Shootings, Transfers, Verpflegungsleistungen oder Ergebnisse dürfen nicht als zugesichert erscheinen. + +Ein beispielhafter Ablauf muss als solcher erkennbar sein. Ein verbindliches Programm darf erst mit bestätigten Angaben veröffentlicht werden. + +### Unterkunft + +- Bildstarke Galerie der Villa beziehungsweise Finca. +- Informationen zu Zimmern und Ausstattung nur aus bestätigten Angaben. +- Der Booking-Link und die Bilder werden nachgereicht. +- Bilder erst nach bestätigter Nutzungsberechtigung übernehmen. +- Kein dauerhaftes Hotlinking fremder Booking-Bilder. +- Bis dahin hochwertige, klar erkennbare Entwicklungsplatzhalter. + +### Dozentinnen + +Zwei vorbereitete Profile mit austauschbaren Porträts, Namen und Kurzbeschreibungen. + +Die tatsächlichen Namen und Bilder werden nachgereicht. Keine erfundenen Lebensläufe oder Referenzen. + +### Über ARTISTA + +Bereite einen eigenständigen Bereich vor. Inhaltliche Grundlage soll die bestehende Stammmodel-Seite von ARTISTA sein; der Link wird nachgereicht. + +Bis dahin keine Unternehmensgeschichte oder Erfolgszahlen erfinden. + +### Video-Feedback + +Exakt drei Slots für echte Feedback-Videos: + +- Austauschbares Vorschaubild. +- Zugänglicher Play-Button. +- Video bei Bedarf laden. +- Keine automatische Tonwiedergabe. +- Platz für echten Namen und freigegebenen Begleittext. + +Fehlende Videos nicht durch erfundene Testimonials ersetzen. Leere Slots im Entwicklungsmodus darstellen und im öffentlichen Produktionsbetrieb ausblenden. + +### Preis und Buchung + +- Verständlicher Gesamtpreis pro Person. +- Gültiger Frühbuchervorteil. +- Einmalzahlung und gegebenenfalls freigegebene Ratenzahlung. +- Neutrales Gutscheinfeld. +- Klare Zusammenfassung von Unterkunft und Leistungen vor Zahlung. + +### FAQ und Abschluss + +FAQ zu Buchung, Unterkunft, Anreise, Zahlungsweise und bestätigten Bedingungen. Abschließender CTA und Footer mit Kontakt und rechtlichen Verweisen. + +Eine mobile Buchungsleiste ist erwünscht, sofern sie keine Inhalte oder Bedienelemente verdeckt. + +## 5. Verbindliche Preislogik + +Alle Beträge in EUR. Intern ausschließlich mit ganzzahligen Cent-Beträgen rechnen. + +| Preisphase | Ohne Stammmodel-Code | Mit gültigem Stammmodel-Code | +|---|---:|---:| +| Early Bird | 2.200,00 € | 1.980,00 € | +| Regulär | 2.750,00 € | 2.475,00 € | + +Regeln: + +- Grundpreis: 2.750,00 € pro Person. +- Early Bird: 20 % Rabatt auf den Grundpreis. +- Early-Bird-Zeitraum: 01.11.2026 bis einschließlich 31.12.2026, Zeitzone Europe/Berlin. +- Regulärer Preis ab 01.01.2027, 00:00 Uhr Europe/Berlin. +- Vor dem 01.11.2026 standardmäßig Vorschau ohne Kaufmöglichkeit. +- Innerhalb der Early-Bird-Phase 2.750 € als eindeutig bezeichneten regulären Preis durchstreichen und 2.200 € hervorheben. +- Nach Ablauf keine Early-Bird-Darstellung und keine künstliche Verlängerung. + +Ein gültiger Stammmodel-Code reduziert den jeweils gültigen Preis anschließend um weitere 10 %: + +- 2.750 € × 0,80 × 0,90 = 1.980 €. +- Der kombinierte Nachlass beträgt 28 %, nicht 30 %. +- Regulär: 2.750 € × 0,90 = 2.475 €. + +Preise und Berechtigung immer serverseitig berechnen. Niemals vom Browser übermittelte Endpreise übernehmen. + +Definiere eine eindeutige Regel für Preisbindung bei begonnenem Checkout und beim Jahreswechsel. Kommuniziere sie verständlich und erzwinge sie auch in Stripe. + +Die steuerliche Behandlung ist noch offen. Keine Umsatzsteuerbeträge oder Aussagen wie „inklusive 19 % MwSt.“ erfinden. Vor Live-Verkauf muss die korrekte Darstellung konfiguriert sein. + +## 6. Individuelle Stammmodel-Codes + +Beispiel: + +`STAMMMODELLAURA1X0` + +Dieser Beispielcode darf nicht automatisch als aktiver Produktionscode angelegt werden. + +Der Nachlass wird persönlich vergeben und nicht öffentlich beworben. Auf der Seite erscheint nur ein neutrales Feld „Gutscheincode“. + +Im geschützten Verwaltungsbereich müssen individuelle Codes anlegbar und bearbeitbar sein: + +- Code. +- Aktiv/inaktiv. +- Gültigkeitszeitraum. +- Maximale Einlösungen. +- Optional Bindung an eine E-Mail-Adresse. +- Interne Zuordnung. +- Reservierte und abgeschlossene Einlösungen. + +Codes sollen mit Early Bird kombinierbar sein, aber nicht mit weiteren Gutscheinen. + +Verhindere Missbrauch durch serverseitige Prüfung, Rate Limiting und transaktionale Einlöselimits. Reservierungen bei abgebrochenem oder abgelaufenem Checkout korrekt freigeben. + +Die Vorlage empfiehlt höchstens vier vergünstigte Stammmodel-Buchungen. Implementiere ein konfigurierbares Gesamtkontingent und hinterlege vier als vorläufigen, vor Live-Verkauf zu bestätigenden Wert. + +Keine öffentlich sichtbare Liste gültiger Codes. + +## 7. Zahlungsvarianten + +Bereite folgende exakten Zahlungspläne vor: + +| Variante | Einmalzahlung | Zwei Raten | Drei Raten | +|---|---:|---|---| +| Early Bird | 2.200,00 € | 1.100,00 € + 1.100,00 € | 800,00 € + 700,00 € + 700,00 € | +| Early Bird Stammmodel | 1.980,00 € | 990,00 € + 990,00 € | 660,00 € + 660,00 € + 660,00 € | +| Regulär | 2.750,00 € | 1.375,00 € + 1.375,00 € | 950,00 € + 900,00 € + 900,00 € | +| Regulär Stammmodel | 2.475,00 € | 1.237,50 € + 1.237,50 € | 825,00 € + 825,00 € + 825,00 € | + +- Maximal drei Raten. +- Erste Rate bei Buchung. +- Letzte Rate spätestens 30 Tage vor Anreise. +- Konkrete weitere Fälligkeiten benötigen einen bestätigten Camp-Termin und eine freigegebene Terminregel. +- Keine Termine erfinden. +- Wenn die verbleibende Zeit eine Zahlungsvariante nicht mehr zulässt, diese serverseitig deaktivieren. + +Die Raten aus der Vorlage sind fachliche Vorgaben, keine rechtliche Freigabe. Zahlungspläne technisch vorbereiten und im Testmodus prüfen; im Live-Modus erst nach Prüfung und ausdrücklicher Freigabe aktivieren. + +Wähle eine anhand aktueller Stripe-Dokumentation geeignete Umsetzung für endliche Zahlungspläne mit teilweise unterschiedlichen Ratenbeträgen. Begründe die Entscheidung. + +Kein unbegrenzt laufendes Abonnement und keine Gleichsetzung mit Klarna oder anderen externen Finanzierungsangeboten. + +Falls spätere Raten automatisch eingezogen werden: + +- Erforderliche Zustimmung und geeignete Zahlungsmethoden berücksichtigen. +- Zusätzliche Authentifizierung ermöglichen. +- Fehlgeschlagene Zahlungen und Wiederholungsversuche sauber abbilden. +- Nach der letzten Rate weitere Abbuchungen zuverlässig ausschließen. + +Zeige Gesamtpreis, heute fälligen Betrag, restliche Raten und Fälligkeiten vor dem Zahlungsabschluss transparent an. + +## 8. Unterkunft und Kapazität + +Die vorliegenden Angaben zur Zimmerart sind widersprüchlich: + +- Es wird von einem einzelnen Platz in einem geteilten Zimmer gesprochen. +- Die interne Kalkulation nennt fünf Doppelzimmer mit französischem Doppelbett für zehn Teilnehmerinnen. + +Erfinde daraus keine Einzelzimmer oder getrennten Einzelbetten. + +Bis zur Klärung gilt: + +- Nur ein Buchungsprodukt: Teilnahme pro Person mit geteilter Unterbringung. +- Keine buchbare Einzelbelegung. +- Keine Zimmer-Upgrades. +- Keine Auswahl zwischen mehreren Unterkunftskategorien. +- Ein möglicherweise geteiltes französisches Doppelbett muss vor einem echten Kauf ausdrücklich und korrekt beschrieben werden. +- Live-Verkauf bleibt bis zur Bestätigung der Unterbringung gesperrt. + +Intern vorläufig mit zehn verkaufbaren Teilnehmerinnenplätzen rechnen. Den Wert konfigurierbar halten und vor Veröffentlichung bestätigen lassen. + +Keine öffentliche Platzzahl, keinen Restplatz-Zähler und keine künstliche Verknappung anzeigen. + +Verhindere Überbuchungen durch Datenbanktransaktionen und zeitlich begrenzte Reservierungen. Berücksichtige parallele Checkouts, verzögerte Zahlungen und verspätete Webhooks. + +## 9. Stripe und Bestellprozess + +Bevorzuge Stripe Checkout, sofern es den bestätigten Ablauf vollständig abbildet. Verwende eine eingebettete Zahlungsoberfläche nur bei begründetem Mehrwert. + +Ablauf: + +1. Person startet Buchung auf der Onepage. +2. Buchungsdaten und Zahlungsvariante werden erfasst. +3. Optionaler Gutschein wird serverseitig geprüft. +4. Gesamtpreis, Unterkunft, Leistungen und Zahlungsplan werden zusammengefasst. +5. Erforderliche Erklärungen werden eingeholt. +6. Bestellung und Platzreservierung werden serverseitig angelegt. +7. Stripe-Zahlung wird gestartet. +8. Verifizierte Webhooks aktualisieren den Zahlungsstatus. +9. Bestätigungsseite und E-Mail zeigen den tatsächlich bestätigten Status. + +Ein Aufruf der Erfolgsseite darf niemals allein eine Bestellung als bezahlt markieren. + +Erforderlich sind: + +- Prüfung der Stripe-Webhook-Signatur anhand des unveränderten Request-Bodys. +- Idempotente Checkout-Erstellung und Webhook-Verarbeitung. +- Umgang mit mehrfachen, verzögerten und ungeordneten Events. +- Getrennte Buchungs- und Zahlungszustände. +- Behandlung von Abbruch, Ablauf, Zahlungsfehlern und Rückerstattungen. +- Nachvollziehbare Zuordnung von Stripe-Objekten zu internen Bestellungen. +- Speicherung des bei Buchung vereinbarten Preises und Zahlungsplans als unveränderlicher Snapshot. +- Keine Zahlungsdaten wie Kartennummern in der eigenen Datenbank. + +Bei Ratenzahlung bedeutet eine erfolgreiche erste Rate nicht „vollständig bezahlt“. Zeige Teilzahlung und offenen Betrag korrekt an. + +Die Buchungsbestätigung erfolgt per konfigurierbarem E-Mail-Anbieter oder SMTP. Im Entwicklungsmodus einen sicheren Testausgang verwenden. + +## 10. Verwaltung und Datenmodell + +Erstelle einen schlanken, wirklich geschützten Adminbereich für: + +- Buchungsübersicht und Buchungsdetails. +- Zahlungsstatus, offene Beträge und Fälligkeiten. +- Filter und CSV-Export. +- Gutscheinverwaltung. +- Interne Kapazität und Reservierungen. +- Pflege der notwendigen Camp-Konfiguration. +- Dokumentierten Umgang mit Stornierungen und Rückerstattungen. + +Kein offener Adminbereich und keine fest eingebauten Standardpasswörter. Nutze eine etablierte Authentifizierungslösung und prüfe Berechtigungen auch serverseitig bei jeder Aktion. + +Modelliere mindestens: + +- Camp und Verkaufsfreigaben. +- Buchung und Teilnehmerdaten. +- Preis-Snapshot. +- Zahlungsplan und Raten. +- Gutschein und Einlösung. +- Platzreservierung. +- Verarbeitete Stripe-Events. + +Bestandsänderungen und Zahlungsaktionen müssen nachvollziehbar sein. + +## 11. Inhalte und Freigaben + +Die beigefügte Kalkulation enthält interne Informationen. Folgendes darf nicht auf die öffentliche Seite oder in öffentlich ausgelieferte Daten gelangen: + +- Kosten der Villa und Verpflegung. +- Margen, Gewinn und Break-even. +- Werbebudget. +- Teambelegung. +- Interne Argumentation zu Stammmodel-Rabatten. +- Interne Empfehlungen zu Kontingenten. + +Laut Vorlage nicht enthalten: + +- Anreise nach Italien und zurück. +- Persönliche Ausgaben vor Ort. +- Reiserücktrittsversicherung. + +Diese Ausschlüsse müssen verständlich dargestellt werden. Leite aus internen Kostenpositionen keine öffentlich zugesicherten Leistungen ab. + +Vor Live-Verkauf sind insbesondere erforderlich: + +- Bestätigter Termin und Ort. +- Veranstalter- und Kontaktdaten. +- Bestätigte Unterkunft und enthaltene Leistungen. +- Steuerlich korrekte Preisangaben. +- Freigegebene Buchungs-, Storno- und Zahlungsbedingungen. +- Prüfung der reiserechtlichen Einordnung sowie gegebenenfalls erforderlicher Absicherung und Unterlagen. + +Übernimm rechtliche Aussagen aus der Kalkulation nicht ungeprüft als geltendes Recht. Erfinde keine rechtssicheren AGB. + +Implementiere eine serverseitige Verkaufsfreigabe, die auch direkt aufgerufene Checkout-Endpunkte schützt. Testmodus und Vorschau müssen ohne Live-Freigabe nutzbar bleiben. + +## 12. Qualität und Datenschutz + +- Tastaturbedienbarkeit, sichtbare Fokuszustände und ausreichende Kontraste. +- Beschriftete Formulare und verständliche Fehlermeldungen. +- Korrektes Fokusmanagement in Dialogen. +- Responsive Bilder und performantes Laden. +- Keine unnötig großen Client-Bundles. +- Keine personenbezogenen Daten oder Secrets in Logs. +- Serverseitige Validierung aller Eingaben. +- Schutz schreibender Endpunkte und administrative Zugriffskontrollen. +- Keine nicht erforderlichen Tracking-Dienste ohne passende Einwilligung. +- Externe Videoanbieter erst gemäß konfiguriertem Datenschutzkonzept laden. + +Bereite Titel, Beschreibung, Canonical und Social-Sharing-Vorschau vor. Strukturierte Veranstaltungsdaten erst mit bestätigten Angaben ausgeben. Vorschau, Admin und Buchungsstatusseiten nicht indexieren. + +## 13. Deployment auf RKE2 + +Liefere: + +- Produktionsfähiges Multi-Stage-Dockerfile. +- Next.js-Standalone-Build. +- Ausführung als Nicht-Root. +- `.dockerignore`. +- `.env.example` ohne echte Secrets. +- Lokale Entwicklungsumgebung mit PostgreSQL. +- Kubernetes-Manifeste oder eine übersichtliche Helm-Struktur. +- Deployment, Service und Traefik-Ingress. +- Konfigurierbare Domain, IngressClass und TLS-Einstellungen. +- cert-manager-Anbindung mit konfigurierbarem ClusterIssuer; `letsencrypt-prod` als anpassbares Beispiel. +- Readiness- und Liveness-Probes. +- Ressourcenanforderungen und sinnvolle Limits. +- Separaten, kontrollierten Datenbank-Migrationsschritt. +- Dokumentation für Secrets, Stripe-Webhooks und E-Mail-Konfiguration. + +Die Anwendung muss mehrere Replikate vertragen. Bestellungen, Kapazitäten, Gutscheine, Reservierungen und Zahlungsjobs dürfen nicht ausschließlich im Prozessspeicher liegen. + +Keine In-Process-Timer für verbindliche Ratenfälligkeiten. Verwende Stripe-native Terminierung oder einen dauerhaften, idempotenten Worker beziehungsweise Kubernetes-CronJob. + +PostgreSQL braucht geeigneten Datenbankspeicher. SMB-Freigaben nicht ungeprüft als PostgreSQL-Datenverzeichnis verwenden. Medien separat persistent speichern; kein `emptyDir` für dauerhaft benötigte Uploads. + +Dokumentiere Backup und Wiederherstellung der Geschäftsdaten. + +## 14. Tests und Abnahme + +Teste die tatsächlichen Geschäftsrisiken: + +- Alle vier Gesamtpreise. +- Exakte Ratensummen einschließlich 1.237,50 €. +- Zeitgrenzen am 01.11.2026 und 01.01.2027 in Europe/Berlin. +- Ungültige, abgelaufene und ausgeschöpfte Gutscheine. +- Gleichzeitige Einlösung begrenzter Codes. +- Gleichzeitige Buchungen bei knapper Kapazität. +- Abgelaufene Reservierungen und verspätete Zahlung. +- Doppelte und ungeordnete Stripe-Webhooks. +- Zahlungserfolg, Abbruch, Fehler und Teilzahlung. +- Sperre sämtlicher Live-Checkout-Zugänge ohne Freigabe. +- Verhinderung weiterer Abbuchungen nach der letzten Rate. +- Zugriffsschutz für Admin und Buchungsdaten. + +Führe außerdem Build, Typprüfung und eine visuelle Prüfung auf Smartphone, Tablet und Desktop durch. + +## 15. Erwartetes Ergebnis + +Liefere eine fertig implementierte Anwendung mit: + +1. Hochwertiger Onepage. +2. Funktionierender Stripe-Testbuchung. +3. Persistenter Bestell- und Gutscheinverwaltung. +4. Geschütztem Adminbereich. +5. Docker- und RKE2-Deployment. +6. Verständlicher README. +7. Kurzer Liste der noch benötigten Inhalte und Freigaben. +8. Übersicht der ausgeführten Tests und verbleibenden Einschränkungen. + +Pflege offene Inhalte zentral, insbesondere: + +- Camp-Termin und genauer Ort. +- Booking-Link und freigegebene Unterkunftsbilder. +- Verbindliche Zimmer- und Bettbeschreibung. +- Namen und Porträts der beiden Dozentinnen. +- Link zur ARTISTA-Stammmodel-Seite. +- Drei echte Feedback-Videos. +- Bestätigte Leistungen und Programm. +- Veranstalterdaten, steuerliche Angaben und freigegebene Bedingungen. + +Beginne mit einer knappen Architektur- und Designentscheidung und setze anschließend die Anwendung um. Stoppe nicht nach einem Konzept oder einer statischen Oberfläche. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b051e89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +# Lokale Entwicklungsumgebung. +# +# docker compose up -d db mailpit nur Datenbank + Test-Postfach +# docker compose --profile app up -d zusaetzlich die gebaute Anwendung +# +# Mailpit faengt alle Mails ab (Weboberflaeche http://localhost:8025): +# MAIL_TRANSPORT=smtp, SMTP_HOST=localhost, SMTP_PORT=1025. +# Ohne Docker: pnpm db:dev (eingebettetes PostgreSQL) + MAIL_TRANSPORT=file. +services: + db: + image: postgres:18.1-alpine + environment: + POSTGRES_USER: artista + POSTGRES_PASSWORD: artista-dev + POSTGRES_DB: artista + ports: + - "127.0.0.1:5432:5432" + volumes: + - db-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U artista"] + interval: 5s + retries: 10 + + mailpit: + image: axllent/mailpit:v1.27 + ports: + - "127.0.0.1:8025:8025" + - "127.0.0.1:1025:1025" + + migrate: + profiles: ["app"] + build: { context: ., target: tools } + environment: + DATABASE_URL: postgresql://artista:artista-dev@db:5432/artista + depends_on: + db: { condition: service_healthy } + + app: + profiles: ["app"] + build: { context: ., target: runner } + env_file: .env + environment: + DATABASE_URL: postgresql://artista:artista-dev@db:5432/artista + SMTP_HOST: mailpit + SMTP_PORT: "1025" + ports: + - "127.0.0.1:3000:3000" + volumes: + - ./media:/app/media:ro + depends_on: + migrate: { condition: service_completed_successfully } + +volumes: + db-data: diff --git a/docs/ARCHITEKTUR.md b/docs/ARCHITEKTUR.md new file mode 100644 index 0000000..ff805a5 --- /dev/null +++ b/docs/ARCHITEKTUR.md @@ -0,0 +1,86 @@ +# Architektur- und Designentscheidungen + +## Referenzseite + +`https://www.freedomfilmmaker.de/filmcamp` war erreichbar. Ausgewertet wurde eine automatisch erzeugte Zusammenfassung des ausgelieferten HTML, keine vollständige visuelle Analyse. Laut dieser Zusammenfassung ist die Reihenfolge: Hero mit Termin, Ort, Preis und CTA, dann Galerie, Tagesablauf, Unterkunftsgalerie, Platzoptionen mit Preisen, Team-Profile, Abschluss-CTA und Footer mit Rechtlichem. Video-Testimonials waren dort nicht erkennbar. Übernommen wurden nur Prinzipien: Fakten schon im Hero, Unterkunft über Bilder statt Text, Team mit Gesicht, früher und wiederholter Buchungseinstieg, eine persistente Buchungsaktion auf Mobilgeräten. Texte, Marken und Medien wurden nicht übernommen. + +## Gestaltung + +- **Idee:** Die Onepage ist als Modemagazin angelegt. Der Hero ist ein Cover, der Masthead „ARTISTA“ überlappt das Leitbild, Termin, Ort und Preis stehen als Cover-Lines daneben. Das Cover ist das einzige laute Element, der Rest bleibt ruhig. +- **Farbe:** Putz-Weiß `#F2ECE3`, Papier `#FAF7F1`, Anthrazit `#23211F`, Stein `#676058`, Linie `#D8CEC0`. Der Akzent ist Pompeji-Rot `#8E3B2B` statt des üblichen Terrakotta. Kontraste gegen den Grund: 14,6 : 1, 5,3 : 1 und 6,7 : 1. +- **Schrift:** Bodoni Moda (Didone) für Display, Hanken Grotesk für Text. Beide werden über `next/font` selbst ausgeliefert, es gibt keine Verbindung zu Google. +- **Layout:** asymmetrisches 12-Spalten-Raster, linksbündig, keine Kartenraster. Nummerierung gibt es nur beim beispielhaften Ablauf, weil der eine echte Abfolge ist. +- **Bewegung:** + - eine orchestrierte Hero-Eröffnung in reinem CSS + - Clip-Reveal nur auf Bildern + - Nav-Indikator + - Dialog-Übergänge und mobile Leiste über Motion (`LazyMotion`) + - `prefers-reduced-motion` wird überall respektiert, es gibt keine Scroll-Übernahme. +- **Platzhalter:** Fehlende Bilder erscheinen als gestaltete Flächen im Mittelmeer-Ton. In Entwicklung und Staging tragen sie das Label „Platzhalter: …“. Leere Video-Slots werden in Produktion ausgeblendet. + +## Stack und Laufzeit + +- Next.js 16 App Router mit Server Components. Clientseitig sind nur Header, Buchungsdialog (lazy geladen), mobile Leiste, Galerie, Video-Slots und Statusaktionen. +- Die Ausgabe ist `standalone` und läuft als Docker-Container unter Nicht-Root. Es gibt keine Vercel-Abhängigkeit. +- Der Zustand liegt vollständig in PostgreSQL: Buchungen, Reservierungen, Gutscheine, Rate Limits, E-Mail-Ausgang und Stripe-Events. Die App ist dadurch zustandslos und kann mehrere Replikate haben. +- Die Content-Security-Policy arbeitet mit Nonce und `strict-dynamic` (src/proxy.ts). Stripe Checkout ist gehostet, deshalb braucht die Seite kein Stripe.js. + +## ADR-1: Stripe Checkout statt eingebetteter Zahlung + +Der gehostete Checkout bildet den Ablauf vollständig ab: Betrag, Ablaufzeit, das Speichern der Zahlungsmethode für Folgeraten und dynamische Zahlungsarten. Kartendaten berühren die Anwendung nie, und die CSP bleibt streng. Eine eingebettete Oberfläche hätte keinen Mehrwert gebracht, der den zusätzlichen Integrations- und Sicherheitsaufwand rechtfertigt. + +## ADR-2: Preisbindung + +**Regel:** Maßgeblich ist der Zeitpunkt des Buchungsstarts. Der dabei serverseitig berechnete Preis gilt, wenn die Zahlung innerhalb der Reservierungszeit abgeschlossen wird (30 Minuten, konfigurierbar, Minimum laut Stripe). Danach verfällt die Reservierung, ein neuer Versuch wird zum dann gültigen Preis berechnet. + +**Erzwingung:** +- `expires_at` der Checkout-Session entspricht `priceLockedUntil`, danach ist die Session bei Stripe nicht mehr bezahlbar. +- Der Platz bleibt 10 Minuten länger reserviert als die Session läuft, damit eine kurz vor Ablauf abgeschlossene Zahlung ihren Platz sicher behält. +- Beim Jahreswechsel gilt: Wer am 31.12. um 23:50 Uhr startet, zahlt Early Bird, sofern die Zahlung bis zum angezeigten Ablauf gelingt. Eine Verlängerung gibt es nicht. +- Kommuniziert wird die Regel im Dialog, in der FAQ und im Stripe-Checkout-Text. + +## ADR-3: Endliche Ratenpläne + +**Gewählt:** Die erste Rate läuft über Stripe Checkout mit `setup_future_usage=off_session`. Die Folgeraten sind einzelne Off-Session-PaymentIntents, die ein idempotenter Kubernetes-CronJob am Fälligkeitstag um 9:00 Uhr Berliner Zeit auslöst. + +**Verworfen: Subscription Schedules.** Sie sind laut Stripe-Doku das Standardmuster für Ratenzahlung, rechnen aber in festen Intervallen (z. B. monatlich). Die geforderte Regel „letzte Rate spätestens 30 Tage vor Anreise“ und eine vom Buchungstag abhängige Mitte lassen sich damit nicht sauber abbilden. Außerdem wäre es formal ein Abonnement. + +**Gründe für den gewählten Weg:** +- Die Raten sind exakte, unveränderliche Datenbankzeilen (per DB-Trigger geschützt). Beträge wie 800 + 700 + 700 € sind direkt abbildbar. +- Endlichkeit ist dreifach gesichert: Es gibt maximal drei Ratenzeilen, jeder Einzug prüft `paid + Rate ≤ Gesamt`, und nach der letzten Rate wird die Zahlungsmethode bei Stripe getrennt. +- Zustimmung: Vor der Zahlung wird ausdrücklich dem Einzug zugestimmt, mit Zeitstempel `installmentMandateAt`. +- Verlangt die Bank eine Authentifizierung, geht die Rate auf `REQUIRES_ACTION`, und die Kundin bekommt per E-Mail einen Link und zahlt über eine eigene Checkout-Session. +- Bei Fehlschlag gibt es bis zu drei Versuche im Abstand von drei Tagen, danach „Prüfung nötig“ im Admin. +- Idempotenzschlüssel `offsession::` verhindern Doppelabbuchungen, auch nach Netzwerkfehlern oder Worker-Abbrüchen. +- Keine Gleichsetzung mit Klarna: Es ist die eigene Ratenvereinbarung des Veranstalters. Im Livemodus ist sie erst nach ausdrücklicher Freigabe verfügbar. + +**Aufteilung:** Ist der Betrag ohne Centrest teilbar, sind alle Raten gleich. Sonst werden die Folgeraten auf volle 50 € abgerundet, der Rest fällt in Rate 1. Das reproduziert alle zwölf Vorgabewerte (Tests). + +**Termine:** Die letzte Rate ist am Stichtag fällig (Anreise minus 30 Tage). Bei drei Raten liegt die zweite in der Mitte zwischen Buchungstag und Stichtag. Der Mindestabstand beträgt 14 Tage, sonst ist die Variante serverseitig deaktiviert. Diese Terminregel ist ein Vorschlag und braucht vor dem Live-Verkauf eine Freigabe. + +## ADR-4: Kapazität und Gutscheine ohne Überbuchung + +- Jede platzbelegende Transaktion sperrt zuerst die Camp-Zeile (`SELECT … FOR UPDATE`), dann gegebenenfalls die Gutscheinzeile. Durch die feste Reihenfolge entstehen keine Deadlocks. Zwei Personen bekommen nie denselben letzten Platz oder dieselbe letzte Einlösung (Tests mit 8 bzw. 7 parallelen Buchungen). +- Belegt sind CONVERTED- und HELD-Reservierungen (verzögerte Zahlung wie SEPA) sowie ACTIVE-Reservierungen, die noch nicht abgelaufen sind. Ein abgelaufener Checkout gibt Platz und Gutschein automatisch frei. +- Test- und Livebuchungen werden getrennt gezählt (`livemode`). +- Öffentlich sind keine Platzzahl, kein Restplatz-Zähler und keine Liste gültiger Codes sichtbar. Abgelehnte Codes bekommen immer dieselbe neutrale Meldung. + +## ADR-5: Webhooks und Zustände + +- Die Signatur wird gegen den unveränderten Body geprüft (`request.text()`). Die Event-ID wird in `StripeEvent` gespeichert, wodurch jedes Event genau einmal verarbeitet wird. +- Buchungsstatus und Zahlungsstatus sind getrennt. Die Übergänge sind monoton und laufen unter Buchungssperre, so dass ein spätes „expired“ keine bezahlte Buchung aufhebt. +- Ist nach einer verspäteten Zahlung der Platz inzwischen vergeben, wird nicht überbucht. Die Buchung geht auf „Prüfung nötig“, und Admin und Kundin werden informiert. +- Die Erfolgsseite zeigt nur den Datenbankstand. Bezahlt markieren darf nur der Webhook oder der Worker-Abgleich, der Stripe selbst abfragt. +- E-Mails laufen über einen transaktionalen Postausgang: gleiche Transaktion wie die Statusänderung, Versand danach, Wiederholung durch den Worker. + +## ADR-6: Verkaufsfreigabe + +`src/lib/sales-gate.ts` wird von jedem Checkout-Endpunkt aufgerufen. + +- **Livemodus (`sk_live_`):** Verkauf nur mit laufender echter Preisphase, eingeschaltetem Live-Verkauf und allen elf Bestätigungen. +- **Testmodus:** Checkout ist ohne Freigaben möglich, die Preisphase lässt sich simulieren. +- **Schutz vor Fehlkonfiguration:** Ein Live-Schlüssel außerhalb von `APP_ENV=production` bricht mit einem Fehler ab. + +## ADR-7: Adminbereich + +Better Auth übernimmt E-Mail und Passwort, Registrierung ist ausgeschaltet. Konten entstehen nur über die Kommandozeile, TOTP-2FA ist Pflicht. Rate Limits der Anmeldung liegen in PostgreSQL, Passwörter werden gegen Datenlecks geprüft. Jede Seite, jede Server-Action und der CSV-Export prüfen Sitzung und 2FA selbst. Der Proxy leitet nur vorab um. Alle Buchungs-, Zahlungs- und Konfigurationsaktionen landen im AuditLog. diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md new file mode 100644 index 0000000..a7d1b96 --- /dev/null +++ b/docs/BETRIEB.md @@ -0,0 +1,117 @@ +# Betrieb auf RKE2 + +## 1. Images bauen + +**Automatisch (Gitea Actions):** `.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main`, bei Tags `v*` oder manuell per „Run workflow“ beide Images und pusht sie in die Gitea-Registry: + +- `main` ergibt die Tags `latest` und `sha-` +- `v0.1.0` ergibt zusätzlich den Tag `0.1.0` + +Einmalig einrichten: +- ein Personal Access Token mit Scope `package` als Repo-Secret `REGISTRY_TOKEN` hinterlegen (Repo-Einstellungen → Actions → Secrets) +- einen Runner mit dem Label `docker-builder` bereitstellen (wie bei Wabenhain) + +Ein Release entsteht dann so: `git tag v0.1.0 && git push origin v0.1.0`, anschließend den Tag im Overlay setzen. + +**Manuell:** + +```bash +docker build --target runner -t git.encry.de/nick.rosenloecher/artista-filmcamp:0.1.0 . +docker build --target tools -t git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0 . +docker push … # beide Images, immer mit identischem Tag +``` + +Der Build braucht Internet für `next/font` (Schriften werden eingebettet), aber weder Datenbank noch Secrets. + +## 2. Einmalig einrichten + +1. **Overlay anpassen** (`k8s/overlays/production/`): Image-Namen und Tag, Domain in `kustomization.yaml` (APP_URL) und `ingress.yaml`, `ingressClassName` (`kubectl get ingressclass`), ClusterIssuer (Beispiel `letsencrypt-prod`) und TLS-Secret-Name. +2. **StorageClasses:** + - PostgreSQL bekommt Blockspeicher mit ReadWriteOnce, auf keinen Fall eine SMB-Freigabe (Begründung in `k8s/base/postgres.yaml`). + - Das Medienvolume braucht ReadWriteMany, sonst `replicas: 1`. + - Das Backup-Volume darf NFS oder SMB sein. +3. **Secret anlegen:** Befehl steht in `k8s/secret.example.yaml`, besser über Sealed Secrets oder den External Secrets Operator. +4. **Anwenden:** `kubectl apply -k k8s/overlays/production` +5. **Migration** (siehe Abschnitt 3), danach das erste Admin-Konto: + ```bash + kubectl -n artista run admin-create --rm -it --restart=Never --labels=artista/db-client=true \ + --image=git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0 \ + --overrides='{"spec":{"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0","command":["tsx","scripts/create-admin.ts","--email","du@example.org","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-config"}},{"secretRef":{"name":"artista-secrets"}}]}]}}' + ``` + Das einmalige Passwort wird ausgegeben. Bei der ersten Anmeldung unter `/admin` wird die 2FA eingerichtet. + +## 3. Update ausrollen (kontrollierter Migrationsschritt) + +```bash +# 1. Neuen Tag im Overlay setzen, anwenden (App rollt noch NICHT aus, solange Tag gleich bleibt) +kubectl apply -k k8s/overlays/production +# 2. Migration bewusst starten und abwarten +JOB=migrate-$(date +%Y%m%d%H%M) +kubectl -n artista create job $JOB --from=cronjob/artista-migrate +kubectl -n artista wait --for=condition=complete job/$JOB --timeout=5m +# 3. Rollout beobachten +kubectl -n artista rollout status deploy/artista-app +``` + +Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können. + +## 4. Stripe + +- **Schlüssel:** Im Test `sk_test_…`, live `sk_live_…` (nur mit `APP_ENV=production`). Empfohlen ist ein eingeschränkter Schlüssel mit Schreibrechten auf Customers, Checkout Sessions, PaymentIntents, PaymentMethods und Refunds. +- **Webhook-Endpunkt:** `https:///api/stripe/webhook`. Diese Events abonnieren: + `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired`, `payment_intent.succeeded`, `payment_intent.payment_failed`, `charge.refunded`, `refund.created`, `refund.updated`, `charge.dispute.created`. + Das Signing Secret gehört als `STRIPE_WEBHOOK_SECRET` ins Secret. Test- und Live-Endpunkt haben getrennte Secrets. +- **Zahlungsarten** (Dashboard → Zahlungsmethoden): Für Ratenzahlung nur wiederverwendbare Methoden aktivieren (Karten, gegebenenfalls SEPA-Lastschrift). Checkout blendet bei Ratenzahlung ohnehin nur Methoden ein, die `setup_future_usage` unterstützen. +- **Test lokal:** `stripe listen --forward-to localhost:3000/api/stripe/webhook` +- **Sicherheitsnetz:** Der Worker gleicht bei abgelaufenen Reservierungen und hängenden Einzügen immer mit Stripe ab. Ein verlorener Webhook führt so nicht zu verlorenen Zahlungen. + +## 5. E-Mail + +`MAIL_TRANSPORT=smtp` mit `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER` und `SMTP_PASSWORD`. Den Absender (`MAIL_FROM`) per SPF, DKIM und DMARC für die Absenderdomain freischalten. `ADMIN_NOTIFY_EMAIL` erhält Hinweise zu „Prüfung nötig“. Nicht zustellbare Mails zeigt das Admin-Dashboard. Der Worker versucht jede Mail bis zu achtmal. + +## 6. Worker + +Der CronJob `artista-worker` läuft alle 10 Minuten und kümmert sich um: +- abgelaufene Reservierungen, vorher mit Rückfrage bei Stripe +- hängende Einzüge +- fällige Raten +- das Trennen der Zahlungsmethode nach der letzten Rate +- den E-Mail-Versand +- das Aufräumen der Rate Limits + +Ein fehlgeschlagener Lauf erscheint als fehlgeschlagener Job (`kubectl -n artista get jobs`). + +## 7. Sicherung und Wiederherstellung + +- **Sicherung:** Der CronJob `artista-db-backup` erstellt täglich um 3:15 Uhr einen Dump im `pg_dump`-Custom-Format nach `artista-db-backup` und bewahrt ihn 30 Tage auf. +- **Aus dem Cluster sichern (regelmäßig):** + ```bash + POD=$(kubectl -n artista get pod -l app.kubernetes.io/name=artista-postgres -o name | head -1) + kubectl -n artista exec $POD -- pg_dump -U artista -d artista --format=custom > artista-$(date +%F).dump + ``` +- **Wiederherstellen:** + 1. App stoppen: `kubectl -n artista scale deploy/artista-app --replicas=0` und den Worker-CronJob pausieren: `kubectl -n artista patch cronjob artista-worker -p '{"spec":{"suspend":true}}'` + 2. Einspielen: `kubectl -n artista exec -i $POD -- pg_restore -U artista -d artista --clean --if-exists < artista-YYYY-MM-DD.dump` + 3. Migration laufen lassen (Abschnitt 3), App wieder hochskalieren, Worker fortsetzen. + 4. Abgleich: Zahlungen, die nach dem Sicherungszeitpunkt eingingen, im Stripe-Dashboard prüfen. Stripe stellt Webhooks bis zu drei Tage erneut zu (Dashboard → Webhooks → „Erneut senden“). +- **Medien:** Das Volume `artista-media` separat sichern, z. B. mit Volume-Snapshots oder der Sicherung des Speichersystems. +- **Wiederherstellung üben:** mindestens einmal vor dem Live-Verkauf in einen Test-Namespace. + +## 8. Stornierung und Erstattung + +Die Software trifft keine Storno-Entscheidung, sie setzt sie nur nachvollziehbar um. + +1. **Stornieren** (Admin → Buchung → „Stornieren“, Grund ist Pflicht): Offene Raten werden beendet, Platz und Gutschein freigegeben, die gespeicherte Zahlungsmethode getrennt und offene Checkout-Sessions geschlossen. Danach wird nichts mehr abgebucht. +2. **Erstatten** (je bezahlter Rate, Betrag frei wählbar, höchstens bis zum gezahlten Betrag): Welcher Betrag erstattet wird, bestimmen die freigegebenen Stornobedingungen. Die Erstattung wird bei Stripe ausgelöst, der Zahlungsstatus aktualisiert sich per Webhook (`charge.refunded`). Ein doppeltes Abschicken wird über einen Idempotenzschlüssel abgefangen. +3. **Prüfung nötig** entsteht bei verspäteter Zahlung ohne freien Platz, Doppelzahlung, Rückbuchung oder endgültig fehlgeschlagener Rate. Dann entweder „Buchung bestätigen“ (nur mit freiem Platz, sonst vorher Kapazität erhöhen) oder stornieren und erstatten. +4. **Dokumentation:** Alle Schritte stehen im Protokoll der Buchung (AuditLog) mit handelnder Person. + +## 9. Medien + +Dateien (jpg, png, webp, avif, mp4, webm) in das Volume `artista-media` kopieren: + +```bash +kubectl -n artista cp villa-aussen.jpg :/app/media/villa-aussen.jpg # Volume ist im App-Pod read-only → einen Hilfs-Pod mit Schreibzugriff nutzen +``` + +Danach in `src/content/site.ts` eintragen. Hotlinking fremder Bilder, etwa von Booking.com, ist nicht vorgesehen: Nur Bilder mit geklärter Nutzungsberechtigung ablegen. diff --git a/docs/OFFENE-PUNKTE.md b/docs/OFFENE-PUNKTE.md new file mode 100644 index 0000000..7a7e58f --- /dev/null +++ b/docs/OFFENE-PUNKTE.md @@ -0,0 +1,41 @@ +# Noch benötigte Inhalte und Freigaben + +Solange die mit **(Freigabe)** markierten Punkte fehlen, bleibt der Live-Verkauf serverseitig gesperrt. Der aktuelle Stand steht im Admin-Dashboard unter „Fehlt für den Live-Verkauf“. + +## Inhalte + +| Punkt | Wo eintragen | +|---|---| +| Camp-Termin (Anreise/Abreise) **(Freigabe)** | Admin → Einstellungen | +| Genauer Ort und Region **(Freigabe)** | Admin → Einstellungen | +| Verbindliche Zimmer- und Bettbeschreibung, insbesondere ob ein französisches Doppelbett geteilt wird **(Freigabe)** | Admin → Einstellungen | +| Booking-Link der Unterkunft | `src/content/site.ts` → `accommodation.bookingUrl` | +| Unterkunftsbilder mit geklärter Nutzungsberechtigung | Medienvolume + `site.ts` | +| Namen, Rollen, Kurzbeschreibungen und Porträts der beiden Dozentinnen | `site.ts` → `teachers` | +| Link zur ARTISTA-Stammmodel-Seite und daraus abgeleiteter Text „Über ARTISTA“ | `site.ts` → `about` | +| Drei echte, freigegebene Feedback-Videos mit Namen und Begleittext | Medienvolume + `site.ts` → `videos` | +| Hero-Leitbild oder -Video | `site.ts` → `hero.media` | +| Bestätigte Leistungen (Workshops, Verpflegung, Programm) **(Freigabe)** | `site.ts` → `services`, danach Admin „Leistungen bestätigt“ | +| Verbindliches Programm (bis dahin nur als Beispiel gekennzeichnet) | `site.ts` → `exampleSchedule` | +| Veranstalter- und Kontaktdaten **(Freigabe)** | Admin → Einstellungen | +| Freigabe der Entwurfstexte (Headline, Camp-Text, Zielgruppe) | `site.ts` | + +## Recht, Steuer, Zahlung + +| Punkt | Hinweis | +|---|---| +| Steuerliche Behandlung und korrekte Preisangabe **(Freigabe)** | Es wird bewusst keine USt-Aussage angezeigt, bis der Hinweis eingetragen und bestätigt ist | +| Buchungs-, Storno- und Zahlungsbedingungen **(Freigabe)** | Als Dokument oder Seite hinterlegen (Link und Version im Admin). Es wurden keine AGB formuliert | +| Reiserechtliche Einordnung (Pauschalreise?) samt gegebenenfalls nötiger Insolvenzabsicherung und Formblätter **(Freigabe)** | Rechtliche Aussagen aus der Kalkulation wurden nicht übernommen | +| Vollständiges Impressum und Datenschutzerklärung | Technische Grundlage siehe `/datenschutz` (nur in Entwicklung sichtbar) | +| Verkaufbare Platzzahl (vorläufig 10) **(Freigabe)** | Admin → Einstellungen | +| Kontingent Stammmodel-Buchungen (vorläufig 4) **(Freigabe)** | Admin → Einstellungen | +| Ratenzahlung: rechtliche Prüfung, Terminregel, Freigabe für den Livemodus | Admin → Einstellungen (zwei Häkchen) | +| Teilnahme Minderjähriger | Die Buchung verlangt derzeit die Erklärung „Ich bin volljährig“. Bitte bestätigen oder die Regel vorgeben | + +## Betrieb + +- Stripe-Konto live verifiziert, Webhook-Endpunkte (Test und Live) mit Secrets angelegt. +- SMTP-Anbieter sowie SPF, DKIM und DMARC der Absenderdomain. +- Domain, TLS und StorageClasses im Cluster (siehe docs/BETRIEB.md). +- Wiederherstellung aus der Sicherung einmal geübt. diff --git a/docs/TESTS.md b/docs/TESTS.md new file mode 100644 index 0000000..e1b40a8 --- /dev/null +++ b/docs/TESTS.md @@ -0,0 +1,53 @@ +# Tests und Abnahme + +Stand 27.09.2026. Alle aufgeführten Läufe sind lokal ausgeführt und bestanden. + +## Automatisierte Tests + +`pnpm test` umfasst 72 Tests in 6 Dateien. Sie laufen gegen eine echte, frisch gestartete PostgreSQL-18-Instanz, damit Zeilensperren und Trigger mitgetestet werden. + +| Geschäftsrisiko | Datei | +|---|---| +| Alle vier Gesamtpreise, 28 % statt 30 % | `tests/pricing.test.ts` | +| Alle zwölf Ratenbeträge exakt, einschließlich 1.237,50 € | `tests/payment-plan.test.ts` | +| Zeitgrenzen 01.11.2026 und 01.01.2027 in Europe/Berlin, Sommerzeit | `tests/pricing.test.ts`, `booking.integration.test.ts` | +| Ungültige, inaktive, abgelaufene, noch nicht gültige, fremd gebundene und ausgeschöpfte Gutscheine | `booking.integration.test.ts` | +| Gleichzeitige Einlösung eines begrenzten Codes (7 parallel, 2 erlaubt) und Gesamtkontingent | `booking.integration.test.ts` | +| Gleichzeitige Buchungen bei knapper Kapazität (8 parallel, 2 Plätze) | `booking.integration.test.ts` | +| Abgelaufene Reservierungen, verzögerte Zahlung (SEPA), verspätete Zahlung mit und ohne freien Platz, verlorener Webhook | `payments.integration.test.ts` | +| Doppelte und ungeordnete Stripe-Events, Livemodus-Event im Testbetrieb, Signaturprüfung | `payments.integration.test.ts` | +| Zahlungserfolg, Abbruch, Fehler, Teilzahlung, Authentifizierung, Erstattung | `payments.integration.test.ts`, `admin.integration.test.ts` | +| Sperre aller Live-Checkout-Zugänge ohne Freigabe, Vorschauphase | `sales-gate.test.ts`, `booking.integration.test.ts` | +| Keine weitere Abbuchung nach der letzten Rate oder nach Stornierung, Zahlungsmethode getrennt | `payments.integration.test.ts`, `admin.integration.test.ts` | +| Unveränderlicher Preis-Snapshot (DB-Trigger), Idempotenz bei Doppelklick | `booking.integration.test.ts` | + +## End-to-End und Sicht + +`pnpm test:e2e` umfasst 9 Tests auf den Profilen Smartphone (Pixel 7), Tablet (820 × 1180) und Desktop (1440 × 900): + +- axe-Prüfung WCAG 2.1 AA ohne Verstöße +- kein horizontales Scrollen +- keine öffentliche Platzzahl +- Buchungsdialog: Pflichtfelder, Gutschein, Ratenplan 3 × 660 €, Erklärungen, Fokus beim Öffnen und Schließen per Escape +- Admin-Weiterleitung, Export mit gefälschtem Cookie ergibt 401, Statusseite mit `noindex` + +Bildschirmfotos aller drei Größen wurden visuell geprüft. + +## Manuell über HTTP geprüft + +- Admin-Ablauf: + - Öffentliche Registrierung wird mit 400 abgelehnt, ein falsches Passwort mit 401. + - Ohne 2FA leitet `/admin` zur Einrichtung um, der Export antwortet 401. + - Nach der TOTP-Aktivierung sind Dashboard und CSV mit 200 erreichbar. +- CSRF: Anfragen ohne JSON bekommen 415, fremde Origins 403. +- Preismanipulation: Ein mitgeschicktes `totalCents` wird ignoriert. +- Sicherheitsheader (CSP mit Nonce, HSTS, X-Frame-Options) sind gesetzt. +- Der Standalone-Build läuft mit Datenbank. Worker-Lauf und `admin:create` funktionieren, eine doppelte Anlage wird abgewiesen. +- Build, Typprüfung und ESLint laufen ohne Fehler, Kustomize rendert 17 Ressourcen. + +## Nicht ausgeführt / Einschränkungen + +- **Echte Stripe-Testbuchung** gegen die Stripe-API: Ein Stripe-Testschlüssel lag nicht vor. Die Stripe-Logik ist gegen eine Attrappe mit Stripe-Semantik getestet (Idempotenz, Session-Zustände, Off-Session-Ergebnisse), die Webhook-Signatur mit der echten Stripe-Bibliothek. Mit Schlüssel: Anleitung in der README (`stripe listen`, Testkarten). +- **Docker-Image und Cluster:** Auf dem Entwicklungsrechner gibt es kein Docker und keinen Cluster-Zugang. Das Dockerfile ist nicht gebaut und nicht im Cluster ausgerollt. Geprüft sind der Standalone-Build und das Rendern der Manifeste. +- **SMTP-Versand:** nur über den Datei-Testausgang geprüft. +- **Referenzseite:** nur über eine Textzusammenfassung ausgewertet (siehe ARCHITEKTUR.md). diff --git a/e2e/onepage.spec.ts b/e2e/onepage.spec.ts new file mode 100644 index 0000000..daa02fe --- /dev/null +++ b/e2e/onepage.spec.ts @@ -0,0 +1,77 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; + +/** + * Voraussetzung: Stripe-Testschluessel und `pnpm db:seed` (simulierte + * Early-Bird-Phase, Testgutschein TEST-STAMMMODEL). Die Zahlung selbst wird + * nicht ausgeloest — der Test endet vor der Weiterleitung zu Stripe. + */ + +test("Onepage: Aufbau, kein horizontales Scrollen, Barrierefreiheit", async ({ page }, testInfo) => { + await page.goto("/"); + await expect(page.getByRole("heading", { level: 1 })).toContainText("ARTISTA"); + for (const id of ["camp", "leistungen", "unterkunft", "dozentinnen", "artista", "preis", "fragen"]) { + await expect(page.locator(`#${id}`)).toBeAttached(); + } + // Keine oeffentliche Platzzahl und kein Restplatz-Zaehler. + await expect(page.locator("body")).not.toContainText(/Restpl|noch \d+ Pl|Plätze frei/); + + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); + expect(overflow).toBeLessThanOrEqual(0); + + await page.waitForTimeout(1600); // Hero-Eroeffnung abwarten + await page.screenshot({ path: testInfo.outputPath(`onepage-${testInfo.project.name}.png`), fullPage: true }); + + const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze(); + expect(results.violations.map((v) => `${v.id}: ${v.nodes.length}`)).toEqual([]); +}); + +test("Buchungsdialog: Daten, Gutschein, Ratenplan, Erklaerungen, Fokus", async ({ page }, testInfo) => { + await page.goto("/"); + const trigger = page.locator("#top").getByRole("button", { name: "Platz buchen" }); + await trigger.click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog.getByRole("heading", { name: "Deine Daten" })).toBeFocused(); + + // Pflichtfelder werden verstaendlich gemeldet. + await dialog.getByRole("button", { name: "Weiter" }).click(); + await expect(dialog.getByText("Bitte gib deinen Vornamen an.")).toBeVisible(); + + await dialog.getByLabel("Vorname").fill("Test"); + await dialog.getByLabel("Nachname").fill("Person"); + await dialog.getByLabel("E-Mail").fill("test@example.org"); + await dialog.getByLabel("Telefon").fill("+49 30 123456"); + await dialog.getByRole("button", { name: "Weiter" }).click(); + + await expect(dialog.getByRole("heading", { name: "Zahlungsweise" })).toBeFocused(); + await dialog.getByRole("radio", { name: /Drei Raten/ }).check(); + await dialog.getByLabel("Gutscheincode (optional)").fill("test-stammmodel"); + await dialog.getByRole("button", { name: "Anwenden" }).click(); + await expect(dialog.getByText("Gutschein angewendet.")).toBeVisible(); + await expect(dialog.getByText("1.980,00 €").first()).toBeVisible(); + + await dialog.getByRole("button", { name: "Weiter" }).click(); + await expect(dialog.getByText("Heute fällig: 660,00 €", { exact: false })).toBeVisible(); + await expect(dialog.getByText("Nicht enthalten:", { exact: false })).toBeVisible(); + + // Ohne Erklaerungen keine Buchung. + await dialog.getByRole("button", { name: "Zahlungspflichtig buchen" }).click(); + await expect(dialog.getByText("Bitte stimme dem Einzug der weiteren Raten zu.")).toBeVisible(); + await dialog.screenshot({ path: testInfo.outputPath(`dialog-${testInfo.project.name}.png`) }); + + // Escape schliesst, Fokus kehrt zum ausloesenden Button zurueck. + await page.keyboard.press("Escape"); + await expect(dialog).toBeHidden(); + await expect(trigger).toBeFocused(); +}); + +test("Verwaltung und Buchungsstatus sind geschuetzt und nicht indexierbar", async ({ page, request }) => { + await page.goto("/admin/buchungen"); + await expect(page).toHaveURL(/\/admin\/anmelden$/); + const exportResponse = await request.get("/admin/export", { maxRedirects: 0, headers: { cookie: "better-auth.session_token=gefaelscht" } }); + expect(exportResponse.status()).toBe(401); + const status = await request.get("/buchung/status/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + expect(status.status()).toBe(404); + expect(status.headers()["x-robots-tag"]).toContain("noindex"); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..93aea04 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +export default defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "src/generated/**"]), +]); diff --git a/k8s/base/app.yaml b/k8s/base/app.yaml new file mode 100644 index 0000000..7a03b47 --- /dev/null +++ b/k8s/base/app.yaml @@ -0,0 +1,129 @@ +# Next.js-Anwendung. Zustandslos: Buchungen, Kapazitaet, Gutscheine, +# Reservierungen, Rate Limits, E-Mail-Ausgang und Zahlungsjobs liegen in +# PostgreSQL — beliebig viele Replikate moeglich. +# +# Migrationen laufen NICHT beim Start, sondern als eigener, kontrollierter +# Schritt (jobs.yaml, CronJob "artista-migrate", siehe docs/BETRIEB.md). +apiVersion: apps/v1 +kind: Deployment +metadata: + name: artista-app +spec: + replicas: 2 + revisionHistoryLimit: 5 + strategy: + type: RollingUpdate + rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } + selector: + matchLabels: + app.kubernetes.io/name: artista-app + template: + metadata: + labels: + app.kubernetes.io/name: artista-app + artista/db-client: "true" + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + seccompProfile: { type: RuntimeDefault } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: artista-app + containers: + - name: app + image: artista-modelcamp + ports: + - name: http + containerPort: 3000 + envFrom: + - configMapRef: { name: artista-config } + - secretRef: { name: artista-secrets } + readinessProbe: + httpGet: { path: /api/health/ready, port: http } + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + # Bewusst ohne Datenbank: eine DB-Stoerung soll nicht zu + # Neustartschleifen fuehren. + httpGet: { path: /api/health, port: http } + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: { cpu: 200m, memory: 256Mi } + limits: { memory: 768Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + volumeMounts: + - name: media + mountPath: /app/media + readOnly: true + # Reiner Zwischenspeicher fuer optimierte Bilder: fluechtig ok. + - name: next-cache + mountPath: /app/.next/cache + - name: tmp + mountPath: /tmp + volumes: + - name: media + persistentVolumeClaim: { claimName: artista-media } + - name: next-cache + emptyDir: { sizeLimit: 512Mi } + - name: tmp + emptyDir: { sizeLimit: 64Mi } +--- +apiVersion: v1 +kind: Service +metadata: + name: artista-app +spec: + selector: + app.kubernetes.io/name: artista-app + ports: + - name: http + port: 80 + targetPort: http +--- +# Die App nimmt Verbindungen nur vom Ingress-Controller an. Ohne diese Regel +# koennte jeder Pod im Cluster den Service direkt ansprechen und per +# gefaelschtem X-Real-Ip-Header die Rate Limits (inkl. Admin-Login/2FA) +# umgehen. Namespace des Traefik-Controllers anpassen +# ("kubectl get pods -A | grep traefik"). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: artista-app-ingress +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: artista-app + policyTypes: ["Ingress"] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik + ports: + - port: 3000 + # Kubelet-Probes kommen vom Knoten und sind von NetworkPolicies je nach + # CNI (Canal/Calico in RKE2) ausgenommen. +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: artista-app +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: artista-app diff --git a/k8s/base/config.yaml b/k8s/base/config.yaml new file mode 100644 index 0000000..06aedca --- /dev/null +++ b/k8s/base/config.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: artista + labels: + # Pod Security Admission: nur eingeschraenkte (non-root) Pods. + pod-security.kubernetes.io/enforce: restricted +--- +# Nicht geheime Einstellungen. Geheimnisse: k8s/secret.example.yaml. +apiVersion: v1 +kind: ConfigMap +metadata: + name: artista-config +data: + APP_ENV: "production" + APP_URL: "https://modelcamp.example.org" + DATABASE_POOL_SIZE: "10" + MAIL_TRANSPORT: "smtp" + MAIL_FROM: "ARTISTA MODELCAMP " + ADMIN_NOTIFY_EMAIL: "" + SMTP_PORT: "587" + SMTP_SECURE: "false" + MEDIA_DIR: "/app/media" + EXTERNAL_VIDEO_PROVIDER: "none" +--- +# Freigegebene Bilder und Videos (Unterkunft, Portraets, Feedback-Videos). +# Dauerhaft, deshalb ein PersistentVolumeClaim und KEIN emptyDir. +# +# ReadWriteMany, weil mehrere App-Replikate dasselbe Verzeichnis lesen. +# Geeignet: z. B. Longhorn (RWX), NFS oder eine SMB/CSI-Freigabe — fuer +# Medien ist ein Netzwerkdateisystem unkritisch (anders als fuer PostgreSQL). +# Steht nur ReadWriteOnce zur Verfuegung: replicas im Deployment auf 1 setzen. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: artista-media +spec: + accessModes: ["ReadWriteMany"] + # storageClassName: longhorn + resources: + requests: + storage: 5Gi +--- +# Die Datenbank ist nur fuer Pods mit dem Label artista/db-client erreichbar +# (App, Migration, Worker, Sicherung). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: artista-postgres-access +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: artista-postgres + policyTypes: ["Ingress"] + ingress: + - from: + - podSelector: + matchLabels: + artista/db-client: "true" + ports: + - port: 5432 diff --git a/k8s/base/jobs.yaml b/k8s/base/jobs.yaml new file mode 100644 index 0000000..05a69a4 --- /dev/null +++ b/k8s/base/jobs.yaml @@ -0,0 +1,159 @@ +# --- Migration ---------------------------------------------------------------- +# Als SUSPENDIERTER CronJob definiert, damit Kustomize das Image mit der +# App gleichzieht. Er laeuft nie von selbst, sondern bewusst vor jedem +# Rollout: +# kubectl -n artista create job migrate-$(date +%Y%m%d%H%M) --from=cronjob/artista-migrate +# kubectl -n artista wait --for=condition=complete job/migrate-… --timeout=5m +apiVersion: batch/v1 +kind: CronJob +metadata: + name: artista-migrate +spec: + suspend: true + schedule: "0 0 1 1 *" + jobTemplate: + spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 86400 + template: + metadata: + labels: + artista/db-client: "true" + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1001 + seccompProfile: { type: RuntimeDefault } + containers: + - name: migrate + image: artista-modelcamp-tools + command: ["prisma", "migrate", "deploy"] + envFrom: + - secretRef: { name: artista-secrets } + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { memory: 512Mi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } +--- +# --- Worker ------------------------------------------------------------------- +# Gibt abgelaufene Reservierungen frei (nach Rueckfrage bei Stripe), zieht +# faellige Raten ein, trennt Zahlungsmethoden nach der letzten Rate, +# versendet den E-Mail-Ausgang. Idempotent; Forbid verhindert +# ueberlappende Laeufe zusaetzlich. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: artista-worker +spec: + schedule: "*/10 * * * *" + timeZone: "Europe/Berlin" + concurrencyPolicy: Forbid + startingDeadlineSeconds: 300 + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 5 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 900 + template: + metadata: + labels: + artista/db-client: "true" + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1001 + seccompProfile: { type: RuntimeDefault } + containers: + - name: worker + image: artista-modelcamp-tools + command: ["tsx", "scripts/worker.ts"] + envFrom: + - configMapRef: { name: artista-config } + - secretRef: { name: artista-secrets } + resources: + requests: { cpu: 50m, memory: 192Mi } + limits: { memory: 512Mi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } +--- +# --- Sicherung ---------------------------------------------------------------- +# Taeglicher pg_dump mit 30 Tagen Aufbewahrung. Das Backup-Volume darf ein +# Netzwerkspeicher (NFS/SMB) sein. Zusaetzlich regelmaessig aus dem Cluster +# kopieren (docs/BETRIEB.md, "Sicherung und Wiederherstellung"). +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: artista-db-backup +spec: + accessModes: ["ReadWriteOnce"] + # storageClassName: smb-backup + resources: + requests: + storage: 10Gi +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: artista-db-backup +spec: + schedule: "15 3 * * *" + timeZone: "Europe/Berlin" + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 5 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + artista/db-client: "true" + spec: + restartPolicy: OnFailure + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + fsGroup: 70 + seccompProfile: { type: RuntimeDefault } + containers: + - name: pg-dump + # Gleiche Hauptversion wie der Server. + image: postgres:18.1-alpine + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: { name: artista-secrets, key: DB_PASSWORD } + command: ["/bin/sh", "-c"] + args: + - | + set -eu + set -o pipefail + target="/backup/artista-$(date -u +%Y-%m-%d_%H%M).dump" + # Custom-Format: komprimiert, selektiv wiederherstellbar. + pg_dump --host=artista-postgres --username=artista --dbname=artista \ + --format=custom --file="${target}.partial" + mv "${target}.partial" "${target}" + find /backup -name 'artista-*.dump.partial' -mtime +1 -delete + find /backup -name 'artista-*.dump' -mtime +30 -delete + ls -lh /backup + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { memory: 256Mi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + volumeMounts: + - name: backup + mountPath: /backup + volumes: + - name: backup + persistentVolumeClaim: { claimName: artista-db-backup } diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..78569a7 --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,14 @@ +# Basis fuer RKE2. Nicht direkt anwenden — ueber ein Overlay +# (k8s/overlays/production), das Domain, Images und TLS setzt. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: artista +resources: + - config.yaml + - postgres.yaml + - app.yaml + - jobs.yaml +labels: + - pairs: + app.kubernetes.io/part-of: artista-modelcamp + includeSelectors: false diff --git a/k8s/base/postgres.yaml b/k8s/base/postgres.yaml new file mode 100644 index 0000000..cc437c5 --- /dev/null +++ b/k8s/base/postgres.yaml @@ -0,0 +1,82 @@ +# PostgreSQL fuer Buchungen, Zahlungen, Gutscheine und Protokoll. +# +# SPEICHER: Blockspeicher mit ReadWriteOnce verwenden (z. B. Longhorn, +# local-path, Ceph RBD). KEINE SMB/CIFS-Freigabe als Datenverzeichnis: +# SMB bildet POSIX-Sperren, fsync-Garantien und Dateirechte nicht so ab, +# wie PostgreSQL sie fuer Absturzsicherheit voraussetzt — Datenverlust bei +# Stromausfall oder Neustart ist die Folge. Sicherungen (backup-CronJob) +# duerfen dagegen auf SMB/NFS liegen. +# +# Alternative fuer hoehere Verfuegbarkeit: CloudNativePG-Operator; dann +# dieses Manifest weglassen und DATABASE_URL auf dessen Service zeigen lassen. +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: artista-postgres +spec: + serviceName: artista-postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: artista-postgres + template: + metadata: + labels: + app.kubernetes.io/name: artista-postgres + spec: + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + fsGroup: 70 + seccompProfile: { type: RuntimeDefault } + containers: + - name: postgres + image: postgres:18.1-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: artista + - name: POSTGRES_DB + value: artista + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: { name: artista-secrets, key: DB_PASSWORD } + volumeMounts: + # Ab PostgreSQL 18 liegt PGDATA unter /var/lib/postgresql/18/docker. + - name: data + mountPath: /var/lib/postgresql + readinessProbe: + exec: { command: ["pg_isready", "-U", "artista"] } + periodSeconds: 5 + livenessProbe: + exec: { command: ["pg_isready", "-U", "artista"] } + initialDelaySeconds: 30 + periodSeconds: 20 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { memory: 1Gi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + # storageClassName: longhorn + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: artista-postgres +spec: + clusterIP: None + selector: + app.kubernetes.io/name: artista-postgres + ports: + - port: 5432 diff --git a/k8s/overlays/production/ingress.yaml b/k8s/overlays/production/ingress.yaml new file mode 100644 index 0000000..53e1f8f --- /dev/null +++ b/k8s/overlays/production/ingress.yaml @@ -0,0 +1,64 @@ +# Ingress fuer Traefik mit cert-manager. +# Anpassen: Host (auch in APP_URL, kustomization.yaml), ingressClassName +# ("kubectl get ingressclass"), ClusterIssuer, TLS-Secret-Name. +# +# X-Forwarded-For/X-Real-Ip: Rate Limits verlassen sich darauf, dass Traefik +# vom Client mitgeschickte Werte verwirft. Dafuer im Traefik-EntryPoint +# forwardedHeaders.trustedIPs nur auf echte vorgelagerte Proxys setzen. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: artista + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" +spec: + ingressClassName: traefik + tls: + - hosts: + - modelcamp.example.org + secretName: artista-tls + rules: + - host: modelcamp.example.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: artista-app + port: + name: http +--- +# HTTP -> HTTPS. Entfaellt, wenn Traefik das bereits EntryPoint-weit tut +# (Helm: ports.web.redirections.entryPoint.to=websecure). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: artista-http-redirect + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: artista-redirect-https@kubernetescrd +spec: + ingressClassName: traefik + rules: + - host: modelcamp.example.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: artista-app + port: + name: http +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/k8s/overlays/production/kustomization.yaml b/k8s/overlays/production/kustomization.yaml new file mode 100644 index 0000000..5856a45 --- /dev/null +++ b/k8s/overlays/production/kustomization.yaml @@ -0,0 +1,27 @@ +# Produktions-Overlay. Anpassen: Images/Tags, Domain, IngressClass, +# ClusterIssuer und TLS-Secret-Name. Anwenden: +# kubectl apply -k k8s/overlays/production +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: artista +resources: + - ../../base + - ingress.yaml + +images: + # Gebaut von .gitea/workflows/docker-build.yml: Git-Tag "v0.1.0" pushen + # ergibt Image-Tag "0.1.0" (Alternativen: "sha-", "latest"). + # Beide Images immer mit identischem Tag ausrollen. + - name: artista-modelcamp + newName: git.encry.de/nick.rosenloecher/artista-filmcamp + newTag: "0.1.0" + - name: artista-modelcamp-tools + newName: git.encry.de/nick.rosenloecher/artista-filmcamp-tools + newTag: "0.1.0" + +patches: + - target: { kind: ConfigMap, name: artista-config } + patch: |- + - op: replace + path: /data/APP_URL + value: https://modelcamp.example.org diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml new file mode 100644 index 0000000..fdcaaff --- /dev/null +++ b/k8s/secret.example.yaml @@ -0,0 +1,28 @@ +# VORLAGE — nicht mit echten Werten ins Repository einchecken. +# Anlegen z. B. per: +# kubectl -n artista create secret generic artista-secrets \ +# --from-literal=DB_PASSWORD='…' \ +# --from-literal=DATABASE_URL='postgresql://artista:…@artista-postgres:5432/artista' \ +# --from-literal=APP_SECRET="$(openssl rand -base64 48)" \ +# --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \ +# --from-literal=STRIPE_SECRET_KEY='sk_test_…' \ +# --from-literal=STRIPE_WEBHOOK_SECRET='whsec_…' \ +# --from-literal=SMTP_HOST='…' --from-literal=SMTP_USER='…' --from-literal=SMTP_PASSWORD='…' +# Besser noch: Sealed Secrets oder External Secrets Operator. +apiVersion: v1 +kind: Secret +metadata: + name: artista-secrets + namespace: artista +type: Opaque +stringData: + DB_PASSWORD: "" + # Passwort URL-kodiert einsetzen, falls Sonderzeichen enthalten sind. + DATABASE_URL: "postgresql://artista:@artista-postgres:5432/artista" + APP_SECRET: "" + BETTER_AUTH_SECRET: "" + STRIPE_SECRET_KEY: "" + STRIPE_WEBHOOK_SECRET: "" + SMTP_HOST: "" + SMTP_USER: "" + SMTP_PASSWORD: "" diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..352391c --- /dev/null +++ b/next.config.ts @@ -0,0 +1,56 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + poweredByHeader: false, + // Minimaler Node-Server fuer das Docker-Image (siehe Dockerfile). + output: "standalone", + images: { + formats: ["image/avif", "image/webp"], + qualities: [75, 85], + // Freigegebene Bilder liegen im persistenten Medienverzeichnis und + // werden unter /media/… ausgeliefert (src/app/media/[...path]/route.ts). + localPatterns: [{ pathname: "/media/**" }], + dangerouslyAllowSVG: false, + }, + // Turbopack-Dateicaches kollidieren im OneDrive-synchronisierten + // Arbeitsverzeichnis mit der Cloud-Synchronisierung (Erfahrung aus dem + // Nachbarprojekt Wabenhain). Im Container spielt das keine Rolle. + experimental: { + turbopackFileSystemCacheForDev: false, + turbopackFileSystemCacheForBuild: false, + }, + async headers() { + // Die Content-Security-Policy setzt src/proxy.ts pro Request mit Nonce. + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), payment=()" }, + { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" }, + ], + }, + { + // Statusseiten enthalten ein Zugangstoken in der URL: nie als + // Referrer weitergeben, nie indexieren, nie cachen. + source: "/buchung/:path*", + headers: [ + { key: "Referrer-Policy", value: "no-referrer" }, + { key: "X-Robots-Tag", value: "noindex, nofollow" }, + { key: "Cache-Control", value: "no-store" }, + ], + }, + { + source: "/admin/:path*", + headers: [ + { key: "X-Robots-Tag", value: "noindex, nofollow" }, + { key: "Cache-Control", value: "no-store" }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..41756ca --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "artista-modelcamp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "lint": "eslint", + "typecheck": "prisma generate && tsc --noEmit", + "postinstall": "prisma generate", + "db:dev": "node scripts/dev-db.mjs", + "db:migrate": "prisma migrate dev", + "db:deploy": "prisma migrate deploy", + "db:seed": "tsx scripts/seed-dev.ts", + "admin:create": "tsx scripts/create-admin.ts", + "worker": "tsx scripts/worker.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test" + }, + "packageManager": "pnpm@12.6.0", + "dependencies": { + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "better-auth": "1.7.6", + "motion": "13.4.4", + "next": "16.3.6", + "nodemailer": "10.0.10", + "pg": "8.23.0", + "qrcode": "1.5.4", + "react": "19.3.0", + "react-dom": "19.3.0", + "server-only": "0.0.1", + "stripe": "22.6.2", + "zod": "4.6.5" + }, + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "4.3.3", + "@types/node": "22.20.4", + "@types/nodemailer": "8.0.2", + "@types/pg": "8.20.0", + "@types/qrcode": "1.5.6", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "dotenv": "18.0.4", + "embedded-postgres": "18.4.0-beta.17", + "eslint": "^9.39.5", + "eslint-config-next": "16.3.6", + "prisma": "7.10.0", + "tailwindcss": "4.3.3", + "tsx": "4.23.15", + "typescript": "5.9.3", + "vitest": "4.1.11" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..4c1de16 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Sichtpruefung und Buchungsstrecke gegen eine laufende Instanz + * (pnpm build && pnpm start, Datenbank mit pnpm db:seed). + * Bildschirmfotos landen unter test-results/. + */ +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + reporter: [["list"]], + use: { + baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000", + locale: "de-DE", + timezoneId: "Europe/Berlin", + trace: "retain-on-failure", + }, + projects: [ + { name: "smartphone", use: { ...devices["Pixel 7"] } }, + { name: "tablet", use: { ...devices["Desktop Chrome"], viewport: { width: 820, height: 1180 }, hasTouch: true } }, + { name: "desktop", use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } } }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..a332be7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7177 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.6.0 + version: 12.6.0 + +packages: + + '@pnpm/exe.android-arm64@12.6.0': + resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.6.0': + resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.6.0': + resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.6.0': + resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.6.0': + resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.6.0': + resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.6.0': + resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.6.0': + resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.6.0': + resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.6.0': + resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.6.0': + resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.6.0': + resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.6.0': + resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.6.0': + resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + cpu: [x64] + os: [win32] + + pnpm@12.6.0: + resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.6.0': + optional: true + + '@pnpm/exe.android-x64@12.6.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.6.0': + optional: true + + '@pnpm/exe.darwin-x64@12.6.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64@12.6.0': + optional: true + + '@pnpm/exe.linux-ppc64@12.6.0': + optional: true + + '@pnpm/exe.linux-riscv64@12.6.0': + optional: true + + '@pnpm/exe.linux-s390x@12.6.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-x64@12.6.0': + optional: true + + '@pnpm/exe.win32-arm64@12.6.0': + optional: true + + '@pnpm/exe.win32-x64@12.6.0': + optional: true + + pnpm@12.6.0: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.6.0 + '@pnpm/exe.android-x64': 12.6.0 + '@pnpm/exe.darwin-arm64': 12.6.0 + '@pnpm/exe.darwin-x64': 12.6.0 + '@pnpm/exe.freebsd-x64': 12.6.0 + '@pnpm/exe.linux-arm64': 12.6.0 + '@pnpm/exe.linux-arm64-musl': 12.6.0 + '@pnpm/exe.linux-ppc64': 12.6.0 + '@pnpm/exe.linux-riscv64': 12.6.0 + '@pnpm/exe.linux-s390x': 12.6.0 + '@pnpm/exe.linux-x64': 12.6.0 + '@pnpm/exe.linux-x64-musl': 12.6.0 + '@pnpm/exe.win32-arm64': 12.6.0 + '@pnpm/exe.win32-x64': 12.6.0 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@prisma/adapter-pg': + specifier: 7.10.0 + version: 7.10.0 + '@prisma/client': + specifier: 7.10.0 + version: 7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3) + better-auth: + specifier: 1.7.6 + version: 1.7.6(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3))(mysql2@3.15.3)(next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@22.20.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vitest@4.1.11(@types/node@22.20.4)(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))) + motion: + specifier: 13.4.4 + version: 13.4.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: + specifier: 16.3.6 + version: 16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@22.20.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + nodemailer: + specifier: 10.0.10 + version: 10.0.10 + pg: + specifier: 8.23.0 + version: 8.23.0 + qrcode: + specifier: 1.5.4 + version: 1.5.4 + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + server-only: + specifier: 0.0.1 + version: 0.0.1 + stripe: + specifier: 22.6.2 + version: 22.6.2(@types/node@22.20.4) + zod: + specifier: 4.6.5 + version: 4.6.5 + devDependencies: + '@axe-core/playwright': + specifier: 4.13.0 + version: 4.13.0(playwright-core@1.63.0) + '@playwright/test': + specifier: 1.63.0 + version: 1.63.0 + '@tailwindcss/postcss': + specifier: 4.3.3 + version: 4.3.3 + '@types/node': + specifier: 22.20.4 + version: 22.20.4 + '@types/nodemailer': + specifier: 8.0.2 + version: 8.0.2 + '@types/pg': + specifier: 8.20.0 + version: 8.20.0 + '@types/qrcode': + specifier: 1.5.6 + version: 1.5.6 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + dotenv: + specifier: 18.0.4 + version: 18.0.4 + embedded-postgres: + specifier: 18.4.0-beta.17 + version: 18.4.0-beta.17 + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-config-next: + specifier: 16.3.6 + version: 16.3.6(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + prisma: + specifier: 7.10.0 + version: 7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) + tailwindcss: + specifier: 4.3.3 + version: 4.3.3 + tsx: + specifier: 4.23.15 + version: 4.23.15 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.1.11 + version: 4.1.11(@types/node@22.20.4)(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)) + +packages: + + '@alloc/quick-lru@5.3.0': + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} + engines: {node: '>=10'} + + '@axe-core/playwright@4.13.0': + resolution: {integrity: sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==} + peerDependencies: + playwright-core: '>= 1.0.0' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@better-auth/core@1.7.6': + resolution: {integrity: sha512-yBDDO0J4VCHT1qEZtzj6aMexX4etyLesitoxehcWHzPrzYqrFHwe/hgskye+imlGhQVOysuljMg8M8BA6GrtIg==} + peerDependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/api': ^1.9.0 + better-call: 1.4.0 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.7.6': + resolution: {integrity: sha512-ENlhwC7kkTjquuiel3Efggik0hC4vtD7ppJ8FhsiHhRJA8K6RT4hbhzkfm6keNvjdnAeWwCejm7FmMO4GoSGpA==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.7.6': + resolution: {integrity: sha512-BV+DX2/z/6ozNC9DYSge78wjLerq6Bxd/lSPjzzuq9eEutkQDAJTuiaETzgGYqNV5LlEWi8TZFAQTEq/wavOEQ==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.7.6': + resolution: {integrity: sha512-ng1xv8k2JsEIjnqzJzdoVOQrnoCLRuk4EiQL9yYAThyg6zYzV00LynlhH+O9OMUZ+yGVpGqGZmr5xcTiUSV2uA==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.7.6': + resolution: {integrity: sha512-GvBwZli8i4XINMAj9tlW7eV9E8Dr+JWqavXjUs2o81GZt/Vh8wiX+31KK1jQ4iOuejfjpLoXcoFtlIPgf/pVog==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.7.6': + resolution: {integrity: sha512-31rz2OXIx2Q1L4Jhd5F71dafIWqder/Bo6vJ+eCgO5iZ0gvnv7fUmM95DvbVUoPC69XYpcIM/6PJmQ3M6RydWg==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.7.6': + resolution: {integrity: sha512-xgRwPja2wTJtcu+OQZR2IiNpAmviddnW3n5Do5umXRugJFW3v9KlSmYjYm580mVsPnU4upvZkvSRl9v8DgSDtA==} + peerDependencies: + '@better-auth/core': ^1.7.6 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + + '@better-auth/utils@0.5.0': + resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} + + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + + '@electric-sql/pglite-socket@0.1.3': + resolution: {integrity: sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite-tools@0.3.3': + resolution: {integrity: sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==} + peerDependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite@0.4.3': + resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==} + + '@embedded-postgres/darwin-arm64@18.4.0-beta.17': + resolution: {integrity: sha512-Kg1ZMNFzkVIJs3g4V2UYEc5X005km9rGinxFBH8R1sOU2Rblvz4pt2Uf93Pu8Rk273Ue9HIdrbMPpgUqwjUi0Q==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@embedded-postgres/darwin-x64@18.4.0-beta.17': + resolution: {integrity: sha512-4tShSYWMxUQTSWoQAdLnHISvRj6L9gTch9/31ASJPg6wgyN64LDRwMcOINEWybM7N0ropJ3nTYhFT3UX1bKY5Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@embedded-postgres/linux-arm64@18.4.0-beta.17': + resolution: {integrity: sha512-TIzjtGnDGSD/LbdW0OWCEcbI+YVT0WKSfSr20TCkkP4UR8zeKe+QCZbO0/CM4hS9EWyZ4cDebjRvpRc3iMi6wg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@embedded-postgres/linux-arm@18.4.0-beta.17': + resolution: {integrity: sha512-VuD1nFasN7yG57zpJcp6MBXM0nXqGfb8GBQV+f1FGJ17+VMNYLIFLhFp99UlY4xnWG74FQC4NPYw8eCwSTJ6qg==} + engines: {node: '>=16'} + cpu: [arm] + os: [linux] + + '@embedded-postgres/linux-ia32@18.4.0-beta.17': + resolution: {integrity: sha512-aSRe4kknqRHuedtpo/rDIaqa8r9VrKIgW9p5FkJyIFqUUkIkxRqND1dg8xrDRREcSUXRJKS0x+j/AuxflgYIfg==} + engines: {node: '>=16'} + cpu: [ia32] + os: [linux] + + '@embedded-postgres/linux-ppc64@18.4.0-beta.17': + resolution: {integrity: sha512-zO2RRUFdKRPplrdhmglG39VXJZzDXu3P3ONAG1sFPhGh89vbk24Btd5P7uOmQLIpWehzL0s+UqUWoeE/ZUKvgw==} + engines: {node: '>=16'} + cpu: [ppc64] + os: [linux] + + '@embedded-postgres/linux-x64@18.4.0-beta.17': + resolution: {integrity: sha512-jVw/MdDtIX/vICH/DKIe6/mHpiCggdx6QVyza4vt/NbcZFsL0KhwglF6F1Koqx3gRBZ9XtN+vi63EsqSyqOSxA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@embedded-postgres/windows-x64@18.4.0-beta.17': + resolution: {integrity: sha512-AwRerliA4IGWyW5jWBvHt5vidVUSB0QQW9Tt2y7ScnmifnCb/awfxZr4BkBSTv+gNt8Djcddqs+xSF5Z6/CkTg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} + + '@next/eslint-plugin-next@16.3.6': + resolution: {integrity: sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==} + + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@2.4.0': + resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@prisma/adapter-pg@7.10.0': + resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==} + + '@prisma/client-runtime-utils@7.10.0': + resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==} + + '@prisma/client@7.10.0': + resolution: {integrity: sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.10.0': + resolution: {integrity: sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==} + + '@prisma/debug@7.10.0': + resolution: {integrity: sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/dev@0.24.17': + resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==} + + '@prisma/driver-adapter-utils@7.10.0': + resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==} + + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': + resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==} + + '@prisma/engines@7.10.0': + resolution: {integrity: sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==} + + '@prisma/fetch-engine@7.10.0': + resolution: {integrity: sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==} + + '@prisma/get-platform@7.10.0': + resolution: {integrity: sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.11': + resolution: {integrity: sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==} + engines: {bun: '>=1.2.0', node: '>=22.0.0'} + + '@prisma/studio-core@0.33.0': + resolution: {integrity: sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==} + 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 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + 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 + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rolldown/binding-android-arm-eabi@1.2.11': + resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.11': + resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.11': + resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.11': + resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.11': + resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.11': + resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.11': + resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.11': + resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.11': + resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.11': + resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/d3-array@3.0.3': + resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==} + + '@types/d3-color@3.1.0': + resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==} + + '@types/d3-delaunay@6.0.1': + resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==} + + '@types/d3-format@3.0.1': + resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-interpolate@3.0.1': + resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.2': + resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@2.1.0': + resolution: {integrity: sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==} + + '@types/d3-time@3.0.0': + resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} + + '@types/node@26.6.3': + resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + + '@types/nodemailer@8.0.2': + resolution: {integrity: sha512-c7M5ox8p0nEOfbJ2E9Qcmt8/QCu/VzsiAhzBiZbvky2PhKZDHpKB3sQHBM5MEZNkCfBOaS6S9//AGPRTNB7IaA==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.70.1': + resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.70.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.70.1': + resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.70.1': + resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.70.1': + resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.70.1': + resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.70.1': + resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.70.1': + resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.70.1': + resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.70.1': + resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.70.1': + resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@visx/curve@4.0.1-alpha.0': + resolution: {integrity: sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==} + + '@visx/event@4.0.1-alpha.0': + resolution: {integrity: sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==} + + '@visx/grid@4.0.1-alpha.0': + resolution: {integrity: sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/group@4.0.1-alpha.0': + resolution: {integrity: sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/point@4.0.1-alpha.0': + resolution: {integrity: sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==} + + '@visx/responsive@4.0.1-alpha.0': + resolution: {integrity: sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/scale@4.0.1-alpha.0': + resolution: {integrity: sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==} + + '@visx/shape@4.0.1-alpha.0': + resolution: {integrity: sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/vendor@4.0.0-alpha.0': + resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.26: + resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-auth@1.7.6: + resolution: {integrity: sha512-WTMqOpmTTj+oBoX7zzPOzL85342Upyf+/v0IkH1kvGRA85lV4JGRFIYMIRKqvjZ6ShsuL5VX/c9C13jtoZXcKA==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.4.0: + resolution: {integrity: sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + better-result@2.10.0: + resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.29.1: + resolution: {integrity: sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001812: + resolution: {integrity: sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.1: + resolution: {integrity: sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==} + engines: {node: '>=12'} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.2: + resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.0: + resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dotenv@18.0.4: + resolution: {integrity: sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==} + engines: {node: '>=12'} + hasBin: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + electron-to-chromium@1.5.439: + resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + + elkjs@0.11.1: + resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} + + embedded-postgres@18.4.0-beta.17: + resolution: {integrity: sha512-ORT/A1YiO6ecpRHpyIIBgyCR/8ASqqYZuFw11aX9PkqTX3FUM5+9sNOXY1mGIixxsm2TWSIA5WghJEk7A5ZxVw==} + engines: {node: '>=16'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@16.3.6: + resolution: {integrity: sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + 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 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-my-way@9.7.0: + resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} + engines: {node: '>=20'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + framer-motion@13.4.4: + resolution: {integrity: sha512-lbsZO95NGbulo6apz70zEt6Vxf/anoRwQI2ECEc/EQYWO5CGbITtl7plDW8P/VXgXw6e8wrbhH4a9i+ygke0Mg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + giget@3.3.1: + resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.13: + resolution: {integrity: sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kysely@0.29.6: + resolution: {integrity: sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==} + engines: {node: '>=22.0.0'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru.min@1.1.5: + resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + motion-dom@13.4.4: + resolution: {integrity: sha512-z2qN3RUABSci4G7cr5aHTFhqPNCWJsEMMfRlzxqtANQsSCIbVJmHvMV288m5x7doEQBTYbRTWbKdztmquCn4Sw==} + + motion-utils@13.3.0: + resolution: {integrity: sha512-sgSschQp7EseHInIlR7hBbMuvet3RA0bs28KPZAXJcGKGdxHGvh1ogpYDilY3bOMtl73EqPNmp75sAKHYPU5sg==} + + motion@13.4.4: + resolution: {integrity: sha512-lyX5kpAum2MmigecKWrFdIj3Sjn7pHek2EUrXY0WGZbOjJ1f5NfX0g4KQAb2Bp2EqL0a6dUzse11s1n7OYr5jg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.5.4: + resolution: {integrity: sha512-4UXY7OAJEK0Y6V3SudBU7cXjAzTDc189R78dDOh0Xbm19gpKYto6WizSg58LJf12lTsRxKSLGyE117OiLpM4DQ==} + engines: {node: ^20.0.0 || >=22.0.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} + engines: {node: '>=20.9.0'} + hasBin: true + 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-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} + + nodemailer@10.0.10: + resolution: {integrity: sha512-He9XskOFms62SyAKkLu8CcGcYHAo+BSHSsORI8s4PJH8qZgZY4L4lDfvyN0twvmbpyG+eGrxpyBlskj9d9rJ8A==} + engines: {node: '>=20.0.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prisma@7.10.0: + resolution: {integrity: sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc9@3.1.0: + resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.2.11: + resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stripe@22.6.2: + resolution: {integrity: sha512-PwRE2scocvXqDKPiskMa5vRJEAEkq46W69XyUYmAwrNSLr675lQnfqLwnsndAWizwgihOWw/irLmoFRo3ez7ew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + 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 + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.70.1: + resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vite@8.3.1: + resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + 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 + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@alloc/quick-lru@5.3.0': {} + + '@axe-core/playwright@4.13.0(playwright-core@1.63.0)': + dependencies: + axe-core: 4.13.0 + playwright-core: 1.63.0 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@7.2.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)': + dependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/semantic-conventions': 1.43.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.4.0(zod@4.6.5) + jose: 6.2.12 + kysely: 0.29.6 + nanostores: 1.5.4 + zod: 4.6.5 + + '@better-auth/drizzle-adapter@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + + '@better-auth/kysely-adapter@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6)': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + optionalDependencies: + kysely: 0.29.6 + + '@better-auth/memory-adapter@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + + '@better-auth/prisma-adapter@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + optionalDependencies: + '@prisma/client': 7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3) + prisma: 7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) + + '@better-auth/telemetry@1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)': + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-auth/utils@0.5.0': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-fetch/fetch@1.3.2': {} + + '@electric-sql/pglite-socket@0.1.3(@electric-sql/pglite@0.4.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite-tools@0.3.3(@electric-sql/pglite@0.4.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite@0.4.3': {} + + '@embedded-postgres/darwin-arm64@18.4.0-beta.17': + optional: true + + '@embedded-postgres/darwin-x64@18.4.0-beta.17': + optional: true + + '@embedded-postgres/linux-arm64@18.4.0-beta.17': + optional: true + + '@embedded-postgres/linux-arm@18.4.0-beta.17': + optional: true + + '@embedded-postgres/linux-ia32@18.4.0-beta.17': + optional: true + + '@embedded-postgres/linux-ppc64@18.4.0-beta.17': + optional: true + + '@embedded-postgres/linux-x64@18.4.0-beta.17': + optional: true + + '@embedded-postgres/windows-x64@18.4.0-beta.17': + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2(supports-color@7.2.0)': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.7(supports-color@7.2.0)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.3 + optional: true + + '@img/sharp-darwin-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.3 + optional: true + + '@img/sharp-linux-arm@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.3 + optional: true + + '@img/sharp-linux-ppc64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.3 + optional: true + + '@img/sharp-linux-riscv64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.3 + optional: true + + '@img/sharp-linux-s390x@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.3 + optional: true + + '@img/sharp-linux-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + optional: true + + '@img/sharp-wasm32@0.35.4': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-win32-arm64@0.35.4': + optional: true + + '@img/sharp-win32-ia32@0.35.4': + optional: true + + '@img/sharp-win32-x64@0.35.4': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@next/env@16.3.6': {} + + '@next/eslint-plugin-next@16.3.6(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint + + '@next/swc-darwin-arm64@16.3.6': + optional: true + + '@next/swc-darwin-x64@16.3.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.6': + optional: true + + '@next/swc-linux-arm64-musl@16.3.6': + optional: true + + '@next/swc-linux-x64-gnu@16.3.6': + optional: true + + '@next/swc-linux-x64-musl@16.3.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.6': + optional: true + + '@next/swc-win32-x64-msvc@16.3.6': + optional: true + + '@noble/ciphers@2.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@nolyfill/is-core-module@1.0.39': {} + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@oxc-project/types@0.151.0': {} + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@prisma/adapter-pg@7.10.0': + dependencies: + '@prisma/driver-adapter-utils': 7.10.0 + '@types/pg': 8.23.1 + pg: 8.23.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.10.0': {} + + '@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.10.0 + optionalDependencies: + prisma: 7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.10.0': + dependencies: + c12: 3.3.4 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.10.0': {} + + '@prisma/debug@7.2.0': {} + + '@prisma/dev@0.24.17(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + '@electric-sql/pglite-socket': 0.1.3(@electric-sql/pglite@0.4.3) + '@electric-sql/pglite-tools': 0.3.3(@electric-sql/pglite@0.4.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(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.10.0': + dependencies: + '@prisma/debug': 7.10.0 + + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': {} + + '@prisma/engines@7.10.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 + + '@prisma/fetch-engine@7.10.0': + dependencies: + '@prisma/debug': 7.10.0 + '@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 + '@prisma/get-platform': 7.10.0 + + '@prisma/get-platform@7.10.0': + dependencies: + '@prisma/debug': 7.10.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.11': + dependencies: + ajv: 8.20.0 + better-result: 2.10.0 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.33.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': 19.2.14 + '@visx/curve': 4.0.1-alpha.0 + '@visx/event': 4.0.1-alpha.0 + '@visx/grid': 4.0.1-alpha.0(react@19.3.0) + '@visx/group': 4.0.1-alpha.0(react@19.3.0) + '@visx/responsive': 4.0.1-alpha.0(react@19.3.0) + '@visx/scale': 4.0.1-alpha.0 + '@visx/shape': 4.0.1-alpha.0(react@19.3.0) + d3-array: 3.2.4 + d3-shape: 3.2.0 + elkjs: 0.11.1 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.3.0)': + dependencies: + react: 19.3.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.3.0) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.3.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.3.0) + react: 19.3.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.3.0) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.3.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.3.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.3.0) + react: 19.3.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.3.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.3.0) + react: 19.3.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.3.0)': + dependencies: + react: 19.3.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@rolldown/binding-android-arm-eabi@1.2.11': + optional: true + + '@rolldown/binding-android-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-x64@1.2.11': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.11': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.11': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.11': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.11': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.11': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rtsao/scc@1.1.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.25.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + 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 + + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.3.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.23 + tailwindcss: 4.3.3 + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@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': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-interpolate@3.0.1': + dependencies: + '@types/d3-color': 3.1.0 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.2': + dependencies: + '@types/d3-time': 3.0.0 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@2.1.0': {} + + '@types/d3-time@3.0.0': {} + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/lodash@4.17.25': {} + + '@types/node@22.20.4': + dependencies: + undici-types: 6.21.0 + + '@types/node@26.6.3': + dependencies: + undici-types: 8.9.0 + + '@types/nodemailer@8.0.2': + dependencies: + '@types/node': 26.6.3 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 26.6.3 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + + '@types/pg@8.23.1': + dependencies: + '@types/node': 26.6.3 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 22.20.4 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/type-utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.1 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ignore: 7.0.10 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.70.1(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3) + '@typescript-eslint/types': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.70.1': + dependencies: + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 + + '@typescript-eslint/tsconfig-utils@8.70.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.70.1': {} + + '@typescript-eslint/typescript-estree@8.70.1(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.70.1': + dependencies: + '@typescript-eslint/types': 8.70.1 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@visx/curve@4.0.1-alpha.0': + dependencies: + '@visx/vendor': 4.0.0-alpha.0 + + '@visx/event@4.0.1-alpha.0': + dependencies: + '@types/react': 19.2.14 + '@visx/point': 4.0.1-alpha.0 + + '@visx/grid@4.0.1-alpha.0(react@19.3.0)': + dependencies: + '@types/react': 19.2.14 + '@visx/curve': 4.0.1-alpha.0 + '@visx/group': 4.0.1-alpha.0(react@19.3.0) + '@visx/point': 4.0.1-alpha.0 + '@visx/scale': 4.0.1-alpha.0 + '@visx/shape': 4.0.1-alpha.0(react@19.3.0) + classnames: 2.5.1 + react: 19.3.0 + + '@visx/group@4.0.1-alpha.0(react@19.3.0)': + dependencies: + '@types/react': 19.2.14 + classnames: 2.5.1 + react: 19.3.0 + + '@visx/point@4.0.1-alpha.0': {} + + '@visx/responsive@4.0.1-alpha.0(react@19.3.0)': + dependencies: + '@types/lodash': 4.17.25 + '@types/react': 19.2.14 + lodash: 4.18.1 + react: 19.3.0 + + '@visx/scale@4.0.1-alpha.0': + dependencies: + '@visx/vendor': 4.0.0-alpha.0 + + '@visx/shape@4.0.1-alpha.0(react@19.3.0)': + dependencies: + '@types/lodash': 4.17.25 + '@types/react': 19.2.14 + '@visx/curve': 4.0.1-alpha.0 + '@visx/group': 4.0.1-alpha.0(react@19.3.0) + '@visx/scale': 4.0.1-alpha.0 + '@visx/vendor': 4.0.0-alpha.0 + classnames: 2.5.1 + lodash: 4.18.1 + react: 19.3.0 + + '@visx/vendor@4.0.0-alpha.0': + 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 + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.2.0: + 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 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + 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 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-exit-hook@2.0.1: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + + axe-core@4.13.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.26: {} + + better-auth@1.7.6(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3))(mysql2@3.15.3)(next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@22.20.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vitest@4.1.11(@types/node@22.20.4)(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))): + dependencies: + '@better-auth/core': 1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4) + '@better-auth/drizzle-adapter': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6) + '@better-auth/memory-adapter': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3))(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3)) + '@better-auth/telemetry': 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.0(zod@4.6.5) + defu: 6.1.7 + jose: 6.2.12 + kysely: 0.29.6 + nanostores: 1.5.4 + zod: 4.6.5 + optionalDependencies: + '@prisma/client': 7.10.0(prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3))(typescript@5.9.3) + mysql2: 3.15.3 + next: 16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@22.20.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + pg: 8.23.0 + prisma: 7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + vitest: 4.1.11(@types/node@22.20.4)(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)) + transitivePeerDependencies: + - '@opentelemetry/api' + + better-call@1.4.0(zod@4.6.5): + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + rou3: 0.9.2 + set-cookie-parser: 3.1.2 + optionalDependencies: + zod: 4.6.5 + + better-result@2.10.0: {} + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.29.1: + dependencies: + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001812 + electron-to-chromium: 1.5.439 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.1) + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.1 + giget: 3.3.1 + jiti: 2.7.0 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + rc9: 3.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + 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 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001812: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + classnames@2.5.1: {} + + client-only@0.0.1: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + confbox@0.2.4: {} + + confbox@0.3.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-array@3.2.1: + dependencies: + internmap: 2.0.3 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.2: + dependencies: + delaunator: 5.1.0 + + d3-format@3.1.0: {} + + d3-geo@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + decamelize@1.2.0: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.7: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + denque@2.1.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + dijkstrajs@1.0.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv@17.4.2: {} + + dotenv@18.0.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.439: {} + + elkjs@0.11.1: {} + + embedded-postgres@18.4.0-beta.17: + dependencies: + async-exit-hook: 2.0.1 + pg: 8.23.0 + optionalDependencies: + '@embedded-postgres/darwin-arm64': 18.4.0-beta.17 + '@embedded-postgres/darwin-x64': 18.4.0-beta.17 + '@embedded-postgres/linux-arm': 18.4.0-beta.17 + '@embedded-postgres/linux-arm64': 18.4.0-beta.17 + '@embedded-postgres/linux-ia32': 18.4.0-beta.17 + '@embedded-postgres/linux-ppc64': 18.4.0-beta.17 + '@embedded-postgres/linux-x64': 18.4.0-beta.17 + '@embedded-postgres/windows-x64': 18.4.0-beta.17 + transitivePeerDependencies: + - pg-native + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + enhanced-resolve@5.25.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@3.0.0: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + 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.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + 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.4 + 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.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + 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.11 + string.prototype.trimend: 1.0.10 + 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.5 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.24 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + 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 + + es-module-lexer@2.3.2: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + 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 + + esbuild@0.28.2: + 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 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.3.6(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.3.6(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + globals: 16.4.0 + typescript-eslint: 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.10(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + is-core-module: 2.17.0 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + get-tsconfig: 4.14.3 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + optionalDependencies: + '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@7.2.0) + doctrine: 2.1.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + hasown: 2.0.4 + is-core-module: 2.17.0 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.2.0 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/parser': 7.29.9 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + hermes-parser: 0.25.1 + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + array-includes: 3.2.0 + 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.4.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + 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.7 + semver: 6.3.1 + string.prototype.matchall: 4.1.0 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2(supports-color@7.2.0) + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.7(supports-color@7.2.0) + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + exsolve@1.1.1: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.8: {} + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-my-way@9.7.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + framer-motion@13.4.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + motion-dom: 13.4.4 + motion-utils: 13.3.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + 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 + + functions-have-names@1.2.3: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-port-please@3.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@3.3.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grammex@3.1.13: {} + + graphmatch@1.1.1: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.10: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + internmap@2.0.3: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-property@1.0.2: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.24 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.7.0: {} + + jose@6.2.12: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.2.0 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kysely@0.29.6: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + 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 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.18.1: {} + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru.min@1.1.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + minimist@1.2.8: {} + + motion-dom@13.4.4: + dependencies: + motion-utils: 13.3.0 + + motion-utils@13.3.0: {} + + motion@13.4.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + framer-motion: 13.4.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + tslib: 2.8.1 + optionalDependencies: + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + + ms@2.1.3: {} + + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.5 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.5 + + nanoid@3.3.19: {} + + nanostores@1.5.4: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@22.20.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@next/env': 16.3.6 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001812 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 + '@playwright/test': 1.63.0 + sharp: 0.35.4(@types/node@22.20.4) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.57: {} + + nodemailer@10.0.10: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.2.1: {} + + ohash@2.0.12: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.1 + pathe: 2.0.3 + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + pngjs@5.0.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prisma@7.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.10.0 + '@prisma/dev': 0.24.17(typescript@5.9.3) + '@prisma/engines': 7.10.0 + '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + queue-microtask@1.2.3: {} + + rc9@3.1.0: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@16.13.1: {} + + react@19.3.0: {} + + readdirp@5.1.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + 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 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + 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 + + remeda@2.33.4: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + ret@0.5.0: {} + + retry@0.12.0: {} + + reusify@1.1.0: {} + + robust-predicates@3.0.3: {} + + rolldown@1.2.11: + dependencies: + '@oxc-project/types': 0.151.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.11 + '@rolldown/binding-android-arm64': 1.2.11 + '@rolldown/binding-darwin-arm64': 1.2.11 + '@rolldown/binding-darwin-x64': 1.2.11 + '@rolldown/binding-freebsd-x64': 1.2.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 + '@rolldown/binding-linux-arm64-gnu': 1.2.11 + '@rolldown/binding-linux-arm64-musl': 1.2.11 + '@rolldown/binding-linux-ppc64-gnu': 1.2.11 + '@rolldown/binding-linux-s390x-gnu': 1.2.11 + '@rolldown/binding-linux-x64-gnu': 1.2.11 + '@rolldown/binding-linux-x64-musl': 1.2.11 + '@rolldown/binding-openharmony-arm64': 1.2.11 + '@rolldown/binding-win32-arm64-msvc': 1.2.11 + '@rolldown/binding-win32-x64-msvc': 1.2.11 + + rou3@0.9.2: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + 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 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safer-buffer@2.1.2: {} + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + seq-queue@0.0.5: {} + + server-only@0.0.1: {} + + set-blocking@2.0.0: {} + + set-cookie-parser@3.1.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.35.4(@types/node@22.20.4): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + 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 + '@types/node': 22.20.4 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + 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 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + sqlstring@2.3.3: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + std-env@4.2.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.1.0: + 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 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + 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 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + stripe@22.6.2(@types/node@22.20.4): + optionalDependencies: + '@types/node': 22.20.4 + + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0): + dependencies: + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.5: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + 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 + + typescript-eslint@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + undici-types@8.9.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + 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 + + update-browserslist-db@1.3.3(browserslist@4.29.1): + dependencies: + browserslist: 4.29.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + valibot@1.4.2(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.11 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.4 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.15 + + vitest@4.1.11(@types/node@22.20.4)(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.3.1(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.4 + transitivePeerDependencies: + - msw + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.24 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.24: + 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 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yocto-queue@0.1.0: {} + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.13 + graphmatch: 1.1.1 + + zod-validation-error@4.0.2(zod@4.6.5): + dependencies: + zod: 4.6.5 + + zod@4.6.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..7bda301 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +allowBuilds: + '@prisma/engines': true + esbuild: true + prisma: true + sharp: true + embedded-postgres: true + '@embedded-postgres/windows-x64': true + '@embedded-postgres/linux-x64': true + unrs-resolver: false diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..d9c3965 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,15 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + seed: "tsx scripts/seed-dev.ts", + }, + datasource: { + // Zur Build-Zeit reicht ein Platzhalter (prisma generate verbindet sich + // nicht); migrate/seed brauchen die echte URL aus der Umgebung. + url: process.env.DATABASE_URL ?? "postgresql://placeholder@localhost:5432/placeholder", + }, +}); diff --git a/prisma/migrations/20260926231703_init/migration.sql b/prisma/migrations/20260926231703_init/migration.sql new file mode 100644 index 0000000..2a5d1bb --- /dev/null +++ b/prisma/migrations/20260926231703_init/migration.sql @@ -0,0 +1,528 @@ +-- CreateEnum +CREATE TYPE "PricePhase" AS ENUM ('PREVIEW', 'EARLY_BIRD', 'REGULAR'); + +-- CreateEnum +CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELED', 'EXPIRED', 'NEEDS_REVIEW'); + +-- CreateEnum +CREATE TYPE "PaymentStatus" AS ENUM ('UNPAID', 'PROCESSING', 'PARTIALLY_PAID', 'PAID', 'FAILED', 'PARTIALLY_REFUNDED', 'REFUNDED'); + +-- CreateEnum +CREATE TYPE "PlanType" AS ENUM ('FULL', 'TWO_INSTALLMENTS', 'THREE_INSTALLMENTS'); + +-- CreateEnum +CREATE TYPE "InstallmentStatus" AS ENUM ('PENDING', 'PROCESSING', 'PAID', 'FAILED', 'REQUIRES_ACTION', 'CANCELED'); + +-- CreateEnum +CREATE TYPE "ReservationStatus" AS ENUM ('ACTIVE', 'HELD', 'CONVERTED', 'RELEASED'); + +-- CreateEnum +CREATE TYPE "RedemptionStatus" AS ENUM ('RESERVED', 'COMPLETED', 'RELEASED'); + +-- CreateEnum +CREATE TYPE "StripeEventStatus" AS ENUM ('PROCESSING', 'PROCESSED', 'IGNORED', 'FAILED'); + +-- CreateEnum +CREATE TYPE "EmailStatus" AS ENUM ('PENDING', 'SENT', 'FAILED'); + +-- CreateEnum +CREATE TYPE "ActorType" AS ENUM ('ADMIN', 'CUSTOMER', 'STRIPE', 'WORKER', 'SYSTEM'); + +-- CreateTable +CREATE TABLE "Camp" ( + "id" TEXT NOT NULL, + "salesEnabled" BOOLEAN NOT NULL DEFAULT false, + "capacity" INTEGER NOT NULL DEFAULT 10, + "capacityConfirmed" BOOLEAN NOT NULL DEFAULT false, + "stammmodelQuota" INTEGER NOT NULL DEFAULT 4, + "stammmodelQuotaConfirmed" BOOLEAN NOT NULL DEFAULT false, + "campStartDate" DATE, + "campEndDate" DATE, + "dateConfirmed" BOOLEAN NOT NULL DEFAULT false, + "locationName" TEXT, + "locationRegion" TEXT, + "locationConfirmed" BOOLEAN NOT NULL DEFAULT false, + "accommodationText" TEXT, + "accommodationConfirmed" BOOLEAN NOT NULL DEFAULT false, + "servicesConfirmed" BOOLEAN NOT NULL DEFAULT false, + "organizerName" TEXT, + "organizerAddress" TEXT, + "organizerEmail" TEXT, + "organizerPhone" TEXT, + "organizerConfirmed" BOOLEAN NOT NULL DEFAULT false, + "taxNote" TEXT, + "taxConfirmed" BOOLEAN NOT NULL DEFAULT false, + "termsVersion" TEXT, + "termsUrl" TEXT, + "termsApproved" BOOLEAN NOT NULL DEFAULT false, + "travelLawChecked" BOOLEAN NOT NULL DEFAULT false, + "installmentsApproved" BOOLEAN NOT NULL DEFAULT false, + "installmentRuleApproved" BOOLEAN NOT NULL DEFAULT false, + "reservationMinutes" INTEGER NOT NULL DEFAULT 30, + "finalInstallmentLeadDays" INTEGER NOT NULL DEFAULT 30, + "minInstallmentGapDays" INTEGER NOT NULL DEFAULT 14, + "testPhaseOverride" "PricePhase", + "updatedAt" TIMESTAMP(3) NOT NULL, + "updatedBy" TEXT, + + CONSTRAINT "Camp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Booking" ( + "id" TEXT NOT NULL, + "reference" TEXT NOT NULL, + "campId" TEXT NOT NULL, + "status" "BookingStatus" NOT NULL DEFAULT 'PENDING', + "paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'UNPAID', + "livemode" BOOLEAN NOT NULL, + "idempotencyKey" TEXT NOT NULL, + "statusTokenHash" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "country" TEXT NOT NULL, + "notes" TEXT, + "adultConfirmedAt" TIMESTAMP(3) NOT NULL, + "termsAcceptedAt" TIMESTAMP(3) NOT NULL, + "termsVersion" TEXT NOT NULL, + "accommodationAcknowledgedAt" TIMESTAMP(3) NOT NULL, + "privacyAcknowledgedAt" TIMESTAMP(3) NOT NULL, + "installmentMandateAt" TIMESTAMP(3), + "pricePhase" "PricePhase" NOT NULL, + "basePriceCents" INTEGER NOT NULL, + "phaseDiscountCents" INTEGER NOT NULL, + "voucherDiscountCents" INTEGER NOT NULL, + "totalCents" INTEGER NOT NULL, + "planType" "PlanType" NOT NULL, + "priceSnapshot" JSONB NOT NULL, + "priceLockedUntil" TIMESTAMP(3) NOT NULL, + "voucherId" TEXT, + "stripeCustomerId" TEXT, + "stripeCheckoutSessionId" TEXT, + "stripeCheckoutUrl" TEXT, + "stripePaymentMethodId" TEXT, + "paymentMethodDetachedAt" TIMESTAMP(3), + "paidCents" INTEGER NOT NULL DEFAULT 0, + "refundedCents" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "confirmedAt" TIMESTAMP(3), + "canceledAt" TIMESTAMP(3), + "cancelReason" TEXT, + "reviewReason" TEXT, + + CONSTRAINT "Booking_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Installment" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "seq" INTEGER NOT NULL, + "amountCents" INTEGER NOT NULL, + "dueAt" TIMESTAMP(3), + "status" "InstallmentStatus" NOT NULL DEFAULT 'PENDING', + "attemptCount" INTEGER NOT NULL DEFAULT 0, + "lastAttemptAt" TIMESTAMP(3), + "nextAttemptAt" TIMESTAMP(3), + "failureCode" TEXT, + "paidAt" TIMESTAMP(3), + "refundedCents" INTEGER NOT NULL DEFAULT 0, + "stripePaymentIntentId" TEXT, + "stripeChargeId" TEXT, + "checkoutSessionId" TEXT, + "checkoutExpiresAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Installment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Refund" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "installmentId" TEXT, + "stripeRefundId" TEXT NOT NULL, + "amountCents" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "reason" TEXT, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Refund_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SeatReservation" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "campId" TEXT NOT NULL, + "livemode" BOOLEAN NOT NULL, + "status" "ReservationStatus" NOT NULL DEFAULT 'ACTIVE', + "expiresAt" TIMESTAMP(3) NOT NULL, + "releasedAt" TIMESTAMP(3), + "releaseReason" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SeatReservation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Voucher" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "validFrom" TIMESTAMP(3), + "validUntil" TIMESTAMP(3), + "maxRedemptions" INTEGER NOT NULL DEFAULT 1, + "boundEmail" TEXT, + "internalNote" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Voucher_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VoucherRedemption" ( + "id" TEXT NOT NULL, + "voucherId" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "livemode" BOOLEAN NOT NULL, + "status" "RedemptionStatus" NOT NULL DEFAULT 'RESERVED', + "releasedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "VoucherRedemption_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StripeEvent" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "livemode" BOOLEAN NOT NULL, + "status" "StripeEventStatus" NOT NULL DEFAULT 'PROCESSING', + "error" TEXT, + "attempts" INTEGER NOT NULL DEFAULT 1, + "receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "processedAt" TIMESTAMP(3), + + CONSTRAINT "StripeEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmailMessage" ( + "id" TEXT NOT NULL, + "dedupeKey" TEXT NOT NULL, + "bookingId" TEXT, + "toAddress" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "textBody" TEXT NOT NULL, + "htmlBody" TEXT NOT NULL, + "status" "EmailStatus" NOT NULL DEFAULT 'PENDING', + "attempts" INTEGER NOT NULL DEFAULT 0, + "lastError" TEXT, + "sentAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmailMessage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "actorType" "ActorType" NOT NULL, + "actorId" TEXT, + "action" TEXT NOT NULL, + "entityType" TEXT NOT NULL, + "entityId" TEXT NOT NULL, + "details" JSONB, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RateLimitBucket" ( + "key" TEXT NOT NULL, + "windowStart" TIMESTAMP(3) NOT NULL, + "count" INTEGER NOT NULL, + + CONSTRAINT "RateLimitBucket_pkey" PRIMARY KEY ("key") +); + +-- CreateTable +CREATE TABLE "user" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "emailVerified" BOOLEAN NOT NULL DEFAULT false, + "image" TEXT, + "twoFactorEnabled" BOOLEAN DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "session" ( + "id" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "ipAddress" TEXT, + "userAgent" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "account" ( + "id" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "accessToken" TEXT, + "refreshToken" TEXT, + "idToken" TEXT, + "accessTokenExpiresAt" TIMESTAMP(3), + "refreshTokenExpiresAt" TIMESTAMP(3), + "scope" TEXT, + "password" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "verification" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "value" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "verification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "twoFactor" ( + "id" TEXT NOT NULL, + "secret" TEXT NOT NULL, + "backupCodes" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "twoFactor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rateLimit" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "count" INTEGER NOT NULL, + "lastRequest" BIGINT NOT NULL, + + CONSTRAINT "rateLimit_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Booking_reference_key" ON "Booking"("reference"); + +-- CreateIndex +CREATE UNIQUE INDEX "Booking_idempotencyKey_key" ON "Booking"("idempotencyKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "Booking_statusTokenHash_key" ON "Booking"("statusTokenHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "Booking_stripeCheckoutSessionId_key" ON "Booking"("stripeCheckoutSessionId"); + +-- CreateIndex +CREATE INDEX "Booking_livemode_status_idx" ON "Booking"("livemode", "status"); + +-- CreateIndex +CREATE INDEX "Booking_email_idx" ON "Booking"("email"); + +-- CreateIndex +CREATE INDEX "Booking_createdAt_idx" ON "Booking"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "Installment_stripePaymentIntentId_key" ON "Installment"("stripePaymentIntentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Installment_stripeChargeId_key" ON "Installment"("stripeChargeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Installment_checkoutSessionId_key" ON "Installment"("checkoutSessionId"); + +-- CreateIndex +CREATE INDEX "Installment_status_dueAt_idx" ON "Installment"("status", "dueAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "Installment_bookingId_seq_key" ON "Installment"("bookingId", "seq"); + +-- CreateIndex +CREATE UNIQUE INDEX "Refund_stripeRefundId_key" ON "Refund"("stripeRefundId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SeatReservation_bookingId_key" ON "SeatReservation"("bookingId"); + +-- CreateIndex +CREATE INDEX "SeatReservation_campId_livemode_status_expiresAt_idx" ON "SeatReservation"("campId", "livemode", "status", "expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "Voucher_code_key" ON "Voucher"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "VoucherRedemption_bookingId_key" ON "VoucherRedemption"("bookingId"); + +-- CreateIndex +CREATE INDEX "VoucherRedemption_voucherId_livemode_status_idx" ON "VoucherRedemption"("voucherId", "livemode", "status"); + +-- CreateIndex +CREATE UNIQUE INDEX "EmailMessage_dedupeKey_key" ON "EmailMessage"("dedupeKey"); + +-- CreateIndex +CREATE INDEX "EmailMessage_status_createdAt_idx" ON "EmailMessage"("status", "createdAt"); + +-- CreateIndex +CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId"); + +-- CreateIndex +CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "session_token_key" ON "session"("token"); + +-- CreateIndex +CREATE INDEX "session_userId_idx" ON "session"("userId"); + +-- CreateIndex +CREATE INDEX "account_userId_idx" ON "account"("userId"); + +-- CreateIndex +CREATE INDEX "verification_identifier_idx" ON "verification"("identifier"); + +-- CreateIndex +CREATE INDEX "twoFactor_secret_idx" ON "twoFactor"("secret"); + +-- CreateIndex +CREATE INDEX "twoFactor_userId_idx" ON "twoFactor"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "rateLimit_key_key" ON "rateLimit"("key"); + +-- AddForeignKey +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_campId_fkey" FOREIGN KEY ("campId") REFERENCES "Camp"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_voucherId_fkey" FOREIGN KEY ("voucherId") REFERENCES "Voucher"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Installment" ADD CONSTRAINT "Installment_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Refund" ADD CONSTRAINT "Refund_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Refund" ADD CONSTRAINT "Refund_installmentId_fkey" FOREIGN KEY ("installmentId") REFERENCES "Installment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SeatReservation" ADD CONSTRAINT "SeatReservation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SeatReservation" ADD CONSTRAINT "SeatReservation_campId_fkey" FOREIGN KEY ("campId") REFERENCES "Camp"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VoucherRedemption" ADD CONSTRAINT "VoucherRedemption_voucherId_fkey" FOREIGN KEY ("voucherId") REFERENCES "Voucher"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VoucherRedemption" ADD CONSTRAINT "VoucherRedemption_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmailMessage" ADD CONSTRAINT "EmailMessage_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "twoFactor" ADD CONSTRAINT "twoFactor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- --------------------------------------------------------------------------- +-- Unveraenderlicher Preis-Snapshot und Zahlungsplan (Briefing Abschnitt 9). +-- Der bei Buchung vereinbarte Preis darf auch durch einen Programmfehler oder +-- einen direkten Datenbankzugriff nicht nachtraeglich veraendert werden. +-- Eine Vertragsaenderung heisst: stornieren und neu buchen. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION booking_snapshot_immutable() RETURNS trigger AS $$ +BEGIN + IF NEW."reference" IS DISTINCT FROM OLD."reference" + OR NEW."livemode" IS DISTINCT FROM OLD."livemode" + OR NEW."idempotencyKey" IS DISTINCT FROM OLD."idempotencyKey" + OR NEW."pricePhase" IS DISTINCT FROM OLD."pricePhase" + OR NEW."basePriceCents" IS DISTINCT FROM OLD."basePriceCents" + OR NEW."phaseDiscountCents" IS DISTINCT FROM OLD."phaseDiscountCents" + OR NEW."voucherDiscountCents" IS DISTINCT FROM OLD."voucherDiscountCents" + OR NEW."totalCents" IS DISTINCT FROM OLD."totalCents" + OR NEW."planType" IS DISTINCT FROM OLD."planType" + OR NEW."priceSnapshot" IS DISTINCT FROM OLD."priceSnapshot" + OR NEW."priceLockedUntil" IS DISTINCT FROM OLD."priceLockedUntil" + OR NEW."voucherId" IS DISTINCT FROM OLD."voucherId" + THEN + RAISE EXCEPTION 'Preis-Snapshot der Buchung % ist unveraenderlich', OLD."id"; + END IF; + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +CREATE TRIGGER booking_snapshot_immutable + BEFORE UPDATE ON "Booking" + FOR EACH ROW EXECUTE FUNCTION booking_snapshot_immutable(); + +CREATE OR REPLACE FUNCTION installment_plan_immutable() RETURNS trigger AS $$ +BEGIN + IF NEW."bookingId" IS DISTINCT FROM OLD."bookingId" + OR NEW."seq" IS DISTINCT FROM OLD."seq" + OR NEW."amountCents" IS DISTINCT FROM OLD."amountCents" + OR NEW."dueAt" IS DISTINCT FROM OLD."dueAt" + THEN + RAISE EXCEPTION 'Rate % ist Teil des vereinbarten Zahlungsplans und unveraenderlich', OLD."id"; + END IF; + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +CREATE TRIGGER installment_plan_immutable + BEFORE UPDATE ON "Installment" + FOR EACH ROW EXECUTE FUNCTION installment_plan_immutable(); + +-- Plausibilitaet auf Datenbankebene. +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_amounts_check" + CHECK ("totalCents" > 0 AND "basePriceCents" > 0 AND "phaseDiscountCents" >= 0 + AND "voucherDiscountCents" >= 0 AND "paidCents" >= 0 AND "refundedCents" >= 0 + AND "totalCents" = "basePriceCents" - "phaseDiscountCents" - "voucherDiscountCents"); +ALTER TABLE "Installment" ADD CONSTRAINT "Installment_amount_check" + CHECK ("amountCents" > 0 AND "seq" BETWEEN 1 AND 3 AND "refundedCents" >= 0); +ALTER TABLE "Camp" ADD CONSTRAINT "Camp_limits_check" + CHECK ("capacity" >= 0 AND "stammmodelQuota" >= 0 AND "reservationMinutes" BETWEEN 30 AND 1440 + AND "finalInstallmentLeadDays" >= 0 AND "minInstallmentGapDays" >= 1); +ALTER TABLE "Voucher" ADD CONSTRAINT "Voucher_limits_check" + CHECK ("maxRedemptions" >= 1 AND "code" = upper("code")); diff --git a/prisma/migrations/20260927025745_better_auth_two_factor/migration.sql b/prisma/migrations/20260927025745_better_auth_two_factor/migration.sql new file mode 100644 index 0000000..f50b712 --- /dev/null +++ b/prisma/migrations/20260927025745_better_auth_two_factor/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "twoFactor" ADD COLUMN "failedVerificationCount" INTEGER DEFAULT 0, +ADD COLUMN "lockedUntil" TIMESTAMP(3), +ADD COLUMN "verified" BOOLEAN DEFAULT true; 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..b925965 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,529 @@ +// ARTISTA MODELCAMP 2027 — Datenmodell +// +// Geld: ausschliesslich ganzzahlige Cent-Betraege (Int), Waehrung immer EUR. +// Zeit: Zeitstempel in UTC; fachliche Grenzen (Preisphasen, Faelligkeiten) +// werden in src/lib/time.ts gegen Europe/Berlin berechnet. +// +// Test- und Livebuchungen liegen in denselben Tabellen, sind aber ueber +// "livemode" strikt getrennt: Kapazitaet und Gutscheinkontingente zaehlen nur +// Buchungen desselben Modus, damit Stripe-Testbuchungen nie echte Plaetze +// blockieren. + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +// --------------------------------------------------------------------------- +// Camp und Verkaufsfreigaben +// --------------------------------------------------------------------------- + +enum PricePhase { + PREVIEW + EARLY_BIRD + REGULAR +} + +/// Genau eine Zeile (id = "modelcamp-2027"). Enthaelt die betrieblichen +/// Angaben, die den Live-Verkauf freischalten. Die Preise selbst sind +/// verbindlich vorgegeben und stehen in src/lib/pricing.ts, nicht hier. +model Camp { + id String @id + + /// Hauptschalter fuer den Live-Verkauf. Wirkt nur zusammen mit einer + /// vollstaendigen Freigabe-Checkliste (src/lib/sales-gate.ts). + salesEnabled Boolean @default(false) + + capacity Int @default(10) + capacityConfirmed Boolean @default(false) + stammmodelQuota Int @default(4) + stammmodelQuotaConfirmed Boolean @default(false) + + campStartDate DateTime? @db.Date + campEndDate DateTime? @db.Date + dateConfirmed Boolean @default(false) + + locationName String? + locationRegion String? + locationConfirmed Boolean @default(false) + + /// Verbindliche Zimmer- und Bettbeschreibung, wird vor dem Kauf angezeigt + /// und muss ausdruecklich bestaetigt werden. + accommodationText String? + accommodationConfirmed Boolean @default(false) + + /// Bestaetigt, dass die in src/content/site.ts als "confirmed" markierten + /// Leistungen verbindlich sind. + servicesConfirmed Boolean @default(false) + + organizerName String? + organizerAddress String? + organizerEmail String? + organizerPhone String? + organizerConfirmed Boolean @default(false) + + /// Steuerlich gepruefter Preis-Hinweis, z. B. wie die Umsatzsteuer + /// ausgewiesen wird. Ohne Freigabe erscheint KEINE Steueraussage. + taxNote String? + taxConfirmed Boolean @default(false) + + /// Version der freigegebenen Buchungs-, Storno- und Zahlungsbedingungen. + termsVersion String? + termsUrl String? + termsApproved Boolean @default(false) + + travelLawChecked Boolean @default(false) + + installmentsApproved Boolean @default(false) + installmentRuleApproved Boolean @default(false) + + /// Reservierungsdauer fuer einen begonnenen Checkout. Stripe verlangt fuer + /// expires_at mindestens 30 Minuten. + reservationMinutes Int @default(30) + finalInstallmentLeadDays Int @default(30) + minInstallmentGapDays Int @default(14) + + /// Nur im Stripe-Testmodus wirksam: simuliert eine Preisphase, damit + /// Testbuchungen auch vor dem 01.11.2026 moeglich sind. + testPhaseOverride PricePhase? + + updatedAt DateTime @updatedAt + updatedBy String? + + bookings Booking[] + reservations SeatReservation[] +} + +// --------------------------------------------------------------------------- +// Buchung +// --------------------------------------------------------------------------- + +enum BookingStatus { + /// Platz reserviert, Zahlung der ersten Rate steht aus. + PENDING + CONFIRMED + CANCELED + /// Checkout abgebrochen oder abgelaufen, Platz wieder frei. + EXPIRED + /// Zahlung eingegangen, aber ein Konflikt (z. B. Platz inzwischen + /// vergeben) verlangt eine manuelle Entscheidung. + NEEDS_REVIEW +} + +enum PaymentStatus { + UNPAID + /// Verzoegerte Zahlungsart (z. B. SEPA) ist unterwegs. + PROCESSING + PARTIALLY_PAID + PAID + FAILED + PARTIALLY_REFUNDED + REFUNDED +} + +enum PlanType { + FULL + TWO_INSTALLMENTS + THREE_INSTALLMENTS +} + +model Booking { + id String @id @default(cuid()) + /// Oeffentliche Buchungsnummer, z. B. AMC27-7K3Q9P. + reference String @unique + + campId String + camp Camp @relation(fields: [campId], references: [id]) + + status BookingStatus @default(PENDING) + paymentStatus PaymentStatus @default(UNPAID) + livemode Boolean + + /// Vom Browser erzeugter Schluessel je Buchungsversuch: ein doppelt + /// abgeschicktes Formular fuehrt zur selben Buchung statt zu zweien. + idempotencyKey String @unique + /// SHA-256 des Zugangstokens fuer die Statusseite. Das Token selbst wird + /// nie gespeichert. + statusTokenHash String @unique + + firstName String + lastName String + email String + phone String + country String + notes String? + + adultConfirmedAt DateTime + termsAcceptedAt DateTime + termsVersion String + accommodationAcknowledgedAt DateTime + privacyAcknowledgedAt DateTime + /// Zustimmung zum Einzug spaeterer Raten (nur bei Ratenzahlung). + installmentMandateAt DateTime? + + // --- Preis-Snapshot (per Datenbank-Trigger unveraenderlich) ------------- + pricePhase PricePhase + basePriceCents Int + phaseDiscountCents Int + voucherDiscountCents Int + totalCents Int + planType PlanType + priceSnapshot Json + /// Bis hierhin gilt der gebundene Preis (= Ablauf der Stripe-Session). + priceLockedUntil DateTime + + voucherId String? + voucher Voucher? @relation(fields: [voucherId], references: [id]) + + stripeCustomerId String? + stripeCheckoutSessionId String? @unique + stripeCheckoutUrl String? + /// Referenz (keine Kartendaten) auf die fuer Folgeraten gespeicherte + /// Zahlungsmethode. Wird nach der letzten Rate bei Stripe getrennt. + stripePaymentMethodId String? + paymentMethodDetachedAt DateTime? + + paidCents Int @default(0) + refundedCents Int @default(0) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + confirmedAt DateTime? + canceledAt DateTime? + cancelReason String? + reviewReason String? + + installments Installment[] + reservation SeatReservation? + redemption VoucherRedemption? + refunds Refund[] + emails EmailMessage[] + + @@index([livemode, status]) + @@index([email]) + @@index([createdAt]) +} + +enum InstallmentStatus { + /// Noch nicht faellig bzw. erste Rate vor dem Checkout. + PENDING + /// Einzug laeuft (Worker hat die Rate beansprucht). + PROCESSING + PAID + FAILED + /// Bank verlangt zusaetzliche Authentifizierung: Kundin zahlt ueber einen + /// Link auf der Statusseite selbst. + REQUIRES_ACTION + CANCELED +} + +model Installment { + id String @id @default(cuid()) + bookingId String + booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict) + + seq Int + amountCents Int + /// null = faellig bei Buchung (erste Rate). + dueAt DateTime? + + status InstallmentStatus @default(PENDING) + attemptCount Int @default(0) + lastAttemptAt DateTime? + nextAttemptAt DateTime? + failureCode String? + paidAt DateTime? + refundedCents Int @default(0) + + stripePaymentIntentId String? @unique + stripeChargeId String? @unique + /// Offene Checkout-Session, ueber die die Kundin diese Rate selbst zahlt. + checkoutSessionId String? @unique + checkoutExpiresAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + refunds Refund[] + + @@unique([bookingId, seq]) + @@index([status, dueAt]) +} + +model Refund { + id String @id @default(cuid()) + bookingId String + booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict) + installmentId String? + installment Installment? @relation(fields: [installmentId], references: [id]) + + stripeRefundId String @unique + amountCents Int + status String + reason String? + createdBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// --------------------------------------------------------------------------- +// Kapazitaet +// --------------------------------------------------------------------------- + +enum ReservationStatus { + /// Zeitlich begrenzt, verfaellt mit expiresAt. + ACTIVE + /// Verzoegerte Zahlung unterwegs: Platz bleibt bis zum Ergebnis gehalten. + HELD + CONVERTED + RELEASED +} + +model SeatReservation { + id String @id @default(cuid()) + bookingId String @unique + booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict) + campId String + camp Camp @relation(fields: [campId], references: [id]) + livemode Boolean + + status ReservationStatus @default(ACTIVE) + expiresAt DateTime + releasedAt DateTime? + releaseReason String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([campId, livemode, status, expiresAt]) +} + +// --------------------------------------------------------------------------- +// Gutscheine (Stammmodel-Codes) +// --------------------------------------------------------------------------- + +model Voucher { + id String @id @default(cuid()) + /// Immer in Grossbuchstaben normalisiert gespeichert. + code String @unique + + active Boolean @default(true) + validFrom DateTime? + validUntil DateTime? + maxRedemptions Int @default(1) + /// Optional: nur fuer diese E-Mail-Adresse (klein geschrieben) einloesbar. + boundEmail String? + /// Interne Zuordnung, z. B. Name des Stammmodels. Nie oeffentlich. + internalNote String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + redemptions VoucherRedemption[] + bookings Booking[] +} + +enum RedemptionStatus { + RESERVED + COMPLETED + RELEASED +} + +model VoucherRedemption { + id String @id @default(cuid()) + voucherId String + voucher Voucher @relation(fields: [voucherId], references: [id], onDelete: Restrict) + bookingId String @unique + booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict) + livemode Boolean + + status RedemptionStatus @default(RESERVED) + releasedAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([voucherId, livemode, status]) +} + +// --------------------------------------------------------------------------- +// Stripe-Ereignisse, E-Mail-Ausgang, Protokoll, Rate Limits +// --------------------------------------------------------------------------- + +enum StripeEventStatus { + PROCESSING + PROCESSED + IGNORED + FAILED +} + +model StripeEvent { + /// Stripe-Event-ID (evt_...) — Primaerschluessel macht die Verarbeitung + /// idempotent. + id String @id + type String + livemode Boolean + status StripeEventStatus @default(PROCESSING) + error String? + attempts Int @default(1) + receivedAt DateTime @default(now()) + processedAt DateTime? +} + +enum EmailStatus { + PENDING + SENT + FAILED +} + +/// Transaktionaler Postausgang: E-Mails entstehen in derselben Transaktion +/// wie die Statusaenderung und werden danach (bzw. vom Worker) versendet. +model EmailMessage { + id String @id @default(cuid()) + /// Verhindert doppelte Mails bei wiederholten Webhooks. + dedupeKey String @unique + bookingId String? + booking Booking? @relation(fields: [bookingId], references: [id]) + + toAddress String + subject String + textBody String + htmlBody String + + status EmailStatus @default(PENDING) + attempts Int @default(0) + lastError String? + sentAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, createdAt]) +} + +enum ActorType { + ADMIN + CUSTOMER + STRIPE + WORKER + SYSTEM +} + +/// Nachvollziehbarkeit fuer Bestands-, Buchungs- und Zahlungsaktionen. +model AuditLog { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + actorType ActorType + actorId String? + action String + entityType String + entityId String + details Json? + + @@index([entityType, entityId]) + @@index([createdAt]) +} + +/// Gemeinsamer Zaehler fuer Rate Limits — funktioniert ueber mehrere +/// Replikate hinweg. Der Schluessel enthaelt nur einen Hash der IP. +model RateLimitBucket { + key String @id + windowStart DateTime + count Int +} + +// --------------------------------------------------------------------------- +// Better Auth (Adminbereich) +// --------------------------------------------------------------------------- + +model User { + id String @id + name String + email String @unique + emailVerified Boolean @default(false) + image String? + twoFactorEnabled Boolean? @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sessions Session[] + accounts Account[] + twofactors TwoFactor[] + + @@map("user") +} + +model Session { + id String @id + expiresAt DateTime + token String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map("session") +} + +model Account { + id String @id + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("account") +} + +model Verification { + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([identifier]) + @@map("verification") +} + +model TwoFactor { + id String @id + secret String + backupCodes String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + verified Boolean? @default(true) + failedVerificationCount Int? @default(0) + lockedUntil DateTime? + + @@index([secret]) + @@index([userId]) + @@map("twoFactor") +} + +/// Rate Limits der Anmeldung (Better Auth, Modellname vom Adapter vorgegeben). +model RateLimit { + id String @id + key String @unique + count Int + lastRequest BigInt + + @@map("rateLimit") +} diff --git a/scripts/create-admin.ts b/scripts/create-admin.ts new file mode 100644 index 0000000..4dc7cde --- /dev/null +++ b/scripts/create-admin.ts @@ -0,0 +1,54 @@ +/** + * Legt ein Konto fuer den Adminbereich an. Es gibt keine Selbstregistrierung + * und kein Standardpasswort. + * + * pnpm admin:create --email admin@example.org --name "Vorname Nachname" + * + * Passwort: aus ADMIN_PASSWORD (mind. 12 Zeichen) oder zufaellig erzeugt und + * EINMAL ausgegeben. Nach der ersten Anmeldung erzwingt der Adminbereich die + * Einrichtung der Zwei-Faktor-Anmeldung. + */ +import { randomBytes } from "node:crypto"; +import { parseArgs } from "node:util"; +import { createProvisioningAuth } from "@/lib/auth"; +import { getDb } from "@/lib/db"; + +try { + process.loadEnvFile(); +} catch { + // Umgebung kommt aus dem Container. +} + +const { values } = parseArgs({ options: { email: { type: "string" }, name: { type: "string" } } }); +const email = values.email?.trim().toLowerCase(); +const name = values.name?.trim(); +if (!email || !/^\S+@\S+\.\S+$/.test(email) || !name) { + console.error('Aufruf: pnpm admin:create --email --name ""'); + process.exit(1); +} + +const provided = process.env.ADMIN_PASSWORD; +if (provided !== undefined && provided.length < 12) { + console.error("ADMIN_PASSWORD muss mindestens 12 Zeichen haben."); + process.exit(1); +} +const password = provided ?? randomBytes(18).toString("base64url"); + +const db = getDb(); +try { + if (await db.user.findUnique({ where: { email } })) { + console.error("Für diese E-Mail-Adresse existiert bereits ein Konto."); + process.exitCode = 1; + } else { + // Offizieller Weg ueber Better Auth: Passwortregeln und Leak-Pruefung + // greifen dabei automatisch. + const result = await createProvisioningAuth().api.signUpEmail({ body: { email, name, password } }); + await db.user.update({ where: { id: result.user.id }, data: { emailVerified: true } }); + await db.auditLog.create({ data: { actorType: "SYSTEM", action: "admin.created", entityType: "User", entityId: result.user.id } }); + console.log(`Konto angelegt: ${email}`); + if (!provided) console.log(`Einmaliges Passwort (jetzt sicher ablegen, wird nicht erneut angezeigt): ${password}`); + console.log("Bei der ersten Anmeldung wird die Zwei-Faktor-Anmeldung eingerichtet."); + } +} finally { + await db.$disconnect(); +} diff --git a/scripts/dev-db.mjs b/scripts/dev-db.mjs new file mode 100644 index 0000000..c4fb336 --- /dev/null +++ b/scripts/dev-db.mjs @@ -0,0 +1,49 @@ +// Lokale PostgreSQL-Instanz fuer die Entwicklung ohne Docker. +// +// pnpm db:dev startet PostgreSQL auf Port 54329 (Strg+C beendet) +// +// Mit Docker stattdessen: docker compose up -d db (siehe docker-compose.yml). +// +// Das Datenverzeichnis liegt bewusst NICHT im Projektordner: dieser liegt +// hier in einem OneDrive-synchronisierten Verzeichnis, und ein +// PostgreSQL-Datenverzeichnis in einer Cloud-Synchronisierung fuehrt zu +// gesperrten oder beschaedigten Dateien. +import EmbeddedPostgres from "embedded-postgres"; +import { existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const baseDir = process.env.LOCALAPPDATA ?? path.join(os.homedir(), ".local", "share"); +const databaseDir = path.join(baseDir, "artista-modelcamp", "dev-db"); +const port = Number(process.env.DEV_DB_PORT ?? 54329); + +const pg = new EmbeddedPostgres({ + databaseDir, + user: "artista", + password: "artista-dev", + port, + persistent: true, + onLog: () => {}, +}); + +if (!existsSync(path.join(databaseDir, "PG_VERSION"))) { + await pg.initialise(); +} +await pg.start(); +try { + await pg.createDatabase("artista"); +} catch { + // existiert bereits +} + +console.log(`PostgreSQL laeuft: postgresql://artista:artista-dev@localhost:${port}/artista`); +console.log(`Daten: ${databaseDir}`); + +const shutdown = async () => { + await pg.stop(); + process.exit(0); +}; +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); +// Prozess am Leben halten. +setInterval(() => {}, 1 << 30); diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts new file mode 100644 index 0000000..d1db946 --- /dev/null +++ b/scripts/seed-dev.ts @@ -0,0 +1,40 @@ +/** + * Testdaten NUR fuer Entwicklung und Staging, damit eine Stripe-Testbuchung + * sofort moeglich ist: + * - Preisphase "Early Bird" simuliert (wirkt nur mit Stripe-Testschluessel) + * - ein TEST-Anreisetag, ausdruecklich NICHT bestaetigt (erscheint deshalb + * nicht oeffentlich, erlaubt aber Ratenplaene im Testmodus) + * - ein Test-Gutschein TEST-STAMMMODEL (nicht der Beispielcode aus dem Briefing) + * + * Verweigert den Lauf mit APP_ENV=production. + */ +import { getDb, CAMP_ID } from "@/lib/db"; +import { isoToDbDate } from "@/lib/time"; + +try { + process.loadEnvFile(); +} catch { + // Umgebung kommt aus dem Container. +} + +if ((process.env.APP_ENV ?? "development") === "production") { + console.error("seed-dev laeuft nicht mit APP_ENV=production."); + process.exit(1); +} + +const db = getDb(); +try { + await db.camp.upsert({ + where: { id: CAMP_ID }, + create: { id: CAMP_ID, testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false }, + update: { testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false }, + }); + await db.voucher.upsert({ + where: { code: "TEST-STAMMMODEL" }, + create: { code: "TEST-STAMMMODEL", maxRedemptions: 3, internalNote: "Entwicklung: Testgutschein" }, + update: {}, + }); + console.log("Entwicklungsdaten gesetzt: Early Bird simuliert, Test-Anreisetag 05.06.2027 (unbestätigt), Gutschein TEST-STAMMMODEL."); +} finally { + await db.$disconnect(); +} diff --git a/scripts/worker.ts b/scripts/worker.ts new file mode 100644 index 0000000..a5e0b7b --- /dev/null +++ b/scripts/worker.ts @@ -0,0 +1,34 @@ +/** + * Hintergrundlauf fuer Reservierungen, Folgeraten, Zahlungsmethoden und + * E-Mails. Im Cluster als Kubernetes-CronJob (k8s/base/cronjob-worker.yaml), + * lokal per `pnpm worker`. + * + * Endet mit Exit-Code 1, wenn etwas fehlschlaegt — der CronJob-Lauf wird + * dann als fehlgeschlagen sichtbar statt still im Log zu verschwinden. + */ +import { getDb } from "@/lib/db"; +import { env, requireAppSecret } from "@/lib/env"; +import { getGateway } from "@/lib/payment-gateway"; +import { runWorker } from "@/lib/worker-jobs"; + +try { + process.loadEnvFile(); +} catch { + // Im Container kommen die Variablen aus der Umgebung. +} + +const db = getDb(); +try { + const result = await runWorker(db, getGateway(), { + now: new Date(), + secret: requireAppSecret(), + adminEmail: env().ADMIN_NOTIFY_EMAIL ?? null, + }); + console.log(`[worker] ${new Date().toISOString()} ${JSON.stringify(result)}`); + process.exitCode = result.emails.failed > 0 ? 1 : 0; +} catch (error) { + console.error(`[worker] Abbruch: ${error instanceof Error ? error.message : "unbekannter Fehler"}`); + process.exitCode = 1; +} finally { + await db.$disconnect(); +} diff --git a/src/app/admin/(bereich)/SignOutButton.tsx b/src/app/admin/(bereich)/SignOutButton.tsx new file mode 100644 index 0000000..2690d27 --- /dev/null +++ b/src/app/admin/(bereich)/SignOutButton.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth-client"; + +export function SignOutButton() { + const router = useRouter(); + return ( + + ); +} diff --git a/src/app/admin/(bereich)/buchungen/[id]/page.tsx b/src/app/admin/(bereich)/buchungen/[id]/page.tsx new file mode 100644 index 0000000..36b03ca --- /dev/null +++ b/src/app/admin/(bereich)/buchungen/[id]/page.tsx @@ -0,0 +1,206 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; +import { confirmReviewAction, cancelBookingAction, refundAction, resendConfirmationAction } from "@/app/admin/actions"; +import { Flash, KeyValue, PageTitle, Panel } from "@/components/admin/AdminUi"; +import { getDb } from "@/lib/db"; +import { BOOKING_STATUS_LABEL, INSTALLMENT_STATUS_LABEL, PAYMENT_STATUS_LABEL, REDEMPTION_STATUS_LABEL, RESERVATION_STATUS_LABEL } from "@/lib/labels"; +import { formatEuro } from "@/lib/money"; +import { PLAN_LABEL } from "@/lib/payment-plan"; +import { PHASE_LABEL } from "@/lib/pricing"; +import { formatDate, formatDateTime } from "@/lib/time"; + +export const metadata = { title: "Buchung" }; + +export default async function BookingDetailPage({ + params, + searchParams, +}: { + params: Promise<{ id: string }>; + searchParams: Promise<{ ok?: string; fehler?: string }>; +}) { + await connection(); + const { id } = await params; + const flash = await searchParams; + const db = getDb(); + const booking = await db.booking.findUnique({ + where: { id }, + include: { + installments: { orderBy: { seq: "asc" }, include: { refunds: true } }, + reservation: true, + redemption: true, + voucher: true, + emails: { orderBy: { createdAt: "desc" }, take: 20 }, + }, + }); + if (!booking) notFound(); + const log = await db.auditLog.findMany({ + where: { OR: [{ entityType: "Booking", entityId: id }, { entityType: "Installment", entityId: { in: booking.installments.map((i) => i.id) } }] }, + orderBy: { createdAt: "desc" }, + take: 100, + }); + + return ( + <> + Alle Buchungen + {booking.reference} + + {booking.reviewReason && booking.status === "NEEDS_REVIEW" && ( +

+ Prüfung nötig: {booking.reviewReason} +

+ )} + +
+ + + + + + + + + +
+ +
+

Raten und Erstattungen

+
+ + + + + + + + + + + + + + {booking.installments.map((i) => ( + + + + + + + + + + ))} + +
RateFälligBetragStatusVersucheStripeErstattung
{i.seq}{i.dueAt ? formatDate(i.dueAt) : "bei Buchung"}{formatEuro(i.amountCents)} + {INSTALLMENT_STATUS_LABEL[i.status]} + {i.failureCode && {i.failureCode}} + {i.nextAttemptAt && nächster Versuch {formatDateTime(i.nextAttemptAt)}} + {i.attemptCount}{i.stripePaymentIntentId ?? "–"} + {i.refunds.map((r) => ( + {formatEuro(r.amountCents)} ({r.status}) + ))} + {i.status === "PAID" && i.stripePaymentIntentId && ( +
+ + + + + + + + +
+ )} +
+
+
+ +
+ {booking.status === "NEEDS_REVIEW" && ( + +
+ +

Bestätigt die Buchung, wenn ein Platz frei ist. Andernfalls stornieren und erstatten.

+ +
+
+ )} + {booking.status !== "CANCELED" && booking.status !== "EXPIRED" && ( + +
+ + + +

+ Beendet alle offenen Raten, gibt Platz und Gutschein frei und trennt die gespeicherte Zahlungsmethode. Erstattungen werden danach gemäß den + freigegebenen Stornobedingungen oben je Rate ausgelöst. +

+ +
+
+ )} + {booking.status === "CONFIRMED" && ( + +
+ + +
+
+ )} +
+ +
+ +
    + {log.map((l) => ( +
  • + {formatDateTime(l.createdAt)} {l.action} ({l.actorType}) +
  • + ))} +
+
+ +
    + {booking.emails.map((m) => ( +
  • + {formatDateTime(m.createdAt)} {m.subject} – {m.status === "SENT" ? "gesendet" : m.status === "FAILED" ? "fehlgeschlagen" : "wartet"} +
  • + ))} +
+
+
+ + ); +} diff --git a/src/app/admin/(bereich)/buchungen/page.tsx b/src/app/admin/(bereich)/buchungen/page.tsx new file mode 100644 index 0000000..ac7d2f0 --- /dev/null +++ b/src/app/admin/(bereich)/buchungen/page.tsx @@ -0,0 +1,89 @@ +import Link from "next/link"; +import { connection } from "next/server"; +import { PageTitle } from "@/components/admin/AdminUi"; +import { BOOKING_STATUSES, bookingWhere, filterQuery, parseBookingFilter, PAYMENT_STATUSES } from "@/lib/admin-queries"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { BOOKING_STATUS_LABEL, PAYMENT_STATUS_LABEL } from "@/lib/labels"; +import { formatEuro } from "@/lib/money"; +import { PLAN_LABEL } from "@/lib/payment-plan"; +import { formatDateTime } from "@/lib/time"; + +export const metadata = { title: "Buchungen" }; + +export default async function BookingsPage({ searchParams }: { searchParams: Promise> }) { + await connection(); + const filter = parseBookingFilter(await searchParams, stripeModeOrNull() === "live" ? "live" : "test"); + const bookings = await getDb().booking.findMany({ + where: bookingWhere(filter), + orderBy: { createdAt: "desc" }, + take: 500, + include: { voucher: { select: { code: true } } }, + }); + + return ( + <> + CSV exportieren}>Buchungen +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + {bookings.map((b) => ( + + + + + + + + + + + ))} + +
NummerNameBuchungZahlungPlanGesamtOffenAngelegt
{b.reference}{b.firstName} {b.lastName}{b.voucher && Gutschein}{BOOKING_STATUS_LABEL[b.status]}{PAYMENT_STATUS_LABEL[b.paymentStatus]}{PLAN_LABEL[b.planType]}{formatEuro(b.totalCents)}{formatEuro(Math.max(0, b.totalCents - b.paidCents))}{formatDateTime(b.createdAt)}
+ {bookings.length === 0 &&

Keine Buchungen für diesen Filter.

} +
+ + ); +} diff --git a/src/app/admin/(bereich)/einstellungen/page.tsx b/src/app/admin/(bereich)/einstellungen/page.tsx new file mode 100644 index 0000000..a7617f0 --- /dev/null +++ b/src/app/admin/(bereich)/einstellungen/page.tsx @@ -0,0 +1,131 @@ +import { connection } from "next/server"; +import { updateCampAction } from "@/app/admin/actions"; +import { Checkbox, Field, Flash, PageTitle, Panel } from "@/components/admin/AdminUi"; +import { getCamp } from "@/lib/camp"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { dbDateToIso } from "@/lib/time"; + +export const metadata = { title: "Einstellungen und Freigaben" }; + +export default async function SettingsPage({ searchParams }: { searchParams: Promise<{ ok?: string; fehler?: string }> }) { + await connection(); + const flash = await searchParams; + const camp = await getCamp(getDb()); + const mode = stripeModeOrNull(); + + return ( + <> + Einstellungen und Freigaben + +

+ Der Live-Verkauf öffnet erst, wenn „Live-Verkauf einschalten“ gesetzt ist UND alle Bestätigungen vorliegen. Setze eine Bestätigung nur, wenn die + Angabe verbindlich geprüft ist. Jede Änderung wird protokolliert. +

+
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + + + +
+ + +
+ + +
+
+ +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + + + {mode === "live" &&

Im Livemodus wirkungslos.

} +
+ +
+ + + + +
+
+ + ); +} diff --git a/src/app/admin/(bereich)/gutscheine/[id]/page.tsx b/src/app/admin/(bereich)/gutscheine/[id]/page.tsx new file mode 100644 index 0000000..22076ae --- /dev/null +++ b/src/app/admin/(bereich)/gutscheine/[id]/page.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; +import { Flash, PageTitle } from "@/components/admin/AdminUi"; +import { VoucherForm } from "@/components/admin/VoucherForm"; +import { getDb } from "@/lib/db"; +import { BOOKING_STATUS_LABEL, REDEMPTION_STATUS_LABEL } from "@/lib/labels"; + +export const metadata = { title: "Gutschein" }; + +export default async function VoucherPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ ok?: string; fehler?: string }> }) { + await connection(); + const { id } = await params; + const flash = await searchParams; + const voucher = await getDb().voucher.findUnique({ + where: { id }, + include: { redemptions: { orderBy: { createdAt: "desc" }, include: { booking: { select: { id: true, reference: true, status: true, livemode: true } } } } }, + }); + if (!voucher) notFound(); + return ( + <> + Alle Gutscheine + {voucher.code} + +
+ +
+

Einlösungen

+
    + {voucher.redemptions.map((r) => ( +
  • + {r.booking.reference} – {REDEMPTION_STATUS_LABEL[r.status]}, Buchung{" "} + {BOOKING_STATUS_LABEL[r.booking.status]} {r.booking.livemode ? "" : "(Test)"} +
  • + ))} + {voucher.redemptions.length === 0 &&
  • Noch keine Einlösungen.
  • } +
+
+
+ + ); +} diff --git a/src/app/admin/(bereich)/gutscheine/page.tsx b/src/app/admin/(bereich)/gutscheine/page.tsx new file mode 100644 index 0000000..c585560 --- /dev/null +++ b/src/app/admin/(bereich)/gutscheine/page.tsx @@ -0,0 +1,74 @@ +import Link from "next/link"; +import { connection } from "next/server"; +import { Flash, PageTitle } from "@/components/admin/AdminUi"; +import { VoucherForm } from "@/components/admin/VoucherForm"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { formatDate } from "@/lib/time"; + +export const metadata = { title: "Gutscheine" }; + +export default async function VouchersPage({ searchParams }: { searchParams: Promise<{ ok?: string; fehler?: string }> }) { + await connection(); + const flash = await searchParams; + const livemode = stripeModeOrNull() === "live"; + const now = new Date(); + const vouchers = await getDb().voucher.findMany({ + orderBy: { createdAt: "desc" }, + include: { redemptions: { where: { livemode }, include: { booking: { select: { reservation: { select: { status: true, expiresAt: true } } } } } } }, + }); + + return ( + <> + Gutscheine (Stammmodel-Codes) + +

+ Codes werden persönlich vergeben und nirgends öffentlich gelistet. Ein Code gibt 10 % auf den jeweils gültigen Preis und ist mit Early Bird + kombinierbar, aber nicht mit einem weiteren Gutschein. Das Gesamtkontingent steht unter Einstellungen. +

+
+ + + + + + + + + + + + + {vouchers.map((v) => { + const completed = v.redemptions.filter((r) => r.status === "COMPLETED").length; + const reserved = v.redemptions.filter( + (r) => + r.status === "RESERVED" && + (r.booking.reservation?.status === "HELD" || + r.booking.reservation?.status === "CONVERTED" || + (r.booking.reservation?.status === "ACTIVE" && r.booking.reservation.expiresAt > now)), + ).length; + return ( + + + + + + + + + ); + })} + +
CodeAktivGültigEinlösungenGebunden anInterne Zuordnung
{v.code}{v.active ? "ja" : "nein"} + {v.validFrom ? formatDate(v.validFrom) : "sofort"} – {v.validUntil ? formatDate(v.validUntil) : "unbegrenzt"} + {completed} abgeschlossen, {reserved} reserviert, max. {v.maxRedemptions}{v.boundEmail ?? "–"}{v.internalNote ?? "–"}
+ {vouchers.length === 0 &&

Noch keine Codes angelegt.

} +
+
+

Neuen Code anlegen

+ +
+ + ); +} diff --git a/src/app/admin/(bereich)/layout.tsx b/src/app/admin/(bereich)/layout.tsx new file mode 100644 index 0000000..f4752ed --- /dev/null +++ b/src/app/admin/(bereich)/layout.tsx @@ -0,0 +1,44 @@ +import Link from "next/link"; +import { requireAdminPage } from "@/lib/admin-auth"; +import { stripeModeOrNull } from "@/lib/env"; +import { SignOutButton } from "./SignOutButton"; + +const NAV = [ + { href: "/admin", label: "Übersicht" }, + { href: "/admin/buchungen", label: "Buchungen" }, + { href: "/admin/gutscheine", label: "Gutscheine" }, + { href: "/admin/einstellungen", label: "Einstellungen und Freigaben" }, +]; + +export default async function ProtectedAdminLayout({ children }: { children: React.ReactNode }) { + const admin = await requireAdminPage(); + const mode = stripeModeOrNull(); + return ( + <> +
+
+
+ ARTISTA + + {mode === "live" ? "Stripe: Livemodus" : mode === "test" ? "Stripe: Testmodus" : "Stripe: nicht konfiguriert"} + +
+ +
+ {admin.email} + +
+
+
+
{children}
+ + ); +} diff --git a/src/app/admin/(bereich)/page.tsx b/src/app/admin/(bereich)/page.tsx new file mode 100644 index 0000000..feeb557 --- /dev/null +++ b/src/app/admin/(bereich)/page.tsx @@ -0,0 +1,138 @@ +import Link from "next/link"; +import { connection } from "next/server"; +import { Panel, PageTitle, KeyValue } from "@/components/admin/AdminUi"; +import { countOccupiedSeats } from "@/lib/booking-service"; +import { getCamp, salesStateFor } from "@/lib/camp"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { formatEuro } from "@/lib/money"; +import { PHASE_LABEL } from "@/lib/pricing"; +import { BLOCKER_TEXT } from "@/lib/sales-gate"; +import { formatDate } from "@/lib/time"; +import { countUsedRedemptions } from "@/lib/vouchers"; + +export const metadata = { title: "Übersicht" }; + +export default async function AdminDashboard() { + await connection(); + const db = getDb(); + const now = new Date(); + const camp = await getCamp(db); + const mode = stripeModeOrNull(); + const livemode = mode === "live"; + const state = mode ? salesStateFor(camp, mode, now) : null; + + const [occupied, quotaUsed, byStatus, money, reviews, failedEvents, failedMails, upcoming] = await Promise.all([ + countOccupiedSeats(db, livemode, now), + countUsedRedemptions(db, { livemode, now }), + db.booking.groupBy({ by: ["status"], where: { livemode }, _count: true }), + db.booking.aggregate({ where: { livemode, status: { in: ["CONFIRMED", "NEEDS_REVIEW"] } }, _sum: { totalCents: true, paidCents: true, refundedCents: true } }), + db.booking.findMany({ where: { livemode, status: "NEEDS_REVIEW" }, select: { id: true, reference: true, reviewReason: true }, take: 20 }), + db.stripeEvent.count({ where: { status: "FAILED" } }), + db.emailMessage.count({ where: { status: "FAILED" } }), + db.installment.findMany({ + where: { booking: { livemode, status: "CONFIRMED" }, status: { in: ["PENDING", "FAILED", "REQUIRES_ACTION"] }, seq: { gt: 1 } }, + include: { booking: { select: { id: true, reference: true } } }, + orderBy: { dueAt: "asc" }, + take: 15, + }), + ]); + const count = (s: string) => byStatus.find((b) => b.status === s)?._count ?? 0; + const total = money._sum.totalCents ?? 0; + const paid = money._sum.paidCents ?? 0; + + return ( + <> + Übersicht +

Alle Zahlen beziehen sich auf den aktuellen Stripe-Modus ({mode === "live" ? "Live" : "Test"}). Test- und Livebuchungen sind getrennt.

+
+ + {state ? ( + <> + + {state.releaseBlockers.length > 0 && ( +
+

Fehlt für den Live-Verkauf:

+
    + {state.releaseBlockers.map((b) => ( +
  • {BLOCKER_TEXT[b]}
  • + ))} +
+ Freigaben bearbeiten +
+ )} + + ) : ( +

STRIPE_SECRET_KEY fehlt oder ist ungültig.

+ )} +
+ + + + + + +
+ +
+ + {reviews.length === 0 ? ( +

Keine offenen Prüfungen.

+ ) : ( +
    + {reviews.map((r) => ( +
  • + {r.reference} + {r.reviewReason} +
  • + ))} +
+ )} +
+ + {upcoming.length === 0 ? ( +

Keine offenen Folgeraten.

+ ) : ( + + + {upcoming.map((i) => ( + + + + + + + + ))} + +
{i.booking.reference}Rate {i.seq}{i.dueAt ? formatDate(i.dueAt) : "–"}{formatEuro(i.amountCents)}{i.status === "PENDING" ? "" : i.status === "FAILED" ? "fehlgeschlagen" : "Aktion nötig"}
+ )} +
+
+ + ); +} diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts new file mode 100644 index 0000000..f69e65e --- /dev/null +++ b/src/app/admin/actions.ts @@ -0,0 +1,152 @@ +"use server"; + +/** + * Server-Actions des Adminbereichs. JEDE Action prueft zuerst selbst die + * Anmeldung inkl. 2FA (requireAdminAction) — unabhaengig davon, ob die + * Seite geschuetzt ist, denn Actions sind eigenstaendig aufrufbar. + */ +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import { adminActor, requireAdminAction } from "@/lib/admin-auth"; +import { + campInputSchema, + cancelBooking, + confirmReviewedBooking, + refundInstallment, + resendConfirmation, + saveVoucher, + updateCamp, + voucherInputSchema, + type ActionResult, +} from "@/lib/admin-service"; +import { getDb } from "@/lib/db"; +import { requireAppSecret } from "@/lib/env"; +import { getGateway } from "@/lib/payment-gateway"; + +function back(path: string, result: ActionResult): never { + const param = result.ok ? "ok" : "fehler"; + redirect(`${path}?${param}=${encodeURIComponent(result.message)}`); +} + +function str(form: FormData, key: string): string { + const value = form.get(key); + return typeof value === "string" ? value : ""; +} + +function bool(form: FormData, key: string): boolean { + return form.get(key) === "on"; +} + +function firstIssue(error: { issues: { message: string }[] }): string { + return error.issues[0]?.message ?? "Ungültige Eingabe."; +} + +const ID = /^[A-Za-z0-9_-]{8,64}$/; + +export async function cancelBookingAction(form: FormData) { + const admin = await requireAdminAction(); + const id = str(form, "bookingId"); + if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." }); + const result = await cancelBooking(getDb(), getGateway(), { bookingId: id, reason: str(form, "reason") }, adminActor(admin)); + revalidatePath(`/admin/buchungen/${id}`); + back(`/admin/buchungen/${id}`, result); +} + +export async function refundAction(form: FormData) { + const admin = await requireAdminAction(); + const bookingId = str(form, "bookingId"); + const installmentId = str(form, "installmentId"); + const requestKey = str(form, "requestKey"); + if (!ID.test(bookingId) || !ID.test(installmentId) || !/^[0-9a-f-]{36}$/.test(requestKey)) { + back("/admin/buchungen", { ok: false, message: "Ungültige Anfrage." }); + } + const euros = Number(str(form, "amount").replace(",", ".")); + const amountCents = Math.round(euros * 100); + const result = await refundInstallment( + getDb(), + getGateway(), + { installmentId, amountCents, requestKey, reason: str(form, "reason") }, + adminActor(admin), + ); + revalidatePath(`/admin/buchungen/${bookingId}`); + back(`/admin/buchungen/${bookingId}`, result); +} + +export async function confirmReviewAction(form: FormData) { + const admin = await requireAdminAction(); + const id = str(form, "bookingId"); + if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." }); + const result = await confirmReviewedBooking(getDb(), id, adminActor(admin)); + revalidatePath(`/admin/buchungen/${id}`); + back(`/admin/buchungen/${id}`, result); +} + +export async function resendConfirmationAction(form: FormData) { + const admin = await requireAdminAction(); + const id = str(form, "bookingId"); + if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." }); + const result = await resendConfirmation(getDb(), id, requireAppSecret(), adminActor(admin)); + back(`/admin/buchungen/${id}`, result); +} + +export async function saveVoucherAction(form: FormData) { + const admin = await requireAdminAction(); + const id = str(form, "voucherId") || null; + if (id && !ID.test(id)) back("/admin/gutscheine", { ok: false, message: "Ungültiger Gutschein." }); + const parsed = voucherInputSchema.safeParse({ + code: str(form, "code"), + active: bool(form, "active"), + validFrom: str(form, "validFrom"), + validUntil: str(form, "validUntil"), + maxRedemptions: str(form, "maxRedemptions"), + boundEmail: str(form, "boundEmail"), + internalNote: str(form, "internalNote"), + }); + const target = id ? `/admin/gutscheine/${id}` : "/admin/gutscheine"; + if (!parsed.success) back(target, { ok: false, message: firstIssue(parsed.error) }); + const result = await saveVoucher(getDb(), id, parsed.data, adminActor(admin)); + revalidatePath("/admin/gutscheine"); + back(result.ok && result.id ? `/admin/gutscheine/${result.id}` : target, result); +} + +export async function updateCampAction(form: FormData) { + const admin = await requireAdminAction(); + const parsed = campInputSchema.safeParse({ + salesEnabled: bool(form, "salesEnabled"), + capacity: str(form, "capacity"), + capacityConfirmed: bool(form, "capacityConfirmed"), + stammmodelQuota: str(form, "stammmodelQuota"), + stammmodelQuotaConfirmed: bool(form, "stammmodelQuotaConfirmed"), + campStartDate: str(form, "campStartDate"), + campEndDate: str(form, "campEndDate"), + dateConfirmed: bool(form, "dateConfirmed"), + locationName: str(form, "locationName"), + locationRegion: str(form, "locationRegion"), + locationConfirmed: bool(form, "locationConfirmed"), + accommodationText: str(form, "accommodationText"), + accommodationConfirmed: bool(form, "accommodationConfirmed"), + servicesConfirmed: bool(form, "servicesConfirmed"), + organizerName: str(form, "organizerName"), + organizerAddress: str(form, "organizerAddress"), + organizerEmail: str(form, "organizerEmail"), + organizerPhone: str(form, "organizerPhone"), + organizerConfirmed: bool(form, "organizerConfirmed"), + taxNote: str(form, "taxNote"), + taxConfirmed: bool(form, "taxConfirmed"), + termsVersion: str(form, "termsVersion"), + termsUrl: str(form, "termsUrl"), + termsApproved: bool(form, "termsApproved"), + travelLawChecked: bool(form, "travelLawChecked"), + installmentsApproved: bool(form, "installmentsApproved"), + installmentRuleApproved: bool(form, "installmentRuleApproved"), + reservationMinutes: str(form, "reservationMinutes"), + finalInstallmentLeadDays: str(form, "finalInstallmentLeadDays"), + minInstallmentGapDays: str(form, "minInstallmentGapDays"), + testPhaseOverride: str(form, "testPhaseOverride"), + }); + if (!parsed.success) back("/admin/einstellungen", { ok: false, message: firstIssue(parsed.error) }); + const result = await updateCamp(getDb(), parsed.data, adminActor(admin)); + revalidatePath("/admin", "layout"); + revalidatePath("/"); + back("/admin/einstellungen", result); +} diff --git a/src/app/admin/anmelden/LoginForm.tsx b/src/app/admin/anmelden/LoginForm.tsx new file mode 100644 index 0000000..9d74c61 --- /dev/null +++ b/src/app/admin/anmelden/LoginForm.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { authClient } from "@/lib/auth-client"; + +export function LoginForm({ initialStep }: { initialStep: "password" | "totp" }) { + const router = useRouter(); + const [step, setStep] = useState(initialStep); + const [useBackup, setUseBackup] = useState(false); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + async function signIn(form: FormData) { + setBusy(true); + setError(null); + const { data, error: err } = await authClient.signIn.email({ + email: String(form.get("email") ?? ""), + password: String(form.get("password") ?? ""), + }); + setBusy(false); + // Bewusst ohne Unterscheidung zwischen falscher E-Mail und falschem Passwort. + if (err) { + setError(err.status === 429 ? "Zu viele Versuche. Bitte warte einige Minuten." : "Anmeldung fehlgeschlagen. Bitte prüfe E-Mail und Passwort."); + return; + } + if (data && "twoFactorRedirect" in data && data.twoFactorRedirect) { + setStep("totp"); + return; + } + router.replace("/admin"); + router.refresh(); + } + + async function verify(form: FormData) { + setBusy(true); + setError(null); + const code = String(form.get("code") ?? "").replace(/\s+/g, ""); + const result = useBackup + ? await authClient.twoFactor.verifyBackupCode({ code }) + : await authClient.twoFactor.verifyTotp({ code }); + setBusy(false); + if (result.error) { + setError(result.error.status === 429 ? "Zu viele Versuche. Bitte warte einige Minuten." : "Der Code ist ungültig oder abgelaufen."); + return; + } + router.replace("/admin"); + router.refresh(); + } + + if (step === "totp") { + return ( +
+
+ + +
+ {error &&

{error}

} + + +
+ ); + } + + return ( +
+
+ + +
+
+ + +
+ {error &&

{error}

} + +
+ ); +} diff --git a/src/app/admin/anmelden/page.tsx b/src/app/admin/anmelden/page.tsx new file mode 100644 index 0000000..2f9a1ab --- /dev/null +++ b/src/app/admin/anmelden/page.tsx @@ -0,0 +1,14 @@ +import { LoginForm } from "./LoginForm"; + +export const metadata = { title: "Anmelden" }; + +export default async function LoginPage({ searchParams }: { searchParams: Promise<{ schritt?: string }> }) { + const { schritt } = await searchParams; + return ( +
+

ARTISTA

+

Verwaltung anmelden

+ +
+ ); +} diff --git a/src/app/admin/export/route.ts b/src/app/admin/export/route.ts new file mode 100644 index 0000000..9c3445d --- /dev/null +++ b/src/app/admin/export/route.ts @@ -0,0 +1,55 @@ +/** + * CSV-Export der Buchungen (Semikolon, UTF-8 mit BOM fuer Excel). + * Nur mit gueltiger Admin-Sitzung inkl. 2FA. + */ +import { adminOrNull } from "@/lib/admin-auth"; +import { audit } from "@/lib/audit"; +import { bookingWhere, csvCell, parseBookingFilter } from "@/lib/admin-queries"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { BOOKING_STATUS_LABEL, PAYMENT_STATUS_LABEL } from "@/lib/labels"; +import { PLAN_LABEL } from "@/lib/payment-plan"; +import { PHASE_LABEL } from "@/lib/pricing"; +import { formatDate, formatDateTime } from "@/lib/time"; + +const euro = (cents: number) => (cents / 100).toFixed(2).replace(".", ","); + +export async function GET(request: Request) { + const admin = await adminOrNull(); + if (!admin) return new Response("Nicht angemeldet.", { status: 401 }); + + const params = Object.fromEntries(new URL(request.url).searchParams); + const filter = parseBookingFilter(params, stripeModeOrNull() === "live" ? "live" : "test"); + const db = getDb(); + const bookings = await db.booking.findMany({ + where: bookingWhere(filter), + orderBy: { createdAt: "asc" }, + include: { installments: { orderBy: { seq: "asc" } }, voucher: { select: { code: true } } }, + }); + + const header = [ + "Buchungsnummer", "Modus", "Buchungsstatus", "Zahlungsstatus", "Vorname", "Nachname", "E-Mail", "Telefon", "Land", + "Preisphase", "Zahlungsweise", "Gesamt EUR", "Bezahlt EUR", "Offen EUR", "Erstattet EUR", "Gutschein", + "Nächste offene Rate", "Angelegt", "Bestätigt", "Anmerkungen", + ]; + const rows = bookings.map((b) => { + const next = b.installments.find((i) => i.status !== "PAID" && i.status !== "CANCELED"); + return [ + b.reference, b.livemode ? "Live" : "Test", BOOKING_STATUS_LABEL[b.status], PAYMENT_STATUS_LABEL[b.paymentStatus], + b.firstName, b.lastName, b.email, b.phone, b.country, PHASE_LABEL[b.pricePhase], PLAN_LABEL[b.planType], + euro(b.totalCents), euro(b.paidCents), euro(Math.max(0, b.totalCents - b.paidCents)), euro(b.refundedCents), + b.voucher?.code ?? "", next ? `${euro(next.amountCents)} ${next.dueAt ? `am ${formatDate(next.dueAt)}` : "bei Buchung"}` : "", + formatDateTime(b.createdAt), b.confirmedAt ? formatDateTime(b.confirmedAt) : "", b.notes ?? "", + ].map(csvCell).join(";"); + }); + + await audit(db, { type: "ADMIN", id: admin.id }, "bookings.exported", "Booking", "*", { count: bookings.length, mode: filter.modus }); + const csv = `${[header.map(csvCell).join(";"), ...rows].join("\r\n")}\r\n`; + return new Response(csv, { + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="buchungen-${filter.modus}-${new Date().toISOString().slice(0, 10)}.csv"`, + "Cache-Control": "no-store", + }, + }); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..24fe9b3 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: { default: "Verwaltung", template: "%s – Verwaltung ARTISTA Modelcamp" }, + robots: { index: false, follow: false }, +}; + +export default function AdminRootLayout({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/src/app/admin/sicherheit/TwoFactorSetup.tsx b/src/app/admin/sicherheit/TwoFactorSetup.tsx new file mode 100644 index 0000000..8dbb4dd --- /dev/null +++ b/src/app/admin/sicherheit/TwoFactorSetup.tsx @@ -0,0 +1,92 @@ +"use client"; + +import QRCode from "qrcode"; +import { useState } from "react"; +import { authClient } from "@/lib/auth-client"; + +export function TwoFactorSetup() { + const [stage, setStage] = useState<"password" | "verify" | "done">("password"); + const [qr, setQr] = useState(null); + const [secret, setSecret] = useState(null); + const [backupCodes, setBackupCodes] = useState([]); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + async function start(form: FormData) { + setBusy(true); + setError(null); + const { data, error: err } = await authClient.twoFactor.enable({ password: String(form.get("password") ?? "") }); + setBusy(false); + if (err || !data || data.method !== "totp") { + setError("Passwort falsch oder Einrichtung nicht möglich."); + return; + } + setQr(await QRCode.toDataURL(data.totpURI, { margin: 1, width: 240 })); + setSecret(new URL(data.totpURI).searchParams.get("secret")); + setBackupCodes(data.backupCodes); + setStage("verify"); + } + + async function verify(form: FormData) { + setBusy(true); + setError(null); + const { error: err } = await authClient.twoFactor.verifyTotp({ code: String(form.get("code") ?? "").replace(/\s+/g, "") }); + setBusy(false); + if (err) { + setError("Der Code stimmt nicht. Bitte prüfe die Uhrzeit deines Geräts und versuche es erneut."); + return; + } + setStage("done"); + } + + if (stage === "password") { + return ( +
+
+ + +
+ {error &&

{error}

} + +
+ ); + } + + if (stage === "verify") { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element -- lokal erzeugte data:-URL */} + {qr && QR-Code für die Authenticator-App} + {secret && ( +

+ Manuelle Eingabe: {secret} +

+ )} +
+

Backup-Codes

+

Jetzt sicher ablegen (z. B. im Passwortmanager). Sie werden nicht erneut angezeigt.

+
    + {backupCodes.map((c) => ( +
  • {c}
  • + ))} +
+
+
+
+ + +
+ {error &&

{error}

} + +
+
+ ); + } + + return ( +
+

Zwei-Faktor-Anmeldung ist aktiv.

+ Zur Übersicht +
+ ); +} diff --git a/src/app/admin/sicherheit/page.tsx b/src/app/admin/sicherheit/page.tsx new file mode 100644 index 0000000..99c5004 --- /dev/null +++ b/src/app/admin/sicherheit/page.tsx @@ -0,0 +1,29 @@ +import Link from "next/link"; +import { requireAdminPage } from "@/lib/admin-auth"; +import { TwoFactorSetup } from "./TwoFactorSetup"; + +export const metadata = { title: "Sicherheit" }; + +export default async function SecurityPage() { + const admin = await requireAdminPage({ allowWithout2fa: true }); + return ( +
+

ARTISTA

+

Zwei-Faktor-Anmeldung

+ {admin.twoFactorEnabled ? ( + <> +

Für {admin.email} ist die Zwei-Faktor-Anmeldung aktiv.

+ Zur Übersicht + + ) : ( + <> +

+ Der Adminbereich ist nur mit Zwei-Faktor-Anmeldung nutzbar. Richte sie jetzt mit einer Authenticator-App ein (z. B. Aegis, 2FAS oder die App deines + Passwortmanagers). +

+ + + )} +
+ ); +} diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..cb91831 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,10 @@ +import { toNextJsHandler } from "better-auth/next-js"; +import { getAuth } from "@/lib/auth"; + +export async function GET(request: Request) { + return toNextJsHandler(getAuth()).GET(request); +} + +export async function POST(request: Request) { + return toNextJsHandler(getAuth()).POST(request); +} diff --git a/src/app/api/booking/checkout/route.ts b/src/app/api/booking/checkout/route.ts new file mode 100644 index 0000000..5b916a5 --- /dev/null +++ b/src/app/api/booking/checkout/route.ts @@ -0,0 +1,55 @@ +/** + * Startet eine Buchung. Preis, Gutschein, Zahlungsplan und Verkaufsfreigabe + * werden ausschliesslich hier auf dem Server bestimmt — vom Browser kommen + * nur Teilnehmerdaten, gewaehlte Zahlungsweise, Gutscheincode und + * Erklaerungen, nie ein Betrag. + */ +import { checkSameOrigin, fail, ok, rateLimited, readJson, type ApiError } from "@/lib/api"; +import { bookingRequestSchema } from "@/lib/booking-service"; +import { startCheckout, type CheckoutError } from "@/lib/checkout"; +import { getDb } from "@/lib/db"; +import { requireAppSecret } from "@/lib/env"; +import { getGateway } from "@/lib/payment-gateway"; +import { RATE_LIMITS } from "@/lib/rate-limit"; +import { PUBLIC_VOUCHER_REJECTION } from "@/lib/vouchers"; + +function mapError(error: CheckoutError): { status: number; body: ApiError } { + switch (error.code) { + case "SALES_CLOSED": + return { status: 409, body: { code: error.code, message: "Die Buchung ist derzeit nicht geöffnet." } }; + case "SOLD_OUT": + return { status: 409, body: { code: error.code, message: "Derzeit ist leider kein Platz frei." } }; + case "VOUCHER_REJECTED": + return { status: 422, body: { code: error.code, message: PUBLIC_VOUCHER_REJECTION, fields: { voucherCode: PUBLIC_VOUCHER_REJECTION } } }; + case "PLAN_UNAVAILABLE": + return { status: 409, body: { code: error.code, message: "Diese Zahlungsweise ist nicht mehr verfügbar. Bitte wähle eine andere." } }; + case "IDEMPOTENCY_CONFLICT": + case "ATTEMPT_EXPIRED": + return { status: 409, body: { code: error.code, message: "Die Reservierung ist abgelaufen. Bitte starte die Buchung erneut." } }; + case "ALREADY_COMPLETED": + return { status: 409, body: { code: error.code, message: "Diese Buchung wurde bereits abgeschlossen. Den Stand findest du in deiner Bestätigungs-E-Mail." } }; + case "PAYMENT_PROVIDER": + return { status: 502, body: { code: error.code, message: "Die Zahlungsseite ist gerade nicht erreichbar. Bitte versuche es in einigen Minuten erneut." } }; + } +} + +export async function POST(request: Request) { + const blocked = checkSameOrigin(request); + if (blocked) return blocked; + const limited = await rateLimited(request, RATE_LIMITS.checkout); + if (limited) return limited; + const body = await readJson(request, bookingRequestSchema); + if (!body.ok) return body.response; + + let gateway; + try { + gateway = getGateway(); + } catch { + return fail(503, { code: "NOT_CONFIGURED", message: "Die Buchung ist derzeit nicht verfügbar." }); + } + + const result = await startCheckout(getDb(), gateway, body.data, { now: new Date(), secret: requireAppSecret() }); + if (result.ok) return ok({ url: result.url, reference: result.reference }); + const mapped = mapError(result.error); + return fail(mapped.status, mapped.body); +} diff --git a/src/app/api/booking/quote/route.ts b/src/app/api/booking/quote/route.ts new file mode 100644 index 0000000..52ec1ad --- /dev/null +++ b/src/app/api/booking/quote/route.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { checkSameOrigin, fail, ok, rateLimited, readJson } from "@/lib/api"; +import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; +import { computeQuote } from "@/lib/quote"; +import { RATE_LIMITS } from "@/lib/rate-limit"; + +const schema = z.object({ + voucherCode: z.string().trim().max(60).optional(), + email: z.string().trim().max(200).optional(), +}); + +export async function POST(request: Request) { + const blocked = checkSameOrigin(request); + if (blocked) return blocked; + const body = await readJson(request, schema); + if (!body.ok) return body.response; + + const limited = await rateLimited(request, RATE_LIMITS.quote); + if (limited) return limited; + // Gutscheinpruefungen zusaetzlich eng begrenzen: gegen Durchprobieren. + if (body.data.voucherCode) { + const voucherLimited = await rateLimited(request, RATE_LIMITS.voucher); + if (voucherLimited) return voucherLimited; + } + + const mode = stripeModeOrNull(); + if (!mode) return fail(503, { code: "NOT_CONFIGURED", message: "Die Buchung ist derzeit nicht verfügbar." }); + + const email = body.data.email && z.email().safeParse(body.data.email).success ? body.data.email : undefined; + const quote = await computeQuote(getDb(), { voucherCode: body.data.voucherCode || undefined, email }, { now: new Date(), mode }); + return ok(quote); +} diff --git a/src/app/api/booking/status-action/route.ts b/src/app/api/booking/status-action/route.ts new file mode 100644 index 0000000..8fc1412 --- /dev/null +++ b/src/app/api/booking/status-action/route.ts @@ -0,0 +1,57 @@ +/** + * Aktionen der Kundin auf ihrer Statusseite. Zugang nur ueber das + * Statustoken aus Bestaetigungsmail bzw. Stripe-Ruecksprung. + */ +import { z } from "zod"; +import { checkSameOrigin, fail, ok, rateLimited, readJson } from "@/lib/api"; +import { abandonCheckout, startInstallmentCheckout } from "@/lib/checkout"; +import { getDb } from "@/lib/db"; +import { requireAppSecret } from "@/lib/env"; +import { getGateway } from "@/lib/payment-gateway"; +import { RATE_LIMITS } from "@/lib/rate-limit"; +import { isWellFormedStatusToken, sha256Hex } from "@/lib/tokens"; + +const schema = z.discriminatedUnion("action", [ + z.object({ action: z.literal("pay_installment"), token: z.string(), installmentId: z.string().min(1).max(64) }), + z.object({ action: z.literal("resume_checkout"), token: z.string() }), + z.object({ action: z.literal("abandon"), token: z.string() }), +]); + +export async function POST(request: Request) { + const blocked = checkSameOrigin(request); + if (blocked) return blocked; + const limited = await rateLimited(request, RATE_LIMITS.statusAction); + if (limited) return limited; + const body = await readJson(request, schema); + if (!body.ok) return body.response; + + const notFound = fail(404, { code: "NOT_FOUND", message: "Diese Buchung wurde nicht gefunden." }); + if (!isWellFormedStatusToken(body.data.token)) return notFound; + const db = getDb(); + const booking = await db.booking.findUnique({ where: { statusTokenHash: sha256Hex(body.data.token) } }); + if (!booking) return notFound; + + const gateway = getGateway(); + if (booking.livemode !== (gateway.mode === "live")) return notFound; + const now = new Date(); + + switch (body.data.action) { + case "pay_installment": { + const result = await startInstallmentCheckout(db, gateway, { bookingId: booking.id, installmentId: body.data.installmentId }, { now, secret: requireAppSecret() }); + if (result.ok) return ok({ url: result.url }); + return result.error === "NOT_PAYABLE" + ? fail(409, { code: "NOT_PAYABLE", message: "Diese Rate kann gerade nicht bezahlt werden. Bitte lade die Seite neu." }) + : fail(502, { code: "PAYMENT_PROVIDER", message: "Die Zahlungsseite ist gerade nicht erreichbar. Bitte versuche es später erneut." }); + } + case "resume_checkout": { + if (booking.status === "PENDING" && booking.paymentStatus === "UNPAID" && booking.stripeCheckoutUrl && booking.priceLockedUntil > now) { + return ok({ url: booking.stripeCheckoutUrl }); + } + return fail(409, { code: "EXPIRED", message: "Die Reservierung ist abgelaufen. Bitte starte eine neue Buchung." }); + } + case "abandon": { + await abandonCheckout(db, gateway, booking, { now }); + return ok({ abandoned: true }); + } + } +} diff --git a/src/app/api/health/ready/route.ts b/src/app/api/health/ready/route.ts new file mode 100644 index 0000000..4a184aa --- /dev/null +++ b/src/app/api/health/ready/route.ts @@ -0,0 +1,11 @@ +import { getDb } from "@/lib/db"; + +/** Readiness: nimmt den Pod aus dem Load-Balancing, wenn die Datenbank fehlt. */ +export async function GET() { + try { + await getDb().$queryRaw`SELECT 1`; + return Response.json({ status: "ready" }, { headers: { "Cache-Control": "no-store" } }); + } catch { + return Response.json({ status: "database_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store" } }); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..d20d437 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,4 @@ +/** Liveness: bewusst ohne Datenbank — eine DB-Stoerung soll den Pod nicht neu starten. */ +export function GET() { + return Response.json({ status: "ok" }, { headers: { "Cache-Control": "no-store" } }); +} diff --git a/src/app/api/stripe/webhook/route.ts b/src/app/api/stripe/webhook/route.ts new file mode 100644 index 0000000..243ce33 --- /dev/null +++ b/src/app/api/stripe/webhook/route.ts @@ -0,0 +1,63 @@ +/** + * Stripe-Webhook. Einzige Stelle (neben dem Worker-Abgleich), die Zahlungen + * als eingegangen verbucht — der Aufruf der Erfolgsseite tut das nie. + * + * Signaturpruefung gegen den unveraenderten Body: deshalb request.text() + * statt request.json(). + */ +import { getDb } from "@/lib/db"; +import { flushEmails } from "@/lib/email"; +import { env, requireAppSecret } from "@/lib/env"; +import { getGateway, verifyStripeWebhook } from "@/lib/payment-gateway"; +import { handleStripeEvent } from "@/lib/webhook"; +import { detachFinishedPaymentMethods } from "@/lib/worker-jobs"; + +const MAX_WEBHOOK_BYTES = 512 * 1024; + +export async function POST(request: Request) { + const secret = env().STRIPE_WEBHOOK_SECRET; + if (!secret) return new Response("Webhook nicht konfiguriert", { status: 503 }); + + const signature = request.headers.get("stripe-signature"); + if (!signature) return new Response("Signatur fehlt", { status: 400 }); + // Stripe-Events sind wenige Kilobyte. Groessere Koerper werden abgewiesen, + // bevor sie (unsigniert) vollstaendig verarbeitet werden. + const declared = Number(request.headers.get("content-length") ?? "0"); + if (declared > MAX_WEBHOOK_BYTES) return new Response("Zu gross", { status: 413 }); + const raw = await request.text(); + if (raw.length > MAX_WEBHOOK_BYTES) return new Response("Zu gross", { status: 413 }); + + let event; + try { + event = verifyStripeWebhook(raw, signature, secret); + } catch { + return new Response("Ungueltige Signatur", { status: 400 }); + } + + const db = getDb(); + const gateway = getGateway(); + let bookingIds: string[]; + try { + const result = await handleStripeEvent(db, gateway, event, { + now: new Date(), + secret: requireAppSecret(), + adminEmail: env().ADMIN_NOTIFY_EMAIL ?? null, + }); + bookingIds = result.bookingIds; + } catch (error) { + // 500 -> Stripe stellt das Event spaeter erneut zu. + console.error(`[webhook] Verarbeitung von ${event.id} (${event.type}) fehlgeschlagen: ${error instanceof Error ? error.message : "unbekannt"}`); + return new Response("Verarbeitung fehlgeschlagen", { status: 500 }); + } + + // Nachgelagert und fehlertolerant: der Worker holt beides nach. + if (bookingIds.length > 0) { + try { + await detachFinishedPaymentMethods(db, gateway, bookingIds); + for (const id of bookingIds) await flushEmails(db, { bookingId: id }); + } catch (error) { + console.error(`[webhook] Nacharbeit fuer ${event.id} verschoben: ${error instanceof Error ? error.message : "unbekannt"}`); + } + } + return Response.json({ received: true }); +} diff --git a/src/app/buchung/status/[token]/StatusActions.tsx b/src/app/buchung/status/[token]/StatusActions.tsx new file mode 100644 index 0000000..50e7fba --- /dev/null +++ b/src/app/buchung/status/[token]/StatusActions.tsx @@ -0,0 +1,119 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +async function action(body: Record): Promise<{ url?: string; error?: string }> { + try { + const res = await fetch("/api/booking/status-action", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + const json = await res.json(); + if (json.success) return { url: json.data.url }; + return { error: json.error?.message ?? "Das hat nicht geklappt. Bitte lade die Seite neu." }; + } catch { + return { error: "Keine Verbindung. Bitte versuche es erneut." }; + } +} + +/** Fragt nach der Rueckkehr von Stripe einige Male den bestaetigten Stand ab. */ +export function AutoRefresh() { + const router = useRouter(); + useEffect(() => { + let count = 0; + const timer = setInterval(() => { + count++; + router.refresh(); + if (count >= 20) clearInterval(timer); + }, 3000); + return () => clearInterval(timer); + }, [router]); + return null; +} + +export function PayInstallmentButton({ token, installmentId }: { token: string; installmentId: string }) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + return ( +
+ + {error &&

{error}

} +
+ ); +} + +export function PendingCheckoutActions({ token, canResume, lockedUntil }: { token: string; canResume: boolean; lockedUntil: string }) { + const router = useRouter(); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + return ( +
+

Die Zahlung ist noch nicht abgeschlossen.

+

+ {canResume + ? `Dein Platz und der Preis sind bis ${lockedUntil} reserviert.` + : "Die Reservierung ist abgelaufen. Du kannst jederzeit eine neue Buchung starten."} +

+
+ {canResume && ( + + )} + {canResume ? ( + + ) : ( + + Neue Buchung starten + + )} +
+ {error &&

{error}

} +
+ ); +} diff --git a/src/app/buchung/status/[token]/page.tsx b/src/app/buchung/status/[token]/page.tsx new file mode 100644 index 0000000..36f4325 --- /dev/null +++ b/src/app/buchung/status/[token]/page.tsx @@ -0,0 +1,149 @@ +/** + * Buchungsstatus fuer die Kundin. Zeigt ausschliesslich den in der + * Datenbank bestaetigten Stand. Der Ruecksprung von Stripe (?checkout=success) + * aendert nichts — bezahlt ist eine Buchung erst, wenn der Webhook es + * bestaetigt hat. + */ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; +import { getDb } from "@/lib/db"; +import { BOOKING_STATUS_LABEL, INSTALLMENT_STATUS_LABEL, PAYMENT_STATUS_LABEL } from "@/lib/labels"; +import { formatEuro } from "@/lib/money"; +import { PLAN_LABEL } from "@/lib/payment-plan"; +import { PHASE_LABEL } from "@/lib/pricing"; +import { formatDate, formatDateTime } from "@/lib/time"; +import { isWellFormedStatusToken, sha256Hex } from "@/lib/tokens"; +import { AutoRefresh, PayInstallmentButton, PendingCheckoutActions } from "./StatusActions"; + +export const metadata: Metadata = { + title: "Deine Buchung", + robots: { index: false, follow: false }, +}; + +export default async function StatusPage({ + params, + searchParams, +}: { + params: Promise<{ token: string }>; + searchParams: Promise<{ checkout?: string }>; +}) { + await connection(); + const { token } = await params; + const { checkout } = await searchParams; + if (!isWellFormedStatusToken(token)) notFound(); + + const booking = await getDb().booking.findUnique({ + where: { statusTokenHash: sha256Hex(token) }, + include: { installments: { orderBy: { seq: "asc" } } }, + }); + if (!booking) notFound(); + + const now = new Date(); + const open = Math.max(0, booking.totalCents - booking.paidCents); + const waitingForConfirmation = checkout === "success" && booking.status === "PENDING" && booking.paymentStatus === "UNPAID"; + const pendingUnpaid = booking.status === "PENDING" && booking.paymentStatus === "UNPAID"; + + return ( +
+ + ARTISTA Modelcamp 2027 + +

Buchung {booking.reference}

+

Hallo {booking.firstName}

+ + {!booking.livemode && ( +

+ Testbuchung Diese Buchung wurde im Stripe-Testmodus angelegt; es wurde kein Geld eingezogen. +

+ )} + +
+ {waitingForConfirmation ? ( + <> +

Wir warten auf die Bestätigung deiner Zahlung.

+

+ Das dauert meist nur wenige Sekunden. Diese Seite aktualisiert sich selbst; zusätzlich bekommst du eine E-Mail, sobald die Zahlung bestätigt ist. +

+ + + ) : ( +
+
+
Buchung
+
{BOOKING_STATUS_LABEL[booking.status]}
+
+
+
Zahlung
+
{PAYMENT_STATUS_LABEL[booking.paymentStatus]}
+
+
+ )} + {booking.status === "NEEDS_REVIEW" && ( +

Deine Zahlung ist eingegangen. Wir prüfen die Buchung kurz manuell und melden uns bei dir.

+ )} +
+ + {pendingUnpaid && checkout !== "success" && ( + now} lockedUntil={formatDateTime(booking.priceLockedUntil)} /> + )} + +
+

Übersicht

+
+ {PHASE_LABEL[booking.pricePhase]} + {formatEuro(booking.totalCents)} + {booking.voucherDiscountCents > 0 && −{formatEuro(booking.voucherDiscountCents)}} + {PLAN_LABEL[booking.planType]} + {formatEuro(booking.paidCents)} + {formatEuro(open)} + {booking.refundedCents > 0 && {formatEuro(booking.refundedCents)}} +
+
+ +
+

Zahlungsplan

+ + + + + + + + + + + {booking.installments.map((i) => ( + + + + + + + ))} + +
RateFälligBetragStatus
{booking.installments.length > 1 ? i.seq : "–"}{i.dueAt ? formatDate(i.dueAt) : "bei Buchung"}{formatEuro(i.amountCents)} + {INSTALLMENT_STATUS_LABEL[i.status]} + {booking.status === "CONFIRMED" && i.seq > 1 && (i.status === "FAILED" || i.status === "REQUIRES_ACTION") && ( + + )} +
+ {booking.installments.length > 1 && ( +

+ Weitere Raten werden am Fälligkeitstag über die gespeicherte Zahlungsmethode eingezogen. Nach der letzten Rate wird die Zahlungsmethode gelöst. +

+ )} +
+
+ ); +} + +function Row({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} diff --git a/src/app/buchungsbedingungen/page.tsx b/src/app/buchungsbedingungen/page.tsx new file mode 100644 index 0000000..2ec6321 --- /dev/null +++ b/src/app/buchungsbedingungen/page.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next"; +import { connection } from "next/server"; +import { redirect } from "next/navigation"; +import { LegalPage, PendingLegalNotice } from "@/components/LegalPage"; +import { getCamp, publicCampFacts } from "@/lib/camp"; +import { getDb } from "@/lib/db"; + +export const metadata: Metadata = { title: "Buchungsbedingungen", alternates: { canonical: "/buchungsbedingungen" } }; + +/** + * Die freigegebenen Buchungs-, Storno- und Zahlungsbedingungen werden als + * Dokument (termsUrl) im Adminbereich hinterlegt. Hier werden keine + * Bedingungen formuliert oder erfunden. + */ +export default async function TermsPage() { + await connection(); + const facts = publicCampFacts(await getCamp(getDb())); + if (facts.termsUrl && facts.termsUrl !== "/buchungsbedingungen") redirect(facts.termsUrl); + return ( + + + + ); +} diff --git a/src/app/datenschutz/page.tsx b/src/app/datenschutz/page.tsx new file mode 100644 index 0000000..2a00bff --- /dev/null +++ b/src/app/datenschutz/page.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { LegalPage, PendingLegalNotice } from "@/components/LegalPage"; +import { showDevPlaceholders } from "@/lib/env"; + +export const metadata: Metadata = { title: "Datenschutz", alternates: { canonical: "/datenschutz" } }; +export const dynamic = "force-dynamic"; + +/** + * Die Datenschutzerklaerung wird rechtlich geprueft ergaenzt. Die Liste + * unten beschreibt nur, was diese Anwendung technisch tatsaechlich tut — + * als Grundlage fuer die Erstellung, sichtbar nur ausserhalb der Produktion. + */ +export default function DatenschutzPage() { + return ( + + + {showDevPlaceholders() && ( +
+

Technische Grundlage für die Erstellung (nur in Entwicklung sichtbar)

+
    +
  • Buchungsdaten: Name, E-Mail, Telefon, Land, optionale Anmerkungen, Zeitpunkte der Erklärungen; gespeichert in einer selbst betriebenen PostgreSQL-Datenbank.
  • +
  • Zahlungen über Stripe (Stripe Payments Europe Ltd.); Kartendaten gelangen nicht auf unsere Server, gespeichert werden nur Stripe-Objekt-IDs.
  • +
  • E-Mail-Versand über den konfigurierten SMTP-Anbieter.
  • +
  • Rate Limiting speichert nur einen gehashten IP-Schlüssel, maximal einen Tag.
  • +
  • Keine Analyse- oder Tracking-Dienste, keine Cookies auf der öffentlichen Seite; Schriften werden selbst ausgeliefert.
  • +
  • Externe Videoanbieter nur nach Klick und nur, wenn EXTERNAL_VIDEO_PROVIDER gesetzt ist.
  • +
  • Adminbereich: Sitzungscookie für angemeldete Administratorinnen.
  • +
+
+ )} +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..957f0cf --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,251 @@ +@import "tailwindcss"; + +/* + * Gestaltung: Modemagazin trifft italienischen Sommer. + * Putz-Weiss (plaster) als Grund, Anthrazit (ink) fuer Schrift und dunkle + * Flaechen, Pompeji-Rot (rosso) als sparsam eingesetzter Akzent. + * Kontraste (gegen plaster): ink 14.6:1, stone 5.3:1, rosso 6.7:1. + */ +@theme { + --color-plaster: #f2ece3; + --color-paper: #faf7f1; + --color-ink: #23211f; + --color-stone: #676058; + --color-line: #d8cec0; + --color-rosso: #8e3b2b; + --color-rosso-deep: #6f2c20; + --color-sand: #c9b79c; + + --font-display: var(--font-bodoni), "Bodoni 72", "Didot", Georgia, serif; + --font-sans: var(--font-hanken), "Helvetica Neue", Arial, sans-serif; + + --ease-editorial: cubic-bezier(0.22, 1, 0.36, 1); +} + +@layer base { + html { + scroll-behavior: smooth; + scroll-padding-top: 5rem; + -webkit-text-size-adjust: 100%; + } + + body { + background: var(--color-plaster); + color: var(--color-ink); + font-family: var(--font-sans); + font-size: 1.0625rem; + line-height: 1.6; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + } + + ::selection { + background: var(--color-rosso); + color: var(--color-paper); + } + + /* Sichtbarer Fokus fuer Tastaturbedienung, nie nur ueber Farbe. */ + :focus-visible { + outline: 2px solid var(--color-rosso); + outline-offset: 3px; + } + + .on-dark :focus-visible { + outline-color: var(--color-paper); + } + + @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } +} + +@layer components { + .container-edge { + width: 100%; + max-width: 88rem; + margin-inline: auto; + padding-inline: 1rem; + } + @media (min-width: 48rem) { + .container-edge { + padding-inline: 2.5rem; + } + } + + .display { + font-family: var(--font-display); + font-weight: 400; + letter-spacing: -0.01em; + line-height: 1.02; + } + + .section-title { + font-family: var(--font-display); + font-weight: 400; + font-size: clamp(2.25rem, 1.4rem + 3.6vw, 4.25rem); + line-height: 1.02; + letter-spacing: -0.015em; + } + + .prose-body { + max-width: 38rem; + } + .prose-body p + p { + margin-top: 1em; + } + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 3rem; + padding: 0.75rem 1.5rem; + font-weight: 600; + font-size: 1rem; + border-radius: 999px; + transition: + background-color 180ms var(--ease-editorial), + color 180ms var(--ease-editorial), + transform 180ms var(--ease-editorial); + } + .btn:active { + transform: scale(0.98); + } + .btn-primary { + background: var(--color-rosso); + color: var(--color-paper); + } + .btn-primary:hover { + background: var(--color-rosso-deep); + } + .btn-primary:disabled { + background: var(--color-stone); + cursor: not-allowed; + } + .btn-quiet { + border: 1px solid currentColor; + } + .btn-quiet:hover { + background: var(--color-ink); + color: var(--color-paper); + border-color: var(--color-ink); + } + + .field-label { + display: block; + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.375rem; + } + .field { + width: 100%; + min-height: 3rem; + padding: 0.625rem 0.875rem; + background: var(--color-paper); + border: 1px solid #b3a794; + border-radius: 0.5rem; + font-size: 1rem; + } + .field:focus-visible { + outline-offset: 0; + border-color: var(--color-ink); + } + .field[aria-invalid="true"] { + border-color: var(--color-rosso); + } + + .draft-flag { + display: inline-block; + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 999px; + background: #efe0b8; + color: #5c4712; + } +} + +/* Masthead: der eine laute Moment der Seite. */ +.masthead { + font-family: var(--font-display); + font-weight: 500; + font-size: clamp(4.25rem, 21vw, 19rem); + line-height: 0.8; + letter-spacing: -0.035em; + font-variation-settings: "opsz" 96; +} + +/* + * Einzige orchestrierte Eroeffnung: Masthead steigt auf, das Leitbild setzt + * sich, die Cover-Lines folgen. Reines CSS, kein JavaScript noetig; bei + * reduzierter Bewegung greift die Regel in @layer base. + */ +@keyframes hero-rise { + from { + opacity: 0; + transform: translateY(0.18em); + clip-path: inset(0 0 100% 0); + } + to { + opacity: 1; + transform: none; + clip-path: inset(0 0 0 0); + } +} +@keyframes hero-settle { + from { + opacity: 0; + transform: scale(1.035); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes hero-fade { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } +} +.hero-rise { + animation: hero-rise 1.1s var(--ease-editorial) both; +} +.hero-image { + overflow: hidden; +} +.hero-image > * { + animation: hero-settle 1.5s var(--ease-editorial) 0.15s both; +} +.hero-fade { + animation: hero-fade 0.9s var(--ease-editorial) 0.5s both; +} + +/* Mobile Buchungsleiste: Inhalt am Seitenende nie verdecken. */ +body:has(.mobile-book-bar[data-visible="true"]) footer { + padding-bottom: 6rem; +} +@media (min-width: 64rem) { + body:has(.mobile-book-bar[data-visible="true"]) footer { + padding-bottom: 0; + } +} + +dialog.booking-dialog::backdrop { + background: rgb(35 33 31 / 0.55); + backdrop-filter: blur(2px); +} diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx new file mode 100644 index 0000000..3bd66d9 --- /dev/null +++ b/src/app/impressum/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import { connection } from "next/server"; +import { LegalPage, PendingLegalNotice } from "@/components/LegalPage"; +import { getCamp, publicCampFacts } from "@/lib/camp"; +import { getDb } from "@/lib/db"; + +export const metadata: Metadata = { title: "Impressum", alternates: { canonical: "/impressum" } }; + +export default async function ImpressumPage() { + await connection(); + const { organizer } = publicCampFacts(await getCamp(getDb())); + return ( + + {organizer ? ( + <> +

Angaben gemäß § 5 DDG

+
+ {organizer.name} +
+ {organizer.address} +
+

+ E-Mail: {organizer.email} + {organizer.phone && ( + <> +
+ Telefon: {organizer.phone} + + )} +

+

Weitere Pflichtangaben (z. B. Vertretungsberechtigte, Register, USt-IdNr.) werden nach rechtlicher Prüfung ergänzt.

+ + ) : ( + + )} +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..853164e --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,59 @@ +import type { Metadata, Viewport } from "next"; +import { Bodoni_Moda, Hanken_Grotesk } from "next/font/google"; +import { env } from "@/lib/env"; +import "./globals.css"; + +// next/font laedt die Schriften zur Build-Zeit herunter und liefert sie +// selbst aus: Besucherinnen stellen keine Verbindung zu Google her. +const bodoni = Bodoni_Moda({ + subsets: ["latin"], + variable: "--font-bodoni", + display: "swap", + axes: ["opsz"], + style: ["normal", "italic"], +}); + +const hanken = Hanken_Grotesk({ + subsets: ["latin"], + variable: "--font-hanken", + display: "swap", +}); + +const TITLE = "ARTISTA MODELCAMP 2027"; +const DESCRIPTION = + "Das ARTISTA Modelcamp 2027 in Italien: lernen, arbeiten und wohnen unter einem Dach. Alle Infos zu Unterkunft, Dozentinnen, Preis und Buchung."; + +export function generateMetadata(): Metadata { + const { APP_URL, APP_ENV } = env(); + return { + metadataBase: new URL(APP_URL), + title: { default: TITLE, template: `%s – ${TITLE}` }, + description: DESCRIPTION, + alternates: { canonical: "/" }, + openGraph: { + type: "website", + locale: "de_DE", + url: "/", + siteName: TITLE, + title: TITLE, + description: DESCRIPTION, + }, + twitter: { card: "summary_large_image", title: TITLE, description: DESCRIPTION }, + // Nur die Produktivumgebung darf indexiert werden. + robots: APP_ENV === "production" ? { index: true, follow: true } : { index: false, follow: false }, + }; +} + +export const viewport: Viewport = { + themeColor: "#f2ece3", + width: "device-width", + initialScale: 1, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/media/[...path]/route.ts b/src/app/media/[...path]/route.ts new file mode 100644 index 0000000..e327ee4 --- /dev/null +++ b/src/app/media/[...path]/route.ts @@ -0,0 +1,70 @@ +/** + * Auslieferung freigegebener Medien aus dem persistenten Verzeichnis + * MEDIA_DIR (Kubernetes: eigenes PersistentVolume, kein emptyDir). + * + * Nur lesend, nur erlaubte Dateiendungen, kein Verlassen des Verzeichnisses. + * Unterstuetzt Range-Anfragen, damit Videos spulbar sind. + */ +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { env } from "@/lib/env"; + +const TYPES: Record = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".webp": "image/webp", + ".avif": "image/avif", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".vtt": "text/vtt", +}; + +const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/; + +export async function GET(request: Request, { params }: { params: Promise<{ path: string[] }> }) { + const { path: segments } = await params; + const notFound = new Response("Nicht gefunden", { status: 404 }); + if (segments.length === 0 || segments.length > 4 || !segments.every((s) => SEGMENT.test(s) && !s.includes(".."))) return notFound; + + const base = path.resolve(env().MEDIA_DIR); + const file = path.resolve(base, ...segments); + if (!file.startsWith(base + path.sep)) return notFound; + const type = TYPES[path.extname(file).toLowerCase()]; + if (!type) return notFound; + + let info; + try { + info = await stat(file); + } catch { + return notFound; + } + if (!info.isFile()) return notFound; + + const headers: Record = { + "Content-Type": type, + "Accept-Ranges": "bytes", + "Cache-Control": "public, max-age=86400", + "X-Content-Type-Options": "nosniff", + }; + + const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.get("range") ?? ""); + if (range && (range[1] || range[2])) { + const size = info.size; + let start = range[1] ? Number(range[1]) : size - Number(range[2]); + let end = range[1] && range[2] ? Number(range[2]) : size - 1; + start = Math.max(0, start); + end = Math.min(end, size - 1); + if (start > end) return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${size}` } }); + const stream = Readable.toWeb(createReadStream(file, { start, end })) as ReadableStream; + return new Response(stream, { + status: 206, + headers: { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": String(end - start + 1) }, + }); + } + + const stream = Readable.toWeb(createReadStream(file)) as ReadableStream; + return new Response(stream, { headers: { ...headers, "Content-Length": String(info.size) } }); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..de82652 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,39 @@ +import { connection } from "next/server"; +import { BookingProvider } from "@/components/booking/BookingProvider"; +import { MobileBookingBar } from "@/components/booking/MobileBookingBar"; +import { Hero, priceLine } from "@/components/Hero"; +import { AccommodationSection, CampSection, ServicesSection } from "@/components/sections/CampSections"; +import { ClosingSection, FaqSection } from "@/components/sections/FaqSection"; +import { AboutSection, TeachersSection, VoicesSection } from "@/components/sections/PeopleSections"; +import { PricingSection } from "@/components/sections/PricingSection"; +import { SiteFooter } from "@/components/SiteFooter"; +import { SiteHeader } from "@/components/SiteHeader"; +import { EventJsonLd } from "@/components/EventJsonLd"; +import { loadPublicPage } from "@/lib/public-page"; + +export default async function HomePage() { + // Preisphase und Verkaufsfreigabe haengen von Uhrzeit und Datenbank ab. + await connection(); + const { facts, pricing, showDrafts, externalVideoProvider } = await loadPublicPage(); + + return ( + + +
+ + + + + + + + + + +
+ + + +
+ ); +} diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..8df2b30 --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next"; +import { env } from "@/lib/env"; + +export const dynamic = "force-dynamic"; + +export default function robots(): MetadataRoute.Robots { + const { APP_ENV, APP_URL } = env(); + if (APP_ENV !== "production") return { rules: { userAgent: "*", disallow: "/" } }; + return { + rules: { userAgent: "*", allow: "/", disallow: ["/admin", "/buchung", "/api"] }, + host: new URL(APP_URL).host, + }; +} diff --git a/src/components/EventJsonLd.tsx b/src/components/EventJsonLd.tsx new file mode 100644 index 0000000..4e48f50 --- /dev/null +++ b/src/components/EventJsonLd.tsx @@ -0,0 +1,35 @@ +import type { PublicCampFacts } from "@/lib/camp"; +import { appUrl } from "@/lib/env"; +import type { PublicPricing } from "@/lib/public-page"; + +/** + * Strukturierte Veranstaltungsdaten (schema.org/Event) — nur, wenn Termin, + * Ort und Veranstalter bestaetigt sind und der Live-Verkauf laeuft. + * Unbestaetigte Angaben erscheinen nie in Suchmaschinen. + */ +export function EventJsonLd({ facts, pricing }: { facts: PublicCampFacts; pricing: PublicPricing }) { + if (!facts.startDate || !facts.location || !facts.organizer || pricing.mode !== "live" || !pricing.checkoutOpen || pricing.currentCents === null) { + return null; + } + const data = { + "@context": "https://schema.org", + "@type": "Event", + name: "ARTISTA MODELCAMP 2027", + startDate: facts.startDate, + ...(facts.endDate ? { endDate: facts.endDate } : {}), + eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", + eventStatus: "https://schema.org/EventScheduled", + location: { "@type": "Place", name: facts.location, address: { "@type": "PostalAddress", addressCountry: "IT", ...(facts.region ? { addressRegion: facts.region } : {}) } }, + organizer: { "@type": "Organization", name: facts.organizer.name, email: facts.organizer.email }, + offers: { + "@type": "Offer", + price: (pricing.currentCents / 100).toFixed(2), + priceCurrency: "EUR", + availability: "https://schema.org/InStock", + url: appUrl("/#preis"), + }, + }; + // JSON.stringify + Ersetzen von "<" verhindert, dass Inhalte das + // Script-Element vorzeitig schliessen. + return