--- phase: quick-260921-i8x plan: 01 subsystem: security tags: [biome, lint, regex, ldap, cookie, jest, vitest, nextjs] requires: [] provides: - "safe-next.ts: FORBIDDEN_CHARS_RE beweisbar unveraendert, jetzt als Unicode-Escapes statt Rohbytes" - "Vier isNaN -> Number.isNaN Haertungen in tender-radar/dkv-fleet-Komponenten" - "Drei exec-Schleifen (dkv-parser.service.ts, dkv-parser.validate.ts, icon-discovery.service.ts) ohne Unterdrueckungskommentar umgeschrieben" - "escapeLdapFilterValue dokumentiert (unveraendert) als RFC-4515-Absicht" - "NEXT_LOCALE-Cookie mit SameSite=Lax" affects: [dkv-fleet-module, tender-radar-module, ldap-sync, i18n] actuals: tokens: 3691 tasks: 3 commits: 3 tech-stack: added: [] patterns: - "for (let m = re.exec(t); m !== null; m = re.exec(t)) statt while ((m = re.exec(t)) !== null) — vermeidet noAssignInExpressions ohne Unterdrueckungskommentar" - "biome-ignore unmittelbar ueber der beanstandeten Zeile, ohne Leerzeile — sonst bleibt der Befund UND es kommt suppressions/unused hinzu" key-files: created: - apps/api/src/dkv/dkv-parser.service.spec.ts - apps/web/src/components/locale-switcher.test.tsx modified: - apps/web/src/lib/safe-next.ts - apps/web/src/lib/safe-next.test.ts - apps/api/src/dkv/dkv-parser.service.ts - apps/api/src/dkv/dkv-parser.validate.ts - apps/api/src/favorites/icon-discovery.service.ts - apps/api/src/ldap/ldap.service.ts - apps/web/src/components/locale-switcher.tsx - "apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx" - "apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx" - "apps/web/src/app/(portal)/modules/dkv-fleet/components/InvoiceHistoryTable.tsx" - "apps/web/src/app/(portal)/modules/cert-manager/actions.ts" key-decisions: - "Sieben der zwoelf Befunde waren keine Fehler (vier isNaN, ein forEach-Rueckgabewert, drei exec-Schleifen) — als gleichwertig/Absicht dokumentiert statt stillschweigend zu Fehlern erklaert" - "escapeLdapFilterValue in ldap.service.ts bleibt zeichengleich — die NUL-Maskierung ist RFC-4515-Pflicht, kein Steuerzeichen-Bug" - "Zwei-Zeichen-Praefixtest und Schema-Liste in safe-next.ts unangetastet — nur die Regex-Zeile und zwei Kommentare geaendert" requirements-completed: [D-01, D-02, D-03, D-04, D-05, D-06] coverage: - id: D1 description: "safe-next.ts: Steuerzeichen als Escapes statt Rohbytes, Zeichenmenge beweisbar unveraendert (54 Codepunkte ueber alle 65536 aus U+0000..U+FFFF)" requirement: "D-01" verification: - kind: unit ref: "apps/web/src/lib/safe-next.test.ts#Test A: die abgewiesene Zeichenmenge ist und bleibt genau diese 54 Codepunkte" status: pass - kind: unit ref: "apps/web/src/lib/safe-next.test.ts#Test B: die Datei enthaelt kein rohes Steuerzeichen" status: pass human_judgment: false - id: D2 description: "Vier isNaN -> Number.isNaN Haertungen (TenderDetail x2, ResultsList, InvoiceHistoryTable)" verification: - kind: unit ref: "pnpm --filter @tessera/web test (67 -> 68 Dateien, 480 -> 481 Tests bleiben gruen)" status: pass - kind: other ref: "biome lint JSON category-Zaehlung: lint/suspicious/noGlobalIsNan == 0" status: pass human_judgment: false - id: D3 description: "cert-manager/actions.ts: forEach-Rueckgabewert verworfen (useIterableCallbackReturn)" verification: - kind: other ref: "biome lint JSON category-Zaehlung: lint/suspicious/useIterableCallbackReturn == 0" status: pass - kind: integration ref: "pnpm --filter @tessera/web test — form.append-Reihenfolge unveraendert, keine Regression" status: pass human_judgment: false - id: D4 description: "Drei exec-Schleifen (dkv-parser.service.ts, dkv-parser.validate.ts, icon-discovery.service.ts) auf for-Kopf-Form umgeschrieben, verhaltensgleich" requirement: "D-03" verification: - kind: unit ref: "apps/api/src/dkv/dkv-parser.service.spec.ts#liefert zwei Fahrzeugbloecke sowie Rechnungsnummer und -datum aus dem Beispieltext" status: pass - kind: other ref: "node --experimental-strip-types apps/api/src/dkv/dkv-parser.validate.ts gegen invoice.pdf: 27 Fahrzeugbloecke/66 Transaktionen unveraendert" status: pass - kind: unit ref: "apps/api/src/favorites/icon-discovery.service.spec.ts (19 Tests, alle gruen)" status: pass human_judgment: false - id: D5 description: "escapeLdapFilterValue dokumentiert als RFC-4515-Absicht, Funktion zeichengleich" requirement: "D-03" verification: - kind: other ref: "git diff apps/api/src/ldap/ldap.service.ts zeigt ausschliesslich eine hinzugefuegte Kommentarzeile" status: pass human_judgment: false - id: D6 description: "NEXT_LOCALE-Cookie mit SameSite=Lax gehaertet" requirement: "D-03" verification: - kind: unit ref: "apps/web/src/components/locale-switcher.test.tsx#schreibt beim Klick genau ein Cookie mit NEXT_LOCALE, path=/, max-age=31536000 und SameSite=Lax" status: pass human_judgment: false - id: D7 description: "Biome-Warnungen 446 -> 434, keine neue Fundstelle, keine neue Abhaengigkeit" requirement: "D-06" verification: - kind: other ref: "npx biome lint --reporter=json . -> total 434, sev {warning:433, info:1}, suppressions/unused 0" status: pass - kind: other ref: "git diff -- package.json '*/package.json' pnpm-lock.yaml (leer)" status: pass human_judgment: false duration: 35min completed: 2026-09-21 status: complete --- # Quick 260921-i8x: Fehlerverdaechtige Lint-Klassen (Steuerzeichen & Co.) Summary **Zwoelf Befunde aus fuenf Regelklassen einzeln beurteilt: drei echte Haertungen umgesetzt (safe-next.ts-Escapes, Number.isNaN, SameSite=Lax), sieben als gleichwertig/Absicht dokumentiert (vier isNaN, ein forEach, drei exec-Schleifen), zwei als bewusste Nicht-Fixes belassen (LDAP-NUL-Maskierung, Praefixtest) — Biome-Warnungen 446 -> 434, kein Verhalten aendert sich ausser an den drei nachgewiesenen Haertungsstellen.** ## Performance - **Duration:** ca. 35 min - **Tasks:** 3/3 abgeschlossen - **Files modified:** 13 (11 geaendert, 2 neu) ## Urteilstabelle (aus `` des Plans, mit tatsaechlich Umgesetztem) | # | Datei | Regel | Urteil | Umgesetzt | |---|---|---|---|---| | 1 | `apps/web/src/lib/safe-next.ts` (2 Befunde) | `noControlCharactersInRegex` | **Haertung** | Regex-Zeile auf Unicode-Escapes umgestellt, Kommentar korrigiert (die frühere Behauptung, Escapes seien "am Edge" noetig, war falsch), `biome-ignore` unmittelbar ueber der Zeile gesetzt. Zeichenmenge ueber alle 65536 Codepunkte als identisch nachgewiesen (54 Codepunkte, Bitmap-SHA-256 unveraendert). | | 2 | `apps/api/src/ldap/ldap.service.ts:1662` | `noControlCharactersInRegex` | **Absicht** | Keine Aenderung an der Funktion — nur `biome-ignore` mit Begruendung (RFC 4515, LDAP-Filter-Injection-Risiko) ergaenzt. `escapeLdapFilterValue` ist zeichengleich zur Ausgangsfassung. | | 3 | `apps/web/src/app/(portal)/modules/cert-manager/actions.ts:154` | `useIterableCallbackReturn` | **gleichwertig** | Pfeilfunktion mit Kurzform-Rumpf auf Block-Rumpf umgestellt; `forEach` verwarf den Rueckgabewert ohnehin. Reine Lesbarkeitsaenderung, kein Verhaltensunterschied. | | 4-7 | `TenderDetail.tsx` (2x), `InvoiceHistoryTable.tsx`, `ResultsList.tsx` | `noGlobalIsNan` | **gleichwertig** | `isNaN(d.getTime())` auf `Number.isNaN(d.getTime())` umgestellt. `getTime()` liefert immer `number`, die Umwandlung fand nie statt — der Tausch ist Haertung gegen eine zukuenftige Aenderung, kein Fehler heute. | | 8-10 | `dkv-parser.service.ts:104`, `dkv-parser.validate.ts:139`, `icon-discovery.service.ts:182` | `noAssignInExpressions` | **Absicht** | `while ((m = re.exec(t)) !== null)` auf `for (let m = re.exec(t); m !== null; m = re.exec(t))` umgeschrieben — verhaltensgleich, kein Unterdrueckungskommentar mehr noetig. Keine der drei Variablen wird nach der Schleife gelesen. | | 11 | `apps/web/src/components/locale-switcher.tsx:15` | `noDocumentCookie` | **Haertung** | `SameSite=Lax` ergaenzt (`path=/` und `max-age` waren bereits korrekt — kein Persistenzdefekt). `biome-ignore` dokumentiert, warum `document.cookie` bleibt: die Cookie-Store-API fehlt in WebKit/Tauri auf Linux und macOS. | **Sieben der zwoelf Befunde waren keine Fehler** — vier `isNaN`-Stellen, eine `forEach`-Stelle und drei `exec`-Schleifen sind als gleichwertig bzw. Absicht ausgewiesen, nicht nachtraeglich zu Bugs erklaert. Zwei weitere (LDAP-NUL-Maskierung, Praefixtest in `safe-next.ts`) sind bewusste Nicht-Fixes: an ihnen wurde nichts geaendert. ## Vorher-/Nachher-Zahlen (aus ``) | Pruefung | Erwartet | Gemessen | |---|---|---| | `pnpm lint` | 5/5 | **5 successful, 5 total** | | Biome JSON-Zaehlung: total / sev | 434 / `{warning:433, info:1}` | **434 / `{'info': 1, 'warning': 433}`** | | `noControlCharactersInRegex` | 0 | **0** | | `useIterableCallbackReturn` | 0 | **0** | | `noGlobalIsNan` | 0 | **0** | | `noAssignInExpressions` | 4 (nur Testdateien) | **4** | | `noDocumentCookie` | 17 (nur Testdateien) | **17** | | `suppressions/unused` | 0 | **0** | | `pnpm type-check` | 4/4 | **4 successful, 4 total** | | `pnpm --filter @tessera/web test` | >= 68 Dateien, >= 480 Tests | **68 Dateien, 481 Tests, alle passed** | | `pnpm --filter @tessera/api test` | 72 Dateien, >= 1137 Tests | **72 Dateien, 1137 Tests, alle passed** | | Byte-Scan `safe-next.ts` | `CTRL_BYTES 0 []` | **`CTRL_BYTES 0 []`** | | `git status --porcelain -- apps packages` | genau die 13 Dateien aus `files_modified` | **bestaetigt (11 geaendert + 2 neu, keine weitere)** | | `git diff -- package.json '*/package.json' pnpm-lock.yaml` | leer | **leer** | Baseline bei Planungsbeginn (selbst nachgemessen, identisch zur Plan-Messung): 446 Diagnosen gesamt (445 warning + 1 info), `noControlCharactersInRegex` 3, `useIterableCallbackReturn` 1, `noGlobalIsNan` 4, `noAssignInExpressions` 7, `noDocumentCookie` 18, `suppressions/unused` 0. ## Task Commits Jeder Task wurde atomar committet: 1. **Task 1: safe-next.ts — Steuerzeichen als Escapes, Zeichenmenge beweisbar unveraendert** - `f85c91b` (fix), inkl. Plan-Datei 2. **Task 2: isNaN und forEach — vier plus ein gleichwertiger Tausch** - `076ca4b` (fix) 3. **Task 3: exec-Schleifen, LDAP-NUL-Maskierung und das Sprachumschalter-Cookie** - `b92dd5d` (fix) ## Files Created/Modified - `apps/web/src/lib/safe-next.ts` - FORBIDDEN_CHARS_RE auf Unicode-Escapes, Kommentar korrigiert, biome-ignore gesetzt - `apps/web/src/lib/safe-next.test.ts` - Test A (54-Codepunkte-Zusicherung), Test B (Byte-Scan), Test C (Praefixtest) ergaenzt - `apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx` - zwei `isNaN` -> `Number.isNaN` - `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` - `isNaN` -> `Number.isNaN` - `apps/web/src/app/(portal)/modules/dkv-fleet/components/InvoiceHistoryTable.tsx` - `isNaN` -> `Number.isNaN` - `apps/web/src/app/(portal)/modules/cert-manager/actions.ts` - `forEach`-Callback auf Block-Rumpf - `apps/api/src/dkv/dkv-parser.service.ts` - exec-Schleife auf for-Kopf-Form - `apps/api/src/dkv/dkv-parser.service.spec.ts` - neu: Rueckfall-Tor fuer `parseDkvText` (zwei Fahrzeugbloecke, Rechnungsnummer/-datum) - `apps/api/src/dkv/dkv-parser.validate.ts` - exec-Schleife auf for-Kopf-Form (Stand-alone-Skript) - `apps/api/src/favorites/icon-discovery.service.ts` - exec-Schleife auf for-Kopf-Form - `apps/api/src/ldap/ldap.service.ts` - biome-ignore-Kommentar ueber der RFC-4515-NUL-Maskierung, Funktion unveraendert - `apps/web/src/components/locale-switcher.tsx` - `SameSite=Lax` ergaenzt, biome-ignore gesetzt - `apps/web/src/components/locale-switcher.test.tsx` - neu: haelt die vollstaendige geschriebene Cookie-Zeichenkette fest ## Decisions Made - Die Erwartungsliste der 54 abgewiesenen Codepunkte in Test A wurde hart hinterlegt statt aus der Regel abgeleitet (D-01) — eine aus derselben Regel erzeugte Erwartung wuerde jede Aenderung der Regel mitmachen und bewiese damit nichts. - Alle drei `biome-ignore`-Kommentare in diesem Vorgang mussten als **einzeilige** Kommentare unmittelbar ueber der beanstandeten Zeile stehen — ein mehrzeiliger Kommentarblock wird von Biome nur in seiner letzten Zeile als Kommentar erkannt, nicht als Unterdrueckungsdirektive, und erzeugt zusaetzlich `suppressions/unused`. Das wurde bei `safe-next.ts` zunaechst falsch gemacht (dreizeiliger Kommentar) und beim ersten Testlauf korrigiert (siehe Deviations). - `escapeLdapFilterValue` bleibt zeichengleich (D-03) — nur ein Kommentar wurde ergaenzt, keine Zeile der Funktion geaendert. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] `biome-ignore`-Kommentar bei safe-next.ts musste einzeilig sein, nicht dreizeilig** - **Found during:** Task 1 - **Issue:** Der erste Entwurf des `biome-ignore`-Kommentars war ueber drei Zeilen umgebrochen. Biome erkennt nur die letzte dieser drei Zeilen als unmittelbar vor der Regex stehend, und diese Zeile beginnt nicht mit `biome-ignore` — die Unterdrueckung griff nicht, der urspruengliche Befund blieb bestehen UND `suppressions/unused` kam hinzu (`npx biome lint` meldete danach 2 statt 0 Befunde fuer die Datei). - **Fix:** Kommentar auf eine einzige Zeile mit der vollen Begruendung zusammengefasst, direkt ueber der Regex-Zeile ohne Leerzeile. - **Files modified:** apps/web/src/lib/safe-next.ts - **Verification:** `npx biome lint apps/web/src/lib/safe-next.ts` meldet danach 0 Warnungen. - **Committed in:** f85c91b (Teil des Task-1-Commits, kein separater Commit noetig — der Fehler wurde vor dem Commit gefunden) **2. [Rule 3 - Blocking] `import.meta.url` in Test B lieferte keine `file:`-URL in der Vitest/jsdom-Umgebung** - **Found during:** Task 1 - **Issue:** Der erste Entwurf von Test B nutzte `fileURLToPath(new URL('./safe-next.ts', import.meta.url))`; das schlug mit `TypeError: The URL must be of scheme file` fehl, bevor der eigentliche Byte-Scan ueberhaupt lief. - **Fix:** Umgestellt auf `join(import.meta.dirname, 'safe-next.ts')` (Node 24, verfuegbar). - **Files modified:** apps/web/src/lib/safe-next.test.ts - **Verification:** Test B laeuft danach korrekt rot (3 Bytes) vor dem Umbau und gruen (0 Bytes) danach. - **Committed in:** f85c91b **3. [Rule 3 - Blocking] Erste Commit-Nachricht (Task 1) enthielt rohe NUL-Bytes durch das Write-Werkzeug** - **Found during:** Commit von Task 1 - **Issue:** Genauso wie im Plan fuer safe-next.ts selbst gewarnt, wandelte das Write-Werkzeug die Unicode-Escape-Schreibweise fuer den Bereich U+0000 bis U+001F plus U+007F in der Commit-Nachricht (als Freitext, nicht als Code) still in die tatsaechlichen Steuerbytes um. `git commit` verweigerte den Commit mit "a NUL byte in commit log message not allowed". - **Fix:** Commit-Nachricht ohne `\uXXXX`-Schreibweise umformuliert (Bereichsangabe in Worten statt als Escape-Literal), vor dem Commit per Python auf verbleibende Steuerbytes geprueft. - **Files modified:** keine (nur die Commit-Nachricht betroffen, kein Code) - **Verification:** Python-Bytescan der Commit-Nachricht-Datei ergab `bad bytes []`, danach committete `git commit -F` fehlerfrei. - **Committed in:** f85c91b --- **Total deviations:** 3 auto-fixed (alle Rule 3 - Blocking, alle Werkzeugfallen ohne Codeauswirkung) **Impact on plan:** Keine Verhaltensaenderung, keine Ausweitung des Umfangs. Alle drei Abweichungen wurden vor dem jeweiligen Commit gefunden und behoben — kein fehlerhafter Zwischenstand wurde committet. ## Issues Encountered Keine ueber die drei oben dokumentierten Werkzeugfallen hinaus. Alle ``-Bloecke aus dem Plan wurden real ausgefuehrt (keiner musste umformuliert werden), alle Zielzahlen wurden exakt erreicht. ## User Setup Required None - keine externe Konfiguration erforderlich. ## Next Phase Readiness Alle zwoelf Befunde sind abgeschlossen und dokumentiert. `noArrayIndexKey` und `noNonNullAssertion` (zusammen 19+11 der verbliebenen 434 Warnungen) sind explizit **nicht** angefasst — sie gehoeren laut Auftrag in den naechsten Vorgang. `noUselessSwitchCase` bleibt der dokumentierte Nicht-Fix aus 260921-bi2. Keine Blocker. --- *Quick-Vorgang: 260921-i8x* *Completed: 2026-09-21* ## Self-Check: PASSED Alle 13 in `files_modified` genannten Dateien existieren auf der Platte. Alle drei Task-Commit-Hashes (`f85c91b`, `076ca4b`, `b92dd5d`) sind im lokalen Git-Verlauf gefunden. `commits: 3` in `actuals` per `git rev-list --count 287799a..HEAD` gemessen (kein narrativer Wert). Byte-Scan dieser SUMMARY-Datei selbst ergab 0 rohe Steuerbytes, nachdem ein durch das Write-Werkzeug verursachter Treffer (dieselbe `\uXXXX`-Falle wie in Task 1) gefunden und korrigiert wurde.