bfdf0a82da
Tests, CSP nonce middleware, SSRF guard, perf-route hardening, Docker env isolation, migration runbook, RBAC E2E coverage. Tickets resolved: - #19: MfaSetup.test.ts — static source tests confirming local QR rendering - #20: ssrf-guard.test.ts (16 tests) + webhook-procedure-support mock fix - #21: /api/perf route.test.ts (5 tests) — header-only auth, fail-closed - #22: middleware.ts (nonce-based CSP) + middleware.test.ts (6 tests); layout.tsx async + nonce prop; CSP removed from next.config.ts - #23: Active-session registry enforcement verified (already in codebase) - #24: docker-compose.yml REDIS_URL hardcoded (no host-env substitution) - #25: docker-compose.yml REDIS_URL + docs/developer-runbook.md created - #26: e2e/dev-system/rbac-data-access.spec.ts (12 tests, 3 roles × 4 procedures) Quality gates: tsc clean, api 1447/1447, web 189/189 passing. Turbo concurrency capped at 2 (package.json) to prevent OOM under parallel test runs. Co-Authored-By: claude-flow <ruv@ruv.net>
343 lines
16 KiB
Markdown
343 lines
16 KiB
Markdown
# CapaKraken — Umsetzungsplan
|
||
|
||
Gitea-Repo: `https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY`
|
||
Stand: 2026-04-01 | Issues: #19–#26
|
||
|
||
---
|
||
|
||
## Plan: In-Review-Tickets verifizieren und in main integrieren
|
||
|
||
### Anforderungsanalyse
|
||
|
||
8 Tickets sind `in-review`. Ziel: jeden Ticket-Scope im Code verifizieren, Gitea-Kommentar
|
||
mit Bewertung (✅ akzeptiert / ⚠️ Nachbesserung nötig) hinterlassen, dann alle offenen
|
||
Changes in einem sauberen Commit auf `main` integrieren.
|
||
|
||
**Umfang der Verifikation pro Ticket:**
|
||
- Code-Artefakte im Repo prüfen (Dateien, Tests)
|
||
- Quality-Gates (tsc, test:unit) müssen grün sein
|
||
- Akzeptanzkriterien aus dem Ticket-Body abgleichen
|
||
- Gitea-Kommentar: Strukturiertes Urteil mit Befunden
|
||
|
||
**Integration-Strategie:**
|
||
- Alle ungestagten Änderungen (`git status`) gehören zu den Tickets #20, #22, #25, #26
|
||
- Ein einziger Commit auf `main` mit klarer Commit-Message
|
||
- plan.md und docs/ mitcommiten
|
||
|
||
### Betroffene Pakete & Dateien
|
||
|
||
| Paket | Dateien | Art der Änderung |
|
||
|-------|---------|-----------------|
|
||
| `apps/web` | `next.config.ts`, `src/app/layout.tsx`, `src/middleware.ts`, `src/middleware.test.ts`, `src/app/api/perf/route.ts`, `src/app/api/perf/route.test.ts`, `src/components/security/MfaSetup.test.ts`, `e2e/dev-system/rbac-data-access.spec.ts`, `e2e/dev-system/helpers.ts` | Verifikation + commit |
|
||
| `packages/api` | `src/__tests__/ssrf-guard.test.ts`, `src/__tests__/webhook-procedure-support.test.ts` | Verifikation + commit |
|
||
| root | `package.json`, `docker-compose.yml`, `docs/developer-runbook.md`, `plan.md` | Verifikation + commit |
|
||
|
||
### Task-Liste (atomare Schritte in Reihenfolge)
|
||
|
||
**Phase 1 — Verifikation & Kommentierung (sequenziell, ein Ticket nach dem anderen)**
|
||
|
||
- [ ] **V-1:** Ticket #19 (MFA-QR) — Code-Check: `MfaSetup.tsx` + `MfaSetup.test.ts`; Urteil auf Gitea posten
|
||
- [ ] **V-2:** Ticket #20 (Webhook SSRF) — Code-Check: `ssrf-guard.ts` + `ssrf-guard.test.ts` + `webhook-procedure-support.test.ts`; Urteil posten
|
||
- [ ] **V-3:** Ticket #21 (/api/perf) — Code-Check: `route.ts` + `route.test.ts`; Urteil posten
|
||
- [ ] **V-4:** Ticket #22 (CSP) — Code-Check: `middleware.ts` + `middleware.test.ts` + `next.config.ts` + `layout.tsx`; Urteil posten
|
||
- [ ] **V-5:** Ticket #23 (Active-Session-Registry) — Code-Check: Session-Guard-Middleware, Auth-Flow; Urteil posten
|
||
- [ ] **V-6:** Ticket #24 (Docker reproducibility) — Code-Check: `docker-compose.yml`, Dockerfile.dev; Urteil posten
|
||
- [ ] **V-7:** Ticket #25 (Env/Migration-Strategie) — Code-Check: `docker-compose.yml`, `docs/developer-runbook.md`; Urteil posten
|
||
- [ ] **V-8:** Ticket #26 (RBAC E2E-Tests) — Code-Check: `rbac-data-access.spec.ts`; Urteil posten
|
||
|
||
**Phase 2 — Quality Gates**
|
||
|
||
- [ ] **Q-1:** `pnpm --filter @capakraken/web exec tsc --noEmit` — keine Fehler
|
||
- [ ] **Q-2:** `pnpm --filter @capakraken/api test:unit` — alle Tests grün
|
||
- [ ] **Q-3:** `pnpm --filter @capakraken/web test:unit` — alle Tests grün
|
||
|
||
**Phase 3 — Git-Integration**
|
||
|
||
- [ ] **G-1:** `git add` aller ungestagten Änderungen (alle Ticket-Artefakte)
|
||
- [ ] **G-2:** Commit mit Message: `security/platform: close audit findings #19–#26 (tests, CSP nonce, SSRF guard, runbook)`
|
||
- [ ] **G-3:** `git push origin main`
|
||
|
||
### Abhängigkeiten
|
||
|
||
- V-1 bis V-8 sind unabhängig voneinander, aber sequenziell (ein Kommentar pro Ticket)
|
||
- Q-1 bis Q-3 können nach allen V-Tasks parallel laufen
|
||
- G-1/G-2/G-3 müssen nach Q-1..3 erfolgen (kein Commit bei roten Tests)
|
||
|
||
### Akzeptanzkriterien
|
||
|
||
- [ ] Alle 8 in-review-Tickets haben einen Bewertungskommentar
|
||
- [ ] `pnpm test:unit` läuft grün (beide Pakete)
|
||
- [ ] `tsc --noEmit` ohne Fehler
|
||
- [ ] Alle neuen Dateien in einem sauberen Commit auf `main`
|
||
- [ ] `git status` danach: working tree clean
|
||
|
||
### Risiken & offene Fragen
|
||
|
||
- **Ticket #23 (Active-Session):** Die Implementierung in früheren Sessions könnte durch spätere Auth-Fixes (jti→sid) überholt worden sein — genau prüfen
|
||
- **Ticket #24 vs. #25:** Überlappen in `docker-compose.yml` — beide Tickets betreffen dieselbe Datei; ein Commit deckt beide ab
|
||
- **Push auf main:** Direkt auf `main` — kein PR da kein Remote-Review-Prozess konfiguriert. Sicherstellen dass alle Tests grün sind
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Ticket #25 — Docker/Env/Migration-Strategie
|
||
|
||
### Anforderungsanalyse
|
||
|
||
Ziel: Docker-Container-Lifecycle ohne manuelle Eingriffe.
|
||
|
||
Konkrete Mängel:
|
||
1. `REDIS_URL` in `docker-compose.yml` nutzt `${REDIS_URL:-redis://redis:6379}` — Host-Env-Var kann Docker-internen Wert überschreiben (gleiche Klasse wie das behobene `DATABASE_URL`-Problem)
|
||
2. Migration-Strategy undokumentiert: DB per `db push` aufgebaut, dann Migration hinzugefügt → `migrate deploy` scheitert mit P3005, erforderte `migrate resolve --applied`
|
||
3. Kein Developer-Runbook (Setup, Restart, DB-Ops fehlen)
|
||
|
||
### Betroffene Dateien
|
||
|
||
| Datei | Änderung |
|
||
|---|---|
|
||
| `docker-compose.yml` | `REDIS_URL` hardcoden |
|
||
| `docs/developer-runbook.md` | create |
|
||
|
||
### Task-Liste
|
||
|
||
- [ ] **#25-T1:** `docker-compose.yml` — `REDIS_URL: redis://redis:6379` (Literal, kein `${}`)
|
||
- [ ] **#25-T2:** `docs/developer-runbook.md` erstellen mit: Erstmaligem Setup, DB-Migration-Strategie inkl. P3005-Recovery, E2E_TEST_MODE-Erklärung, Container-Neustart-Checkliste
|
||
|
||
---
|
||
|
||
## Ticket #26 — RBAC Datenzugriffs-Matrix E2E-Tests
|
||
|
||
### Anforderungsanalyse
|
||
|
||
Neue Testdatei `apps/web/e2e/dev-system/rbac-data-access.spec.ts` mit **Netzwerk-Ebene** tRPC-Response-Assertions (nicht nur UI-Sichtbarkeit).
|
||
|
||
Grundlage `docs/route-access-matrix.md`:
|
||
|
||
| tRPC-Prozedur | Audience | Admin | Manager | Viewer |
|
||
|---|---|---|---|---|
|
||
| `user.list` | `admin-only` | ✓ 200 | FORBIDDEN | FORBIDDEN |
|
||
| `allocation.listView` | `planning-read` | ✓ 200 | ✓ 200 | FORBIDDEN |
|
||
| `resource.listSummaries` | `resource-overview` | ✓ 200 | ✓ 200 | FORBIDDEN |
|
||
| `user.listAssignable` | `manager-write` | ✓ 200 | ✓ 200 | FORBIDDEN |
|
||
|
||
Technik: `page.evaluate()` mit `fetch()` gegen `/api/trpc/<proc>?batch=1&input=...` — läuft im Browser-Kontext der gespeicherten Session.
|
||
|
||
tRPC GET-Format:
|
||
```
|
||
GET /api/trpc/<proc>?batch=1&input={"0":{"json":null}}
|
||
Erfolg: [{"result":{"data":{"json":[...]}}}]
|
||
Fehler: [{"error":{"json":{"data":{"code":"FORBIDDEN","httpStatus":403}}}}]
|
||
```
|
||
|
||
### Betroffene Dateien
|
||
|
||
| Datei | Änderung |
|
||
|---|---|
|
||
| `apps/web/e2e/dev-system/rbac-data-access.spec.ts` | create |
|
||
|
||
### Task-Liste
|
||
|
||
- [ ] **#26-T1:** Datei erstellen mit `trpcQuery(page, procedure, input?)` Helper-Funktion
|
||
- [ ] **#26-T2:** Admin-Describe-Block (4 Tests: alle 4 Prozeduren → Erfolg erwartet)
|
||
- [ ] **#26-T3:** Manager-Describe-Block (4 Tests: `user.list` → FORBIDDEN, Rest → Erfolg)
|
||
- [ ] **#26-T4:** Viewer-Describe-Block (4 Tests: alle → FORBIDDEN)
|
||
- [ ] **#26-T5:** Smoke-Run: `pnpm exec playwright test e2e/dev-system/rbac-data-access.spec.ts --config=playwright.dev.config.ts`
|
||
|
||
### Risiken
|
||
|
||
- `allocation.listView` könnte ein Pflicht-Input-Objekt erfordern → Falls `BAD_REQUEST` statt FORBIDDEN, Schema im Router prüfen und minimalen Input übergeben
|
||
- Viewer-Permissions aus DB-Seed (SystemRoleConfig) — prüfen ob VIEWER tatsächlich kein `VIEW_PLANNING` hat
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Anforderungsanalyse
|
||
|
||
Alle 6 Issues stammen aus einem Security-Audit und einem Plattform-Review.
|
||
5 davon sind Security-Findings (OWASP A02/A05/A07/A09), 1 ist ein Plattform-Thema (Docker-Reproduzierbarkeit).
|
||
Die Security-Issues sind weitgehend unabhängig voneinander; lediglich #23 greift in den tRPC-Request-Pfad ein und sollte zuletzt umgesetzt werden, da es den Haupt-Auth-Pfad berührt.
|
||
|
||
---
|
||
|
||
## Betroffene Pakete & Dateien
|
||
|
||
| Issue | Paket/Pfad | Datei | Art |
|
||
|-------|-----------|-------|-----|
|
||
| #21 | `apps/web` | `src/app/api/perf/route.ts` | edit |
|
||
| #19 | `apps/web` | `src/components/security/MfaSetup.tsx` | edit |
|
||
| #19 | `apps/web` | `package.json` | edit (neue Dep: `qrcode` + `@types/qrcode`) |
|
||
| #20 | `packages/api` | `src/lib/webhook-dispatcher.ts` | edit |
|
||
| #20 | `packages/api` | `src/router/webhook-support.ts` | edit |
|
||
| #20 | `packages/api` | `src/lib/ssrf-guard.ts` | create |
|
||
| #22 | `apps/web` | `next.config.ts` | edit |
|
||
| #23 | `apps/web` | `src/app/api/trpc/[trpc]/route.ts` | edit |
|
||
| #23 | `apps/web` | `src/server/auth.ts` | edit (Doku/Cleanup) |
|
||
| #24 | root | `Dockerfile.dev`, `Dockerfile.prod`, `docker-compose.yml`, `docker-compose.prod.yml`, `tooling/docker/app-dev-start.sh` | edit |
|
||
|
||
---
|
||
|
||
## Task-Liste (in empfohlener Reihenfolge)
|
||
|
||
### Issue #21 — /api/perf fail-closed + Query-Token entfernen
|
||
|
||
- [ ] **Task 1:** `GET`-Handler in `apps/web/src/app/api/perf/route.ts` ändern:
|
||
- `if (cronSecret)` → `if (!cronSecret) return 401/403` (fail-closed)
|
||
- `queryToken`-Zweig vollständig entfernen
|
||
- Nur noch `Authorization: Bearer <secret>` prüfen
|
||
- → Datei: `apps/web/src/app/api/perf/route.ts`
|
||
|
||
- [ ] **Task 2:** Unit-Tests für `/api/perf`:
|
||
- Test: autorisiert per Header → 200
|
||
- Test: kein Secret → 401
|
||
- Test: Query-Param-Token → 401 (nicht mehr akzeptiert)
|
||
- Test: fehlende `CRON_SECRET`-Env → fail-closed (kein Metrics-Leak)
|
||
- → Datei: `apps/web/src/app/api/perf/route.test.ts` (neu)
|
||
|
||
---
|
||
|
||
### Issue #19 — MFA QR lokal rendern
|
||
|
||
- [ ] **Task 3:** `qrcode`-Paket und `@types/qrcode` zu `apps/web/package.json` hinzufügen, `pnpm install` ausführen.
|
||
|
||
- [ ] **Task 4:** `MfaSetup.tsx` umschreiben:
|
||
- `<img src="https://api.qrserver.com/...">` durch lokale QR-Generierung ersetzen
|
||
- `qrcode.toDataURL(uri)` im Client-Effekt aufrufen und als `<img src={dataUrl}>` rendern
|
||
- Sicherstellen: der `otpauth://`-URI verlässt den Browser nicht mehr
|
||
- → Datei: `apps/web/src/components/security/MfaSetup.tsx`
|
||
|
||
- [ ] **Task 5:** Test sicherstellen, dass kein Rendering-Request an externe QR-URL geht:
|
||
- Unit-Test oder Playwright-Test der prüft, dass kein `<img src>` mit `qrserver.com` oder `chart.googleapis.com` gerendert wird
|
||
- → Datei: `apps/web/src/components/security/MfaSetup.test.tsx` (neu oder bestehend ergänzen)
|
||
|
||
---
|
||
|
||
### Issue #20 — Webhook SSRF-Schutz
|
||
|
||
- [ ] **Task 6:** `ssrf-guard.ts` erstellen mit einer `assertWebhookUrlAllowed(url: string): void`-Funktion:
|
||
- Parst die URL, löst Hostname auf (DNS-Check via Node `dns.lookup`)
|
||
- Blockt: Loopback (`127.0.0.0/8`, `::1`), RFC1918 (`10.x`, `172.16–31.x`, `192.168.x`), Link-Local (`169.254.x`), Cloud-Metadata (`169.254.169.254`)
|
||
- Blockt: alle Schemes außer `https` (und `http` nur wenn expliziter Dev-Override gesetzt)
|
||
- Wirft `TRPCError({ code: "BAD_REQUEST" })` mit allgemeiner Fehlermeldung (ohne IP preiszugeben)
|
||
- → Datei: `packages/api/src/lib/ssrf-guard.ts`
|
||
|
||
- [ ] **Task 7:** `ssrf-guard` in `webhook-support.ts` und `webhook-dispatcher.ts` integrieren:
|
||
- Vor Speicherung + vor Dispatch `assertWebhookUrlAllowed(url)` aufrufen
|
||
- → Dateien: `packages/api/src/router/webhook-support.ts`, `packages/api/src/lib/webhook-dispatcher.ts`
|
||
|
||
- [ ] **Task 8:** Unit-Tests für `ssrf-guard.ts`:
|
||
- Erlaubt: `https://example.com/hook`
|
||
- Blockt: `http://localhost/…`, `http://127.0.0.1/…`, `http://10.0.0.1/…`, `http://192.168.1.1/…`, `http://169.254.169.254/…`, `ftp://…`
|
||
- → Datei: `packages/api/src/__tests__/ssrf-guard.test.ts` (neu)
|
||
|
||
---
|
||
|
||
### Issue #22 — CSP härten
|
||
|
||
- [ ] **Task 9:** CSP in `apps/web/next.config.ts` überarbeiten:
|
||
- `unsafe-eval` entfernen oder nur für `NODE_ENV === "development"` erlauben
|
||
- `unsafe-inline` aus `script-src` entfernen
|
||
- Nonce-basierte Inline-Scripts prüfen: Next.js 15 unterstützt CSP-Nonces via `nonce`-Prop auf `<Script>` — recherchieren ob tatsächliche Inline-Scripts existieren, die Nonces brauchen
|
||
- Unnötige `connect-src`-Origins bereinigen
|
||
- → Datei: `apps/web/next.config.ts`
|
||
|
||
- [ ] **Task 10:** Smoke-Test: App unter gehärteter CSP starten, Browser-DevTools auf CSP-Violations prüfen (mindestens Login → Dashboard → Timeline → Allocations).
|
||
Gefundene Violations entweder beheben (Move to external file / Nonce) oder als Known Exception dokumentieren.
|
||
|
||
---
|
||
|
||
### Issue #23 — Active-Session-Registry bei jedem Request prüfen
|
||
|
||
- [ ] **Task 11:** Im tRPC-Route-Handler (`apps/web/src/app/api/trpc/[trpc]/route.ts`) nach dem `auth()`-Call die `jti` aus dem Session-Token lesen und gegen `prisma.activeSession` validieren:
|
||
```ts
|
||
const jti = session?.user?.jti as string | undefined;
|
||
if (jti) {
|
||
const active = await prisma.activeSession.findUnique({ where: { jti } });
|
||
if (!active) {
|
||
return NextResponse.json({ error: "Session revoked" }, { status: 401 });
|
||
}
|
||
}
|
||
```
|
||
- Gilt für alle authentisierten tRPC-Requests
|
||
- Gilt auch für nicht-tRPC Auth-Pfade wenn vorhanden (Route-Handler prüfen)
|
||
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.ts`
|
||
|
||
- [ ] **Task 12:** Unit-/Integrations-Tests:
|
||
- gültige aktive Session → Request durch
|
||
- ausgeloggte (gelöschte) Session → 401
|
||
- durch Concurrent-Session-Limit verdrängte Session → 401
|
||
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.test.ts` (neu oder ergänzen)
|
||
|
||
---
|
||
|
||
### Issue #24 — Docker-Setup host-unabhängig
|
||
|
||
- [ ] **Task 13:** `Dockerfile.dev` prüfen und absichern:
|
||
- `pnpm install` muss im Container ablaufen (kein Volume-Mount der Host-`node_modules`)
|
||
- `prisma generate` muss Teil des Starts sein (nicht als Host-Voraussetzung)
|
||
- Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
|
||
- → Datei: `Dockerfile.dev`
|
||
|
||
- [ ] **Task 14:** `Dockerfile.prod` prüfen:
|
||
- Multi-Stage-Build: Build-Stage hat pnpm + alle Dev-Deps; Runtime-Stage nur Prod-Artefakte
|
||
- Generierte Prisma-Artefakte (`node_modules/.prisma`) korrekt aus Build-Stage kopiert
|
||
- → Datei: `Dockerfile.prod`
|
||
|
||
- [ ] **Task 15:** `docker-compose.yml` absichern:
|
||
- `node_modules`-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappen
|
||
- `app-dev-start.sh` ausführbar und alle Schritte (generate, migrate, start) enthalten
|
||
- → Dateien: `docker-compose.yml`, `tooling/docker/app-dev-start.sh`
|
||
|
||
- [ ] **Task 16:** Frischer-Checkout-Smoke-Test dokumentieren:
|
||
```bash
|
||
git clone … && cd capakraken
|
||
docker compose up --build
|
||
# → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten
|
||
```
|
||
→ Schritt in `docs/` oder `README.md` festhalten
|
||
|
||
---
|
||
|
||
## Abhängigkeiten
|
||
|
||
```
|
||
Task 1 → Task 2 (Tests setzen fertige Impl voraus)
|
||
Task 3 → Task 4 → Task 5 (Dep-Install → Impl → Test)
|
||
Task 6 → Task 7 → Task 8 (Guard-Lib → Integration → Tests)
|
||
Task 9 → Task 10 (CSP-Änderung → Smoke-Test)
|
||
Task 11 → Task 12 (Session-Impl → Tests)
|
||
Task 13–16 unabhängig von allen anderen (Docker-only)
|
||
|
||
Parallel möglich (nach Task-Gruppe):
|
||
- #21 (Tasks 1–2) || #19 (Tasks 3–5) || #24 (Tasks 13–16)
|
||
- #20 (Tasks 6–8) || #22 (Tasks 9–10) — erst nachdem #21/#19 fertig sind (separater Branch)
|
||
- #23 (Tasks 11–12) — zuletzt, da Auth-Pfad berührt
|
||
```
|
||
|
||
---
|
||
|
||
## Akzeptanzkriterien
|
||
|
||
- [ ] `pnpm test:unit` läuft grün
|
||
- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen Errors
|
||
- [ ] `pnpm lint` — sauber
|
||
- [ ] **#21:** `/api/perf` ohne `CRON_SECRET` gibt 401/403, Query-Token wird nicht mehr akzeptiert
|
||
- [ ] **#19:** `MfaSetup` macht keinen Request an `qrserver.com` oder `chart.googleapis.com`
|
||
- [ ] **#20:** Webhook-Save und Dispatch für `http://127.0.0.1/…` gibt `BAD_REQUEST`
|
||
- [ ] **#22:** Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
|
||
- [ ] **#23:** Request mit gelöschter `ActiveSession`-`jti` gibt 401
|
||
- [ ] **#24:** `docker compose up --build` auf sauberem Checkout bootet die App ohne Host-Abhängigkeiten
|
||
|
||
---
|
||
|
||
## Risiken & offene Fragen
|
||
|
||
| Risiko | Einschätzung | Maßnahme |
|
||
|--------|-------------|----------|
|
||
| CSP-Nonces in Next.js 15: `<Script>` und Tailwind CSS benötigen ggf. Nonces | Mittel | Vor Task 9 in Next.js-15-Doku recherchieren; ggf. nur `unsafe-eval` entfernen als erster Schritt |
|
||
| Session-Registry-Check (#23) erhöht DB-Load: jeder tRPC-Request = 1 DB-Read | Mittel | Redis-Cache mit kurzer TTL (30s) als Opt-in; erst messen ob nötig |
|
||
| SSRF-Guard DNS-Lookup: async, könnte Race-Condition durch DNS-Rebinding haben | Niedrig | Nach DNS-Lookup Socket-Verbindung ebenfalls gegen IP prüfen (defense-in-depth) |
|
||
| Docker #24: `node_modules`-Volume-Semantik bei pnpm-Workspaces komplex | Mittel | Symlink-Struktur von pnpm in Container testen; ggf. `--shamefully-hoist` Flag |
|
||
| `jti` im Session-Token: Auth.js-Version muss `jti` ins JWT schreiben | Offen | In `auth.ts` prüfen ob `token.jti` tatsächlich im JWT-Callback persistiert wird |
|