4 Commits

Author SHA1 Message Date
schalli cfba3c9532 docs(quick-260921-i8x): fuenf Fehlerklassen geprueft, kein echter Fehler darunter
Tessera CI/CD / Lint & Type Check (push) Successful in 54s
Tessera CI/CD / Tests (push) Successful in 1m35s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m10s
Zusammenfassung und STATE.md zum Quick-Vorgang 260921-i8x.

Zwoelf Stellen einzeln beurteilt: sieben gleichwertig oder Absicht,
zwei Haertungen, drei idiomatisch korrekt. Kein echter Defekt.

Die eine Stelle mit echtem Wert ist safe-next.ts, der Schutz gegen
Weiterleitung auf fremde Seiten. Der Kommentar behauptete Escapes,
die rohen Bytes zeigten eingebettete Steuerzeichen. Jetzt echte
Escapes; Gleichwertigkeit ueber alle 65536 Codepunkte nachgerechnet,
54 abgelehnte Zeichen, null Abweichung, vom Orchestrator unabhaengig
gegen ein eigenes Referenzmuster gegengeprueft.

Bewusst nicht angefasst: die NUL-Maskierung in ldap.service.ts nach
RFC 4515 - genau dieses Zeichen zu treffen ist ihr Zweck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:36:37 +02:00
schalli b92dd5dda1 fix(api,web): exec-Schleifen ohne Unterdrueckungskommentar umgeschrieben, LDAP-NUL-Maskierung und Sprachcookie-SameSite dokumentiert/gehaertet
Drei while ((m = re.exec(t)) !== null)-Schleifen (dkv-parser.service.ts,
dkv-parser.validate.ts, icon-discovery.service.ts) sind die korrekte
Standardform fuer globale Regexe - kein verrutschtes "=". Umgeschrieben auf
eine verhaltensgleiche for-Schleife, die ohne noAssignInExpressions-
Unterdrueckung auskommt: Zuweisung wandert in Initialisierung und
Fortschaltung der for-Schleife, Bedingung prueft weiterhin auf null.
Abfolge der exec-Aufrufe, lastIndex-Fortschritt und Rumpfinhalte
unveraendert. Neue Spezifikation dkv-parser.service.spec.ts deckt
parseDkvText erstmals eigenstaendig ab (zwei Fahrzeugbloecke, Rechnungsnummer
und -datum aus einer gemockten pdf-parse-Attrappe) - das Rueckfall-Tor fuer
diesen Umbau. dkv-parser.validate.ts bleibt bei 27 Fahrzeugbloecken/66
Transaktionen gegen die reale invoice.pdf identisch.

LdapService.escapeLdapFilterValue bleibt zeichengleich: der NUL-Treffer in
der Regel ist die von RFC 4515 vorgeschriebene \00-Maskierung, kein Fehler.
Ein biome-ignore-Kommentar dokumentiert das, statt die Funktion zu aendern.

locale-switcher.tsx setzt jetzt SameSite=Lax auf dem NEXT_LOCALE-Cookie -
path=/ und max-age waren bereits korrekt, es lag also kein Persistenzdefekt
vor. Ohne SameSite haengt die Uebertragung am Browservorgabewert statt an
einer Festlegung. Neue Spezifikation locale-switcher.test.tsx haelt die
vollstaendige geschriebene Cookie-Zeichenkette fest.

Biome-Warnungen 446 -> 434 (noControlCharactersInRegex/useIterableCallbackReturn/
noGlobalIsNan auf 0, noAssignInExpressions auf 4 und noDocumentCookie auf 17 -
beide Reste ausschliesslich in Testdateien, suppressions/unused auf 0).

Quick-Vorgang 260921-i8x, Task 3/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:32:31 +02:00
schalli 076ca4bf64 fix(web): Number.isNaN statt globalem isNaN in vier Date-Pruefungen, forEach-Rueckgabewert verworfen
Alle vier Stellen (TenderDetail, ResultsList, InvoiceHistoryTable) rufen
isNaN(d.getTime()) auf - Date.prototype.getTime() liefert immer number,
also findet an keiner der vier Stellen tatsaechlich eine Umwandlung statt.
Der Tausch auf Number.isNaN ist reine Haertung: sobald dort einmal ein
String ankaeme, wuerde globales isNaN ihn stillschweigend umwandeln statt
ihn als kaputte Eingabe zu erkennen. Kein Rueckfallwert geaendert.

In cert-manager/actions.ts gibt files.forEach jetzt keinen Wert mehr aus
der Schleifenfunktion heraus - forEach verwirft ihn ohnehin, die Aenderung
ist rein kosmetisch (useIterableCallbackReturn). Reihenfolge der
form.append-Aufrufe unveraendert.

Quick-Vorgang 260921-i8x, Task 2/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:28:07 +02:00
schalli f85c91be01 fix(web): Steuerzeichen in safe-next.ts als Unicode-Escapes statt Rohbytes
FORBIDDEN_CHARS_RE in sanitizeNextPath bestand aus drei rohen Steuerbytes
(NUL, US, DEL) statt Escapes - jeder Editor, Formatierer oder Minifier in
der Kette kann solche Bytes stillschweigend verschlucken. Umgeschrieben auf
Unicode-Escapes fuer den Bereich U+0000 bis U+001F und U+007F. Zeichenmenge
ueber alle 65536 Codepunkte aus U+0000 bis U+FFFF als unveraendert
nachgewiesen (54 abgewiesene Codepunkte, Bitmap-SHA-256
3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64 vor und
nach dem Umbau identisch). Kommentar korrigiert: die vorherige Behauptung,
Escapes seien "am Edge" noetig, war falsch - zwischen den beiden
Escape-Schreibweisen gibt es zur Laufzeit keinen Unterschied.

Quick-Vorgang 260921-i8x, Task 1/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:26:25 +02:00
16 changed files with 1005 additions and 16 deletions
+5 -4
View File
@@ -4,8 +4,8 @@ milestone: v1.2
current_phase: 18
current_phase_name: desktop-client-fertigstellen
status: verified
stopped_at: "Fuenf Quick-Vorgaenge am 2026-09-21 abgeschlossen und verifiziert (9ie, a1d, bi2, fi3, gof). Naechste Fehlerklassen auf Zuruf des Nutzers: Listenschluessel per Positionsnummer (19), Ausrufezeichen-Zusicherungen (11), isNaN statt Number.isNaN (4) und Kleinkram (~9). Danach kommen zwei neue Widgets. Der gof-Vorgang ist noch nicht gepusht."
last_updated: "2026-09-21T11:05:00.000Z"
stopped_at: "Sechs Quick-Vorgaenge am 2026-09-21 abgeschlossen (9ie, a1d, bi2, fi3, gof, i8x). Offen als naechste Fehlerklasse: Listenschluessel per Positionsnummer (19 im Quellcode) und Ausrufezeichen-Zusicherungen (11 im Quellcode). Danach kommen zwei neue Widgets. i8x ist noch nicht gepusht."
last_updated: "2026-09-21T12:00:00.000Z"
last_activity: 2026-09-21
last_activity_desc: Quick 260921-9ie, a1d, bi2, fi3 und gof — Lint-Tor scharf, Benutzerverwaltung meldet abgewiesene Aktionen, Lint-Rueckstand 2923 → 446, erzwungener Passwortwechsel an der API durchgesetzt (war eine tote Sperre), 21 Effekt-Abhaengigkeiten einzeln beurteilt; alle fuenf verifiziert, die letzten drei am laufenden System
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
Plan: 6 of 6
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
Last activity: 2026-09-21 - Quick 260921-gof: 21 Effekt-Abhaengigkeiten einzeln beurteilt (15 Fallen, 2 echte Defekte, 3 Absicht, 1 Ballast); Kalender-Falle haette jeden Druck auf den Monatstitel bis zum Exchange-Server durchschlagen lassen. Im Browser nachgemessen: keine Schleife auf zehn Ansichten, Stoppuhr echtzeitgetreu
Last activity: 2026-09-21 - Quick 260921-i8x: fuenf fehlerverdaechtige Lint-Klassen geprueft, kein echter Fehler darunter; Weiterleitungsschutz safe-next.ts von rohen Steuerbytes auf Escapes gehaertet, Gleichwertigkeit ueber alle 65536 Codepunkte nachgerechnet (54 abgelehnte Zeichen, null Abweichung)
Progress: [██████████] 99%
@@ -449,6 +449,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| 260921-bi2 | **Lint-Rueckstand abgebaut: 2923 → 465 Warnungen (WINDOWS #35 Folgearbeit).** Seit das Lint-Tor wirklich prueft, war der Rueckstand sichtbar. Aufgeteilt nach Risiko statt nach Datei: (1) Konfiguration — zwei begruendete `overrides`, (2) maschinelle Fixes + toter Code, (3) Barrierefreiheit von Hand. **Der wichtigste Befund ist ein Beinahe-Schaden:** Biomes `style/useImportType`-Korrektur ist als *safe* eingestuft, zerstoert in `apps/api` aber die NestJS-Abhaengigkeitsspritze — `__metadata("design:paramtypes", [PrismaService, …])` kollabiert zu `[Function, …]`, 61 von 65 Dateien betroffen, API startet nicht mehr. Dabei bleibt `tsc` gruen **und alle 1124 API-Tests bleiben gruen**, weil kein einziger Test `createTestingModule` aufruft — das waere durch jedes vorhandene Tor unbemerkt bis auf alpha durchgelaufen. Planer und Plan-Pruefer haben es unabhaengig voneinander reproduziert (Datei kompiliert, Metadatenzeile verglichen). Deshalb zweiter `overrides`-Eintrag auf `apps/api/**`. Zweite Falle, ebenfalls gemessen: `--only=<regel>` schaltet eine in der Konfiguration abgeschaltete Regel wieder AN — ein repo-weites `biome lint . --only=useImportType --write` haengt die Ausnahme aus (77 API-Dateien veraendert). Nur pfadgebundene Aufrufe. **Nachweis, dass sich nichts geaendert hat, ist NICHT die Testsuite**, sondern ein sha256 ueber alle 593 erzeugten `__metadata`-Zeilen: `6e1583f1…`, vor und nach dem Umbau identisch, dreifach geprueft. Barrierefreiheit: 155 Handkorrekturen in 53 Dateien (Symbole 71, Knopf-Typen 52, Beschriftungen 22, Rollen/Semantik 10) — je Fundstelle entschieden, ob ein Symbol dekorativ (`aria-hidden`) oder die einzige Beschriftung ist (`<title>`/`aria-label`); in der Seitenleiste erkannt, dass der Text beim Einklappen verschwindet, dort also ein echter Name noetig ist. Alle neuen Texte ueber next-intl in de **und** en (892/892 Schluessel). **Zahlen:** gesamt 2923 → 465, echter Quellcode 856 → 386, Testdateien 2067 → 79, Fehler-Rang durchgehend 0. **Bewusst NICHT angefasst, benannt statt stillschweigend:** 288 `noExplicitAny` im Quellcode (echte Typarbeit), 20 `useExhaustiveDependencies` (je ein moeglicher Effekt-Fehler), 30 a11y-Befunde mit Bedienentscheidungsbedarf, `noUselessSwitchCase` (Fallmarke dokumentiert Absicht), sowie fuenf tote Stellen, die Symptome echter Luecken sind — darunter: Passwortwechsel-Seite leitet nach erzwungenem Wechsel nicht weiter, Loeschknopf in `VehicleTable` ohne Besetztzustand, `force-password-change.interceptor` liest das HTTP-Verfahren und fragt es nie ab. **Klicktest am laufenden System** (echte Abbilder, Playwright): API meldet `healthy` und `Nest application successfully started` — Abhaengigkeitsspritze zur Laufzeit bewiesen; `Abbrechen` legt nichts an, `Speichern` legt an; als ADMIN bietet die SUPER_ADMIN-Zeile nur noch `Details`; abgewiesene Server-Antwort erscheint sichtbar als "Der Server hat die Aktion abgelehnt: …". Testbenutzer wieder geloescht. Verifikation passed. | 2026-09-21 | 8d1c8f3,636fe0d,+11 | [260921-bi2-lint-rueckstand-abbauen-mechanische-fixe](./quick/260921-bi2-lint-rueckstand-abbauen-mechanische-fixe/) |
| 260921-fi3 | **Erzwungener Passwortwechsel wurde an der API nie durchgesetzt — Sicherheitsfix.** Aus dem Lint-Durchlauf 260921-bi2 kamen fuenf gemeldete "Symptome". Alle am laufenden System nachgestellt: eines widerlegt (Passwortwechsel-Seite leitet sehr wohl weiter, siehe bi2-VERIFICATION), drei bestaetigt, eines (ZIP-Dateiname) bewusst nicht angefasst. **Der schwere Befund:** `auth.service.ts:176` legt `mustChangePassword` in den JWT, `jwt.strategy.ts` liess das Feld beim Auspacken fallen, also war `request.user.mustChangePassword` immer `undefined` und der global registrierte `ForcePasswordChangeInterceptor` eine Attrappe — er hat seit seiner Einfuehrung nie etwas blockiert. Durchgesetzt wurde der Zwangswechsel allein von der Web-Middleware; jeder Weg daran vorbei (Desktop-App, Skript, curl) umging ihn. Der Kommentar des Interceptors behauptete woertlich "T-02-14: Prevents bypass via direct API access" — das war falsch. Kein Rechteausbau: die eigene Rolle bleibt, aber der Zwang entfaellt. **Gemessen vorher:** Sitzung mit `mustChangePassword=true` bekam auf `GET /users` **200 samt vollstaendiger Benutzerliste**. **Behoben:** Strategie reicht das Feld durch (strikt `=== true`, fehlender Anspruch in alten Sitzungen wird `false`), Erlaubnisliste von Teilzeichenketten-Vergleich auf exaktes Verfahren+Pfad umgestellt. **Sauberer Nachweis** (gleicher Nutzer, gleiche Rolle, gleiche Route, nur die Kennzeichnung unterscheidet sich — auf `/users` haette der Rollen-Riegel das Ergebnis verdeckt): `GET /modules/active` → 403 `{"message":"FORCE_PASSWORD_CHANGE"}` mit Zwang, 200 ohne. `/auth/me`, `/auth/change-password` und `/auth/logout` kommen weiterhin durch. **Rot-dann-Gruen belegt:** neue Spezifikationen gegen den alten Stand 6 von 12 rot, danach 12/12 gruen — vom Verifier unabhaengig nachgestellt (alte Dateien aus `116041b` rekonstruiert). Gezielt nach Schlupfloechern gesucht (Schraegstrich am Ende, Abfragezeichen, Gross/Klein, `../`): keins. **Kein Aussperren:** kompletter Browser-Ablauf durchgespielt — Anmeldung leitet auf `/change-password`, Seite bedienbar, Wechsel gelingt, landet auf `/`, Kennzeichnung geloescht, freie Navigation. Die Seitenleiste zeigt waehrenddessen "Keine Module" (neuer 403 auf `/modules/active`, wortlos geschluckt) — sachlich richtig. **Dazu Fahrzeugtabelle (dkv-fleet):** Loeschknopf war doppelt ausloesbar (`isDeleting` wurde geschrieben, nie gelesen; Dialog blieb waehrend der Anfrage offen) — beide Dialogknoepfe jetzt gesperrt. Nebenbefund des Verifiers: die Wirkung kommt vom `disabled`-Attribut, React unterdrueckt Klicks darauf selbst; der Zustandscheck ist redundant, nicht falsch. Ausserdem sieben fest verdrahtete deutsche Texte und sechs Vorlese-Beschriftungen auf next-intl umgestellt (de und en, 87 Schluessel deckungsgleich). **Nicht angefasst, begruendet:** `SplitTab.tsx` `'certificates.zip'` — ein Downloadname ist ein Dateisystem-Artefakt, kein Bedienelement; uebersetzt braechte er Umlaute in Windows-Dateifreigaben. **Balken:** api 71 Dateien/1136 Tests, web 66/462, type-check 4/4, `pnpm lint` 5/5 ohne Fehlerstufe, Warnungen 466. Verifikation passed (9/9). | 2026-09-21 | f7c02b7,e56cce4 | [260921-fi3-erzwungener-passwortwechsel-wird-von-der](./quick/260921-fi3-erzwungener-passwortwechsel-wird-von-der/) |
| 260921-gof | **21 React-Effekt-Abhaengigkeiten einzeln beurteilt — 15 davon waren Fallen, nicht Fehler.** Die Klasse war aus 260921-bi2 zurueckgestellt worden, weil jeder Befund einzeln zu beurteilen ist. Ergebnis: nur **2 echte Defekte** (A), **15 Fallen** (B, das naive Eintragen haette eine Abruf-Schleife erzeugt), **3 Absicht** (C, mit begruendetem `biome-ignore` — erste Verwendung im Projekt), **1 Ballast** (D). **Die gefaehrlichste Stelle:** `calendar-widget.tsx:82` — `showToday` setzt bei jedem Klick ein frisches `Date`; `monthDate` naiv in die Liste einzutragen haette **jeden** Druck auf den Monatstitel einen Termin-Abruf ausloesen lassen, ueber die API bis zum Exchange-Server. Reihenfolge war Pflicht: erst Identitaet stabilisieren, dann die Liste umstellen. **Die haeufigste Falle:** `t` aus `useTranslations` ist in diesem Projekt bei jedem Durchlauf eine frische Funktion (die Test-Attrappen sind nachweislich so gebaut) — 8 Befunde. Griff ohne Ausnahme-Kommentar: den uebersetzten Text vor dem Hook in eine Konstante ziehen und diese eintragen; React vergleicht Zeichenketten per Wert. **Nebenbefund:** 11 `eslint-disable`-Zeilen fuer genau diese Regel waren wirkungslos, seit Biome ESLint abgeloest hat — alle entfernt. **Laufzeitnachweis vom Orchestrator im Browser** (Netzwerkprotokoll, nie `fetch` aus der Seite; gegen neu gebaute Abbilder): Dashboard 62 s Ruhe → Protokoll byte-identisch, genau 1 `calendar/events`; Monatstitel 3x gedrueckt → nur der erste Druck (Bereich aendert sich wirklich) loest einen Abruf aus, Druck 2 und 3 **null**; "Weiter" 3x → 3 Abrufe, korrekt; Stoppuhr 6 s real → Anzeige 00:06, 4 Runden ueber 4,8 s → 16/17/19/20 monoton, kein Ruecksprung. Dazu acht weitere Ansichten je 20-25 s ruhen gelassen (Marktplatz, Modulverwaltung, Gruppenverwaltung, DKV dreimal, Ausschreibungsradar zweimal) — jeder Endpunkt genau einmal. `InvoiceHistoryTable` hatte als einzige Datei keinen Test und ist damit gemessen statt nur gelesen; `ResultsList` ist die Stelle, an der die `t`-Falle in bi2 tatsaechlich zuschnappte. **Zahlen:** Warnungen 467 → 446 (exakt 21, nichts anderswo gewachsen), `useExhaustiveDependencies` 0, web-Tests 66/462 → 67/477, api 71/1136 unveraendert, type-check 4/4, `pnpm lint` 5/5 ohne Fehlerrang. Verifikation passed. **Benannt, nicht behoben:** zwei Verschwendungen im Kalender-Abruffenster (gleicher Zeitbereich zweimal geholt; `calendar/sources` bei jedem Monatswechsel) — vorbestehend; und `t` in vier vorbestehenden Abhaengigkeitslisten ausserhalb des Auftrags, die Biome nie gemeldet hat. | 2026-09-21 | b3f0e3c,e2c508c,e780b2c | [260921-gof-effekt-abhaengigkeiten-in-react-21-befun](./quick/260921-gof-effekt-abhaengigkeiten-in-react-21-befun/) |
| 260921-i8x | **Fuenf fehlerverdaechtige Lint-Klassen geprueft — kein einziger echter Fehler darunter.** Zwoelf Stellen einzeln beurteilt, Ergebnis: 7x gleichwertig oder Absicht, 2x Haertung, 3x idiomatisch korrekt. Das ist das Ergebnis, keine Ausrede — die Klassen klangen gefaehrlicher als sie waren. **Die eine Stelle mit echtem Wert:** `apps/web/src/lib/safe-next.ts`, der Schutz gegen Weiterleitung auf fremde Seiten nach der Anmeldung. Der Kommentar dort behauptete, die Steuerzeichen stuenden als Unicode-Escapes im Muster; die rohen Bytes zeigten das Gegenteil (NUL, 0x1F, 0x7F direkt eingebettet). Funktionierte, war aber zerbrechlich: verschluckt ein Werkzeug das NUL-Byte, wird aus dem Bereich stillschweigend ein anderer und der Schutz loechrig — der Rueckgabewert landet in `login/page.tsx` direkt in `window.location.href`. Jetzt echte Escapes, Datei ohne ein einziges Steuerbyte. **Beweis der Gleichwertigkeit, nicht Behauptung:** ueber alle 65536 Codepunkte dieselbe Menge abgelehnter Zeichen — 54 Stueck (32 Steuerzeichen 0x00-0x1F, dazu 0x7F, Backslash und die 20 Leerraum-Zeichen von `\s`), null Abweichung; vom Orchestrator unabhaengig gegen ein selbst gebautes Referenzmuster nachgerechnet. **Bewusst nicht angefasst:** die NUL-Maskierung in `ldap.service.ts` (RFC 4515) — genau dieses Zeichen zu treffen ist ihr Zweck, wer sie "repariert", oeffnet LDAP-Filter-Injection. Ebenso die drei `while ((m = re.exec(s)))`-Schleifen (idiomatisch, kein verrutschtes Gleichheitszeichen) und `noUselessSwitchCase` aus bi2. **Zwei Korrekturen an frueheren Annahmen:** `sanitizeNextPath` laeuft NICHT in der Edge-Middleware (die importiert nur `buildNextParam`), und das blosse Umschreiben auf Escapes senkt die Warnzahl nicht — Biome beanstandet die Escape-Schreibweise genauso, es braucht zusaetzlich einen einzeiligen Unterdrueckungskommentar. **Werkzeugfalle, dreimal zugeschnappt:** das Schreibwerkzeug wandelt `\uXXXX` still in das echte Zeichen um — der Planer erzeugte so zehn rohe Steuerbytes in seiner ersten Planfassung, der Executor zweimal in Commit-Text und Akte (git verweigerte den Commit wegen eines NUL-Bytes), und der Orchestrator beim Nachrechnen. Umgehung ueber `python3`/`chr(92)` ist im Plan hinterlegt. **Zahlen:** 446 → 434, `noControlCharactersInRegex`/`useIterableCallbackReturn`/`noGlobalIsNan` je 0, `suppressions/unused` 0, web-Tests 67/477 → 68/481, api 71/1136 → 72/1137, type-check 4/4, lint 5/5. | 2026-09-21 | f85c91b,076ca4b,b92dd5d | [260921-i8x-fehlerverdaechtige-lint-klassen-steuerze](./quick/260921-i8x-fehlerverdaechtige-lint-klassen-steuerze/) |
## Deferred Items
@@ -494,4 +495,4 @@ Last session: 2026-09-21T04:50:00Z
Resumed: 2026-09-21 — Sitzung ueber /gsd-resume-work fortgesetzt. Stand geprueft: Arbeitsbaum sauber, main == origin/main auf 55aa287, CI-Lauf 387 fuer 55aa287 erfolgreich (Beta-Images gebaut). Push und CI aus dem letzten Stopp-Punkt sind damit erledigt.
Stopped at: Warte auf Nutzerentscheidung, womit weitergearbeitet wird. Offen fuer den User: alpha pullen (web+api) und danach am Windows-VM-Client die echte Fehlermeldung schicken (Betreff `[Desktop/Windows]` + `Herkunft:`-Zeile pruefen); eigenen Arbeitsplatz-Client einmal per Browser-Installer erneuern; Freigabe 1.3.0 auf Zuruf. Technisch offen im Ledger: WINDOWS #35 (Biome laeuft nicht — biome.json:3 `organizeImports` ist in Biome 2.5.0 unbekannt, `biome check` bricht mit Konfigurationsfehler ab, reproduziert 2026-09-21) und WINDOWS #36 (403-Antworten bleiben in handleSubmit/handleDelete ohne sichtbare Reaktion).
Resume file: None
Last activity: 2026-09-21 - Quick 260921-gof: 21 Effekt-Abhaengigkeiten einzeln beurteilt (15 Fallen, 2 echte Defekte, 3 Absicht, 1 Ballast); Kalender-Falle haette jeden Druck auf den Monatstitel bis zum Exchange-Server durchschlagen lassen. Im Browser nachgemessen: keine Schleife auf zehn Ansichten, Stoppuhr echtzeitgetreu
Last activity: 2026-09-21 - Quick 260921-i8x: fuenf fehlerverdaechtige Lint-Klassen geprueft, kein echter Fehler darunter; Weiterleitungsschutz safe-next.ts von rohen Steuerbytes auf Escapes gehaertet, Gleichwertigkeit ueber alle 65536 Codepunkte nachgerechnet (54 abgelehnte Zeichen, null Abweichung)
@@ -0,0 +1,530 @@
---
phase: quick-260921-i8x
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [D-01, D-02, D-03, D-04, D-05, D-06]
files_modified:
- apps/web/src/lib/safe-next.ts
- apps/web/src/lib/safe-next.test.ts
- 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
- apps/api/src/dkv/dkv-parser.service.ts
- apps/api/src/dkv/dkv-parser.service.spec.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/components/locale-switcher.test.tsx
estimate:
tokens: 90000
raw_tokens: 45000
tasks: 3
confidence: low
must_haves:
truths:
- "sanitizeNextPath weist nach dem Umbau exakt dieselben 54 Codepunkte aus U+0000..U+FFFF ab wie vorher — nachgewiesen, nicht behauptet (D-01)."
- "apps/web/src/lib/safe-next.ts enthaelt kein einziges rohes Steuerbyte mehr (vorher 3: Offset 826/828/829)."
- "Jeder der 12 Befunde hat eine Entscheidung und eine Begruendung, die benennt, was sonst schiefginge (D-02)."
- "Kein Verhalten aendert sich ausser an einer Stelle, an der ein echter Defekt benannt wurde (D-03)."
- "Der Open-Redirect-Schutz bleibt unveraendert streng: kein Schema-Allowlist, kein gelockerter Zwei-Zeichen-Praefixtest (D-04)."
- "pnpm lint bleibt 5/5 ohne Befund der Stufe error; Biome meldet 433 Warnungen statt 445 und keinen einzigen suppressions/unused-Befund (D-06)."
- "Die Testsuiten bleiben gruen und wachsen: apps/web >= 68 Dateien / >= 480 Tests, apps/api 72 Dateien / >= 1137 Tests."
artifacts:
- apps/web/src/lib/safe-next.ts
- apps/web/src/lib/safe-next.test.ts
- apps/api/src/dkv/dkv-parser.service.spec.ts
- apps/web/src/components/locale-switcher.test.tsx
key_links:
- "login/page.tsx Zeile 37: window.location.href = sanitizeNextPath(next) — der Rueckgabewert landet ungefiltert in der Adresszeile."
- "middleware.ts importiert nur buildNextParam; safe-next.ts muss deshalb weiterhin frei von Node- und DOM-APIs bleiben."
- "LdapService.escapeLdapFilterValue speist drei Filterbauten (Zeile 576, 1142, 1228) — die Escape-Kette ist die LDAP-Injection-Sperre."
- "i18n/request.ts liest NEXT_LOCALE serverseitig; was locale-switcher.tsx schreibt, muss dort ankommen."
---
<objective>
Zwoelf fehlerverdaechtige Lint-Befunde aus fuenf Regelklassen einzeln beurteilen und
abschliessen — jeder mit Urteil und Begruendung (D-02), ohne Verhaltensaenderung
ausser an nachgewiesenen Defekten (D-03).
Purpose: Der Weiterleitungsschutz nach der Anmeldung haengt heute an drei rohen
Steuerbytes in einer Zeichenklasse. Das funktioniert, ist aber gegen jeden
Editor, Formatierer, Minifier und Copy-Paste ungeschuetzt — und der Kommentar
zwei Zeilen darueber behauptet bereits faelschlich, es seien Escapes. Die
restlichen elf Befunde sind zu klaeren, damit die Klassen nicht als "ungeprueft"
im Rueckstand stehen bleiben.
Output: Zwoelf abgeschlossene Befunde, ein beweisbar unveraenderter
Zeichenraum in safe-next.ts, drei neue bzw. erweiterte Testdateien,
Biome-Warnungen 445 -> 433.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@CLAUDE.md
@apps/web/src/lib/safe-next.ts
@apps/web/src/lib/safe-next.test.ts
@apps/web/src/app/(auth)/login/page.tsx
@apps/api/src/ldap/ldap.service.ts
@apps/web/src/components/locale-switcher.tsx
@biome.json
</context>
<measured_baseline_at_planning_time>
Am 2026-09-21 bei der Planung selbst gemessen, Arbeitsverzeichnis sauber:
| Messung | Wert |
|---|---|
| `npx biome lint --reporter=json .` Diagnosen gesamt | 446 (445 warning + 1 info) |
| `pnpm lint` | 5/5 erfolgreich, 0 Befunde der Stufe error |
| `pnpm type-check` | 4/4 erfolgreich |
| `pnpm --filter @tessera/web test` | 67 Dateien / 477 Tests |
| `pnpm --filter @tessera/api test` | 71 Dateien / 1136 Tests |
Die zwoelf Befunde im Auftrag, Regel fuer Regel nachgezaehlt (Testdateien getrennt):
| Regel | gesamt | echter Quelltext | Testdateien |
|---|---|---|---|
| `noControlCharactersInRegex` | 3 | 3 | 0 |
| `useIterableCallbackReturn` | 1 | 1 | 0 |
| `noGlobalIsNan` | 4 | 4 | 0 |
| `noAssignInExpressions` | 7 | 3 | 4 |
| `noDocumentCookie` | 18 | 1 | 17 |
**Rohbytes in `apps/web/src/lib/safe-next.ts` (selbst ausgelesen, nicht uebernommen):**
Datei 2458 Byte, Zeile 18 lautet byteweise
`63 6f 6e 73 74 20 46 4f 52 42 49 44 44 45 4e 5f 43 48 41 52 53 5f 52 45 20 3d 20 2f 5b 5c 5c 5c 73 00 2d 1f 7f 5d 2f 3b`.
Die drei Nicht-Zeilenumbruch-Steuerbytes der Datei sitzen an Offset 826 (0x00),
828 (0x1F) und 829 (0x7F). Die Zeichenklasse ist also: Backslash, `\s`,
Bereich U+0000 bis U+001F, dazu U+007F. Die Transkription im Auftrag stimmt.
**Drei Messungen, die den Plan tragen (jeweils mit einer Wegwerf-Datei geprueft und wieder entfernt):**
1. Biome beanstandet Steuerzeichen **auch als Escape**. `\u0000-\u001f` und
`\x00-\x1f` erzeugen genau dieselben zwei Warnungen wie die Rohbytes. Das
blosse Umschreiben auf Escapes senkt die Warnungszahl also **nicht** — es
braucht zusaetzlich einen Unterdrueckungskommentar. U+007F beanstandet Biome
nicht; die Regel deckt nur U+0000..U+001F ab. Deshalb sind es 2 Befunde in
dieser Datei und nicht 3.
2. Ein `// biome-ignore` unmittelbar ueber der Regex-Zeile unterdrueckt **beide**
Spaltenbefunde dieser Zeile mit einem einzigen Kommentar. Steht der Kommentar
dagegen eine Zeile zu hoch, bleibt der urspruengliche Befund stehen **und**
Biome meldet zusaetzlich `suppressions/unused` — die Warnungszahl steigt dann.
Das ist die gefaehrlichste Falle dieses Vorgangs.
3. Die Form `for (let m = re.exec(t); m !== null; m = re.exec(t))` wird von
`noAssignInExpressions` **nicht** beanstandet. Die drei exec-Schleifen lassen
sich also ohne Unterdrueckungskommentar und ohne Verhaltensaenderung
umschreiben.
**Der Zeichenraum von `sanitizeNextPath` heute, vollstaendig ausgemessen**
(U+0000..U+FFFF, Eingabe `'/a' + Zeichen + 'b'`, abgewiesen = Rueckgabe `'/'`):
genau **54** Codepunkte werden abgewiesen —
`0x00`-`0x1f`, `0x20`, `0x5c`, `0x7f`, `0xa0`, `0x1680`, `0x2000`-`0x200a`,
`0x2028`, `0x2029`, `0x202f`, `0x205f`, `0x3000`, `0xfeff`.
SHA-256 der 65536 Zeichen langen Bitmap aus `'1'`/`'0'`:
`3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64`.
Und vorab bewiesen, ebenfalls ueber alle 65536 Codepunkte: die Escape-Variante
`/[\\\s\u0000-\u001f\u007f]/` trifft **exakt** dieselbe Menge wie die Variante
mit Rohbytes. Null Abweichungen.
</measured_baseline_at_planning_time>
<verdicts>
Das Urteil je Befund, aus dem gelesenen Quelltext, nicht aus der Regel (D-02):
| # | Datei | Regel | Urteil | Begruendung |
|---|---|---|---|---|
| 1 | `apps/web/src/lib/safe-next.ts` (2 Befunde) | `noControlCharactersInRegex` | **Haertung** | Kein Fehler heute, aber die Zeichenklasse des Open-Redirect-Schutzes haengt an drei rohen Bytes, die jedes Werkzeug in der Kette stillschweigend fressen kann — faellt das NUL weg, wird `[\\\s-\u001f\u007f]` daraus, ein Bereich, der ploetzlich andere Zeichen umfasst. |
| 2 | `apps/api/src/ldap/ldap.service.ts:1662` | `noControlCharactersInRegex` | **Absicht** | `.replace(/\x00/g, '\\00')` ist die von RFC 4515 vorgeschriebene NUL-Maskierung in `escapeLdapFilterValue` — genau dieses Steuerzeichen zu treffen ist der Zweck; wer es entfernt, oeffnet LDAP-Filter-Injection. |
| 3 | `apps/web/src/app/(portal)/modules/cert-manager/actions.ts:154` | `useIterableCallbackReturn` | **gleichwertig** | Es ist `forEach`, kein `.every`/`.some`/`.map`/`.sort`: der Pfeilausdruck gibt das `undefined` von `FormData.append` zurueck, `forEach` verwirft jeden Rueckgabewert. Rein kosmetisch. |
| 4-7 | `TenderDetail.tsx` (2x), `InvoiceHistoryTable.tsx`, `ResultsList.tsx` | `noGlobalIsNan` | **gleichwertig** | Alle vier Stellen lauten `isNaN(d.getTime())`. `Date.prototype.getTime()` liefert laut Spezifikation immer `number`, also findet gar keine Umwandlung statt. Der Tausch ist sicher, weil das Argument statisch `number` ist; er lohnt trotzdem, weil `isNaN` bei einer spaeteren Aenderung auf einen String stillschweigend umwandeln wuerde. |
| 8-10 | `dkv-parser.service.ts:104`, `dkv-parser.validate.ts:139`, `icon-discovery.service.ts:182` | `noAssignInExpressions` | **Absicht** | Alle drei sind die idiomatische Form `while ((m = re.exec(text)) !== null)` mit globalem Regex — kein verrutschtes `=`. Keine der drei Variablen wird nach der Schleife gelesen. |
| 11 | `apps/web/src/components/locale-switcher.tsx:15` | `noDocumentCookie` | **Haertung** | `path=/` und `max-age=31536000` sind gesetzt und richtig, die Sprache bleibt also erhalten — kein Persistenzdefekt. Es fehlt `SameSite`: das Cookie haengt damit am Browservorgabewert statt an einer Festlegung. Die Cookie-Store-API, die Biome vorschlaegt, fehlt in WebKit und damit im Tauri-Wrapper auf Linux/macOS. |
</verdicts>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: safe-next.ts — Steuerzeichen als Escapes, Zeichenmenge beweisbar unveraendert</name>
<files>apps/web/src/lib/safe-next.ts, apps/web/src/lib/safe-next.test.ts</files>
<read_first>
apps/web/src/lib/safe-next.ts (vollstaendig, inklusive Rohbytes),
apps/web/src/lib/safe-next.test.ts (Tests 1-9),
apps/web/src/app/(auth)/login/page.tsx Zeile 30-40 (die Senke: window.location.href),
apps/web/src/middleware.ts Zeile 1-5 und 94-100 (importiert nur buildNextParam).
Umsetzt D-01 und D-04.
</read_first>
<behavior>
Zuerst die Tests, dann der Umbau. Neu in safe-next.test.ts:
- Test A "die abgewiesene Zeichenmenge ist und bleibt genau diese 54 Codepunkte":
Schleife ueber cp = 0 bis 0xffff, Eingabe `'/a' + String.fromCharCode(cp) + 'b'`,
abgewiesen genau dann, wenn `sanitizeNextPath(...)` den Wert `'/'` liefert.
Die gesammelte Liste der abgewiesenen Codepunkte muss exakt gleich sein zu
der im Plan gemessenen Liste (0x00-0x1f, 0x20, 0x5c, 0x7f, 0xa0, 0x1680,
0x2000-0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, 0xfeff — 54 Stueck).
Die Erwartung wird als Liste hart hinterlegt, nicht aus einem Praedikat
erzeugt: eine aus derselben Regel abgeleitete Erwartung wuerde jede
Aenderung der Regel mitmachen und damit nichts beweisen.
- Test B "die Datei enthaelt kein rohes Steuerzeichen": liest die eigene
Quelldatei ueber `node:fs` als Buffer und zaehlt Bytes < 0x20 ausser 0x0a,
plus 0x7f. Erwartung: 0.
- Test C "der Zwei-Zeichen-Praefixtest bleibt scharf": `'//host'`,
`'/\\host'` und `'/'` + ein U+0000 landen weiterhin auf `'/'`.
Test A muss **vor** dem Umbau geschrieben werden und **gegen die heutige
Fassung gruen** sein — das ist der Sinn: er haelt den Ist-Zustand fest.
Test B ist vor dem Umbau rot (3 Bytes) und nach dem Umbau gruen.
</behavior>
<action>
Schritt 1 — Ausgangswert sichern, bevor irgendetwas geaendert wird. Test A und
Test C in safe-next.test.ts ergaenzen, Test B ebenfalls ergaenzen aber zunaechst
sein Rotsein zur Kenntnis nehmen. `npx vitest run src/lib/safe-next.test.ts` in
apps/web laufen lassen: A und C gruen, B rot. Zusaetzlich den SHA-256 der
Bitmap aus Test A ausgeben lassen oder einmalig berechnen und notieren; er muss
`3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64` lauten.
Weicht er ab, sofort anhalten und melden — dann ist der Arbeitsbaum nicht der,
auf dem dieser Plan gemessen wurde.
Schritt 2 — den Umbau. In safe-next.ts die Zeile mit `FORBIDDEN_CHARS_RE` so
ersetzen, dass die Zeichenklasse aus Backslash, `\s`, dem Bereich `\u0000` bis
`\u001f` und `\u007f` besteht, geschrieben ausschliesslich mit
Unicode-Escapes. Danach enthaelt die Datei keine Rohbytes mehr (D-01). Die
Reihenfolge der Klassenglieder unveraendert lassen.
Schritt 3 — den Kommentar berichtigen. Der bestehende Zweizeiler ueber der
Konstante behauptet heute, die Zeichen seien "als Unicode-Escapes, nicht \x,
weil safe-next.ts auch am Edge laeuft" geschrieben. Das war doppelt falsch:
sie waren keine Escapes, und zwischen `\x` und `\u` gibt es zur Laufzeit
keinerlei Unterschied, am Edge so wenig wie im Browser. Neu formulieren: was
die Klasse trifft (Backslash, jegliches Whitespace, U+0000 bis U+001F, U+007F),
und die tatsaechliche Randbedingung der Datei — sie wird von der
Edge-Middleware und vom Client importiert und darf deshalb keine Node- oder
DOM-APIs verwenden. Nicht behaupten, Escapes seien am Edge noetig.
Schritt 4 — die beiden Biome-Befunde unterdruecken. Unmittelbar, ohne
Leerzeile, ueber die Regex-Zeile einen `biome-ignore`-Kommentar fuer
`lint/suspicious/noControlCharactersInRegex` setzen. Begruendung im
Repo-Stil (siehe die vier vorhandenen Beispiele, deutsch, nennt die Folge):
dass die Regel den Bereich auch in Escape-Schreibweise beanstandet, dass
genau dieser Bereich der Zweck des Ausdrucks ist, und dass er den
Weiterleitungsschutz T-gyd-01 traegt. Die Platzierung ist kritisch: eine
Zeile zu hoch und Biome meldet den alten Befund weiter und zusaetzlich einen
unbenutzten Unterdrueckungskommentar.
Schritt 5 — beweisen. Test A erneut laufen lassen; er muss ohne jede Aenderung
an der Erwartungsliste gruen bleiben. Test B ist jetzt gruen.
WERKZEUGFALLE, bei der Planung selbst hineingetappt: Die Schreibwerkzeuge
(Write, Edit) behandeln ihren Inhalt wie eine JSON-Zeichenkette und wandeln
eine Sequenz aus Backslash, kleinem `u` und vier Hexziffern **still in das
Zeichen selbst um**. Wer die neue Regex-Zeile auf diesem Weg schreibt,
erzeugt exakt wieder die rohen Steuerbytes, die dieser Task beseitigen soll —
Test B faellt dann durch, und beim fluechtigen Hinsehen sieht die Datei
richtig aus. Abhilfe: die Zeile mit einem Platzhalter statt des Backslashs
schreiben und den Platzhalter anschliessend per `python3` durch `chr(92)`
ersetzen, oder die Zeile gleich vollstaendig per `python3` in die Datei
schreiben. Danach zwingend der Byte-Scan aus `<verify>`, bevor irgendetwas
als fertig gilt.
Was NICHT passiert (D-04): der Zwei-Zeichen-Praefixtest bleibt wie er ist, es
kommt keine Liste bekannter Schemata hinzu, `MAX_NEXT_PATH_LENGTH` bleibt bei
2048, die Reihenfolge der Pruefungen bleibt, die bestehenden Tests 1-9 werden
nicht angefasst. Keine Abhaengigkeit, keine Formatierung ausserhalb dieser
Datei (D-05).
</action>
<verify>
<automated>cd apps/web &amp;&amp; npx vitest run src/lib/safe-next.test.ts 2>&amp;1 | tail -6</automated>
<automated>python3 -c "b=open('apps/web/src/lib/safe-next.ts','rb').read(); c=[i for i,x in enumerate(b) if (x&lt;0x20 and x!=0x0a) or x==0x7f]; print('CTRL_BYTES', len(c), c); assert len(c)==0, 'rohe Steuerbytes noch vorhanden'"</automated>
<automated>npx biome lint apps/web/src/lib/safe-next.ts 2>&amp;1 | tail -3</automated>
</verify>
<done>
Vitest meldet `Test Files 1 passed`, Tests >= 12 passed, kein failed.
Der Byte-Scan gibt `CTRL_BYTES 0 []` aus und bricht nicht ab.
Biome meldet fuer die Datei `Found 0 warnings` (weder ein
Steuerzeichen-Befund noch ein unbenutzter Unterdrueckungskommentar).
Test A erzwingt weiterhin exakt 54 abgewiesene Codepunkte — dieselbe Liste
wie vor dem Umbau, damit ist der Zeichenraum nachweislich identisch (D-01).
Die Tests 5 bis 9 aus der Bestandsdatei laufen unveraendert durch (D-04).
</done>
<reversibility rating="reversible">Eine Zeile Regex plus Tests; jederzeit per git revert zurueckzunehmen, und die Testdatei haelt den Ist-Zustand fest.</reversibility>
</task>
<task type="auto">
<name>Task 2: isNaN und forEach — vier plus ein gleichwertiger Tausch</name>
<files>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</files>
<read_first>
TenderDetail.tsx Zeile 40-70, ResultsList.tsx Zeile 45-55,
InvoiceHistoryTable.tsx Zeile 25-40, cert-manager/actions.ts Zeile 148-160.
Umsetzt D-02 und D-03.
</read_first>
<action>
Fuenf Stellen, alle als gleichwertig beurteilt — die Aenderung ist Haertung,
kein Defektbehebung, und so ist sie auch in der Zusammenfassung zu benennen
(D-02: das offene Eingestaendnis ist das erwuenschte Ergebnis, das stille
Umdeuten zum Fehler nicht).
Vier Stellen `isNaN(d.getTime())` auf `Number.isNaN(d.getTime())` umstellen:
TenderDetail.tsx Zeile 44 und Zeile 65, InvoiceHistoryTable.tsx Zeile 33,
ResultsList.tsx Zeile 51. Der Tausch ist deshalb sicher, weil `getTime()`
immer `number` liefert — die Umwandlung, vor der die Regel warnt, findet an
diesen Stellen gar nicht statt. Der Nutzen liegt in der Zukunft: sobald dort
einmal ein String aus einer API oder einem Formular ankaeme, wuerde das
globale `isNaN` ihn stillschweigend umwandeln und eine kaputte Eingabe als
gueltiges Datum durchwinken. Die bestehenden Rueckfallwerte
(`noDeadlineLabel`, `'–'`, `isoString`) unveraendert lassen, ebenso den
Kommentar ueber Zeile 33 in InvoiceHistoryTable.tsx (WR-03), der weiterhin
stimmt. Die vorhandenen `biome-ignore`-Kommentare fuer
`useExhaustiveDependencies` in InvoiceHistoryTable.tsx Zeile 84 und
ResultsList.tsx Zeile 141 nicht beruehren.
In cert-manager/actions.ts Zeile 154 die Pfeilfunktion mit Kurzform-Rumpf auf
einen Block-Rumpf umstellen, sodass der Rueckgabewert von `FormData.append`
nicht mehr aus der Schleifenfunktion herausgereicht wird. `forEach` verwirft
ihn ohnehin — deshalb ist das eine Lesbarkeitsaenderung ohne jede Wirkung.
Eine Umstellung auf `for...of` ist ebenso zulaessig, aendert aber mehr Zeilen
als noetig. Reihenfolge der `form.append`-Aufrufe strikt beibehalten: erst
alle Dateien, dann `outputFormat`, dann optional `password` — der Server
liest das Multipart-Formular in dieser Reihenfolge.
</action>
<verify>
<automated>npx biome lint --reporter=json --max-diagnostics=5000 . 2>/dev/null | python3 -c "import sys,json,collections; d=json.load(sys.stdin); c=collections.Counter(x['category'] for x in d['diagnostics']); print('noGlobalIsNan', c['lint/suspicious/noGlobalIsNan']); print('useIterableCallbackReturn', c['lint/suspicious/useIterableCallbackReturn']); assert c['lint/suspicious/noGlobalIsNan']==0 and c['lint/suspicious/useIterableCallbackReturn']==0"</automated>
<automated>pnpm --filter @tessera/web test 2>&amp;1 | tail -5</automated>
<automated>pnpm type-check 2>&amp;1 | tail -4</automated>
</verify>
<done>
Die Zaehlung aus der Biome-JSON-Ausgabe meldet `noGlobalIsNan 0` und
`useIterableCallbackReturn 0` und bricht nicht ab. Die Zahlen stammen aus
dem Feld `category` der Diagnosen, nicht aus einer Textsuche im Quelltext —
ein Kommentar im Code kann das Tor deshalb nicht verfaelschen.
`pnpm --filter @tessera/web test` meldet mindestens 67 Dateien und
mindestens 477 Tests, alle passed.
`pnpm type-check` meldet 4/4 erfolgreich.
Kein Rueckfallwert und keine append-Reihenfolge wurde veraendert (D-03).
</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: exec-Schleifen, LDAP-NUL-Maskierung und das Sprachumschalter-Cookie</name>
<files>apps/api/src/dkv/dkv-parser.service.ts, apps/api/src/dkv/dkv-parser.service.spec.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/components/locale-switcher.test.tsx</files>
<read_first>
apps/api/src/dkv/dkv-parser.service.ts Zeile 34-115 (parsePdf, extractText, parseDkvText),
apps/api/src/dkv/dkv-parser.validate.ts Zeile 128-150,
apps/api/src/favorites/icon-discovery.service.ts Zeile 177-190,
apps/api/src/ldap/ldap.service.ts Zeile 1650-1675 (escapeLdapFilterValue und die NOTE darunter),
apps/web/src/components/locale-switcher.tsx,
apps/web/src/i18n/request.ts,
apps/api/src/tenders/tender-mail.service.spec.ts (Vorbild fuer vi.mock auf ein Paket),
apps/api/src/favorites/icon-discovery.service.spec.ts (deckt parseAttributes ueber discoverFavoriteIconUrl ab).
Umsetzt D-02, D-03 und D-05.
</read_first>
<behavior>
Zwei neue Tests, beide **vor** der jeweiligen Aenderung geschrieben und gegen
die heutige Fassung gruen:
- `apps/api/src/dkv/dkv-parser.service.spec.ts` (neu): `pdf-parse` per
`vi.mock` durch eine Attrappe ersetzen, deren `getText()` einen fest
hinterlegten DKV-Beispieltext mit **zwei** Fahrzeugbloecken liefert
(`VEHICLE: ... CARD NO.: ...`, je eine tabulatorgetrennte Transaktionszeile,
dazu eine Rechnungsnummer im Format DD/DDDDDDDDD/DDD gefolgt von einem
Datum in der naechsten Zeile). `destroy()` als no-op. Ein Test:
`parsePdf` liefert zwei Fahrzeugbloecke mit den erwarteten Kennzeichen und
Kartennummern, plus die erwartete Rechnungsnummer und das erwartete
Rechnungsdatum. Dieser Test ist das Rueckfall-Tor fuer den
Schleifenumbau — `parseDkvText` hat heute keine eigene Abdeckung.
- `apps/web/src/components/locale-switcher.test.tsx` (neu): den Setter von
`document.cookie` per `Object.defineProperty` durch eine Attrappe ersetzen,
die Komponente rendern, den Knopf klicken und die genau **eine**
geschriebene Zeichenkette pruefen: sie enthaelt `NEXT_LOCALE=en`,
`path=/`, `max-age=31536000` und — nach der Aenderung — `SameSite=Lax`.
Vor der Aenderung faellt nur die SameSite-Zusicherung durch; das ist
beabsichtigt und zeigt genau die Luecke.
</behavior>
<action>
Teil A — die drei exec-Schleifen (Urteil: Absicht, kein verrutschtes `=`).
Alle drei sind die korrekte Standardform. Sie werden trotzdem umgeschrieben,
weil es eine verhaltensgleiche Schreibweise gibt, die ohne
Unterdrueckungskommentar auskommt: die Zuweisung wandert in den Kopf einer
`for`-Schleife, deren Initialisierung den ersten `exec`-Aufruf macht, deren
Bedingung auf `null` prueft und deren Fortschaltung den naechsten
`exec`-Aufruf macht. Bei Planung nachgemessen: diese Form loest die Regel
nicht aus. Die Abfolge der `exec`-Aufrufe und der Rumpfdurchlaeufe ist
identisch, der globale Regex behaelt seinen `lastIndex`-Fortschritt.
Betroffen: `dkv-parser.service.ts` Zeile 103/104 (`match`),
`dkv-parser.validate.ts` Zeile 138/139 (`match`),
`icon-discovery.service.ts` Zeile 180/182 (`m`). In allen drei Faellen wird
die Variable nach der Schleife nicht mehr gelesen — nachgeprueft —, die
vorgezogene `let`-Deklaration entfaellt also ersatzlos. Die Regex-Literale
selbst, die Rumpfinhalte und die `.trim()`-Aufrufe bleiben Zeichen fuer
Zeichen unveraendert. `dkv-parser.validate.ts` ist ein eigenstaendiges
Pruefskript, das nur per `node --experimental-strip-types` laeuft; es wird
mitgeaendert, damit es nicht von der Service-Fassung abdriftet, und von
`pnpm type-check` mit abgedeckt.
Teil B — die NUL-Maskierung im LDAP-Filter (Urteil: Absicht). An
`LdapService.escapeLdapFilterValue` wird **nichts** geaendert. Der Ausdruck
trifft U+0000 absichtlich, weil RFC 4515 genau dieses Zeichen als `\00`
verlangt; die Funktion ist die Injection-Sperre fuer die drei Filterbauten in
Zeile 576, 1142 und 1228. Stattdessen unmittelbar ueber die betroffene Zeile
ein `biome-ignore` fuer `lint/suspicious/noControlCharactersInRegex` setzen,
mit deutscher Begruendung, die den RFC und die Folge des Entfernens nennt
(ein unmaskiertes NUL kann den Filter beim Verzeichnisserver abschneiden).
Die Escape-Reihenfolge — Backslash zuerst, dann Stern, Klammer auf, Klammer
zu, NUL — nicht antasten: der Backslash muss zuerst kommen, sonst werden die
eigenen Ersetzungen erneut maskiert.
Teil C — das Sprachumschalter-Cookie (Urteil: Haertung). `path=/` stimmt und
`max-age=31536000` stimmt; `i18n/request.ts` liest `NEXT_LOCALE`
serverseitig und bekommt das Cookie heute auch — es liegt also kein
Persistenzdefekt vor, und das ist so zu benennen. Ergaenzt wird `SameSite=Lax`,
damit die Uebertragung bei der Seitennavigation festgelegt ist statt vom
Vorgabewert des jeweiligen Browsers abzuhaengen; `Lax` und nicht `Strict`,
weil das Cookie bei einer Navigation von aussen auf die Seite mitkommen muss,
sonst faellt die Oberflaeche auf Deutsch zurueck. Kein `Secure` setzen: die
Entwicklungsumgebung laeuft auf `http://localhost:3000`, und das Cookie traegt
keine Berechtigung, nur die Sprachwahl. Der Schreibzugriff bleibt
`document.cookie`; darueber ein `biome-ignore` fuer
`lint/suspicious/noDocumentCookie` mit der Begruendung, dass die von Biome
vorgeschlagene Cookie-Store-API in WebKit fehlt und die Sprachumschaltung
damit im Tauri-Wrapper auf Linux und macOS wirkungslos waere.
Fuer alle drei Unterdrueckungskommentare in diesem Vorgang gilt: sie stehen
direkt ueber der beanstandeten Zeile, ohne Leerzeile dazwischen. Sitzt einer
falsch, bleibt der alte Befund stehen und Biome meldet zusaetzlich einen
unbenutzten Kommentar — die Warnungszahl steigt dann statt zu fallen und das
Tor in `<done>` schlaegt fehl.
Keine neue Abhaengigkeit, keine Versionsanhebung, keine Formatierung ueber
die genannten Dateien hinaus (D-05).
</action>
<verify>
<automated>pnpm --filter @tessera/api test 2>&amp;1 | tail -5</automated>
<automated>pnpm --filter @tessera/web test 2>&amp;1 | tail -5</automated>
<automated>pnpm type-check 2>&amp;1 | tail -4</automated>
<automated>npx biome lint --reporter=json --max-diagnostics=5000 . 2>/dev/null | python3 -c "import sys,json,collections; d=json.load(sys.stdin); c=collections.Counter(x['category'] for x in d['diagnostics']); s=collections.Counter(x['severity'] for x in d['diagnostics']); print('total',len(d['diagnostics']),'sev',dict(s)); print({k:c[k] for k in ['lint/suspicious/noControlCharactersInRegex','lint/suspicious/useIterableCallbackReturn','lint/suspicious/noGlobalIsNan','lint/suspicious/noAssignInExpressions','lint/suspicious/noDocumentCookie','suppressions/unused']}); assert c['lint/suspicious/noControlCharactersInRegex']==0; assert c['lint/suspicious/noAssignInExpressions']==4; assert c['lint/suspicious/noDocumentCookie']==17; assert c['suppressions/unused']==0; assert s['error']==0; assert len(d['diagnostics'])==434"</automated>
<automated>pnpm lint 2>&amp;1 | tail -4</automated>
</verify>
<done>
`pnpm --filter @tessera/api test` meldet 72 Dateien und mindestens 1137 Tests,
alle passed (die 71/1136 von vorher plus die neue Parser-Spezifikation).
`pnpm --filter @tessera/web test` meldet mindestens 68 Dateien und mindestens
480 Tests, alle passed.
`pnpm type-check` meldet 4/4 erfolgreich.
Die Biome-Zaehlung meldet `total 434`, `sev {'warning': 433, 'info': 1}` und
bricht bei keiner der sechs Zusicherungen ab: die drei Steuerzeichen-Befunde
sind weg, `noAssignInExpressions` steht bei 4 (die vier verbliebenen liegen
ausschliesslich in Testdateien und waren nie im Auftrag),
`noDocumentCookie` steht bei 17 (ebenfalls samtlich Testdateien), es gibt
keinen unbenutzten Unterdrueckungskommentar und keinen Befund der Stufe error.
434 = 446 minus die zwoelf bearbeiteten Befunde — die Zahl faellt also genau
um das, was bearbeitet wurde, und waechst nirgends sonst (D-06).
`pnpm lint` meldet `5 successful, 5 total`.
`escapeLdapFilterValue` ist zeichengleich zur Ausgangsfassung geblieben (D-03).
</done>
<reversibility rating="reversible">Mechanische Umschreibungen plus zwei Kommentare; die beiden neuen Spezifikationen halten das Verhalten fest, ein Zurueckrollen ist folgenlos.</reversibility>
</task>
</tasks>
<threat_model>
ASVS-Stufe 1, blockierend ab Schweregrad high. Keine Paketinstallation in diesem
Vorgang (D-05), deshalb kein Paket-Legitimitaetstor.
## Trust Boundaries
| Grenze | Beschreibung |
|---|---|
| Adresszeile des Browsers -> `next`-Parameter -> `sanitizeNextPath` -> `window.location.href` | Vollstaendig angreiferkontrollierter Wert. In `login/page.tsx:37` wird der Rueckgabewert **direkt** der Adresszeile zugewiesen; zwischen der Funktion und der Navigation liegt nichts mehr. |
| Browser -> Edge-Middleware -> `buildNextParam` | Die Middleware importiert nur `buildNextParam` und **nicht** `sanitizeNextPath`. Der Schutz laeuft also ausschliesslich im Client. Die Doppelnutzung der Datei bedeutet praktisch nur eine Auflage: keine Node- und keine DOM-APIs in safe-next.ts. Zwischen `\x`- und `\u`-Escapes gibt es zur Laufzeit keinen Unterschied, am Edge so wenig wie im Browser — die gegenteilige Behauptung im heutigen Kommentar ist gegenstandslos und wird in Task 1 berichtigt. |
| Nutzereingabe / Gruppen-DN aus der Datenbank -> `escapeLdapFilterValue` -> LDAP-Filterzeichenkette -> Verzeichnisserver | Die Maskierung ist die einzige Sperre gegen Filter-Injection; das Verzeichnis wird nur lesend gebunden. |
| Client -> `document.cookie` NEXT_LOCALE -> `cookies().get('NEXT_LOCALE')` in `i18n/request.ts` | Vom Nutzer setzbarer Wert ohne Berechtigung; steuert nur die Sprachauswahl. |
| DKV-PDF aus dem Postfach -> `parsePdf` -> Fahrzeug- und Transaktionsdaten | Fremdformat; ein stiller Parserfehler erzeugt falsche Rechnungsdaten statt eines Abbruchs. |
## STRIDE Threat Register
| Threat ID | Kategorie | Komponente | Schweregrad | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-i8x-01 | Spoofing | `FORBIDDEN_CHARS_RE` in `safe-next.ts` | high | mitigate | Faellt beim Umschreiben ein Klassenglied weg oder verrutscht der Bereichsstrich, veraendert sich die abgewiesene Zeichenmenge unbemerkt — und genau diese Klasse haelt Whitespace-Schmuggel und Steuerzeichen aus dem Ziel heraus, das `login/page.tsx:37` in die Adresszeile schreibt. Task 1 haelt die Menge mit einem Test ueber alle 65536 Codepunkte gegen eine hart hinterlegte Liste von 54 Codepunkten fest, geschrieben **vor** dem Umbau und gruen gegen die Altfassung. Zusaetzlich vorab bewiesen: die Escape-Variante trifft dieselbe Menge, null Abweichungen. |
| T-i8x-02 | Tampering | Zwei-Zeichen-Praefixtest in `sanitizeNextPath` | high | mitigate | Ein "Vereinfachen" von `raw[1] === '/' \|\| raw[1] === '\\'` wuerde protokoll-relative Ziele wie `//boeser.host` wieder durchlassen, die der Browser als absolute Adresse liest. D-04 verbietet die Aenderung; Task 1 aendert ausschliesslich die Regex-Zeile und die zwei Kommentarzeilen, Test C und die Bestandstests 7 und 8 sichern den Praefixtest ab. |
| T-i8x-03 | Tampering | `LdapService.escapeLdapFilterValue` | high | mitigate | Wer das als "Steuerzeichen-Befund" behandelt und den NUL-Ersatz entfernt oder die Reihenfolge der Ersetzungen dreht, oeffnet LDAP-Filter-Injection bzw. laesst den Backslash die eigenen Ersetzungen doppelt maskieren. Task 3 Teil B aendert die Funktion nicht, sondern setzt nur einen Unterdrueckungskommentar; `<done>` fordert Zeichengleichheit zur Ausgangsfassung. |
| T-i8x-04 | Tampering | `parseDkvText` und `parseAttributes`, Umbau der exec-Schleifen | medium | mitigate | Ein falsch gesetzter Fortschaltungsausdruck ergibt eine Endlosschleife oder ueberspringt jeden zweiten Treffer — bei der Rechnungsauswertung faellt das erst an falschen Zahlen auf. `parseDkvText` hat heute keine Abdeckung; Task 3 legt dafuer eine Spezifikation mit zwei Fahrzeugbloecken an, **bevor** umgebaut wird. `parseAttributes` ist ueber `icon-discovery.service.spec.ts` ("extracts the apple-touch-icon from page HTML") bereits abgedeckt. |
| T-i8x-05 | Information Disclosure | NEXT_LOCALE-Cookie | low | mitigate | Ohne ausdrueckliches `SameSite` haengt die Uebertragung am Vorgabewert des Browsers; ein Browser ohne Lax-Vorgabe schickt das Cookie auch seitenuebergreifend mit. Das Cookie traegt keine Berechtigung, der Schaden ist entsprechend gering. Task 3 Teil C setzt `SameSite=Lax` und sichert die vollstaendige geschriebene Zeichenkette mit einem Test ab. |
| T-i8x-06 | Repudiation | Unterdrueckungskommentare in allen drei Dateien | low | mitigate | Ein falsch platzierter `biome-ignore` laesst den urspruenglichen Befund stehen und erzeugt zusaetzlich `suppressions/unused` — die sicherheitsrelevante Regel waere dann still weiter offen, waehrend die Zusammenfassung sie als erledigt fuehrt. Das Tor in Task 3 zaehlt `suppressions/unused` ausdruecklich auf 0 und die Gesamtzahl auf exakt 434. |
| T-i8x-07 | Tampering | Messtor selbst | low | accept | Die Zaehlungen lesen das Feld `category` aus Biomes JSON-Ausgabe, nie den Quelltext per Textsuche. Ein Regelname im Unterdrueckungskommentar kann das Tor deshalb nicht verfaelschen. Kein weiterer Aufwand noetig. |
</threat_model>
<verification>
Nach allen drei Tasks, im sauberen Arbeitsbaum, vom Projektwurzelverzeichnis aus:
1. `pnpm lint` — erwartet `5 successful, 5 total`.
2. Die Biome-JSON-Zaehlung aus Task 3 — erwartet `total 434`,
`sev {'warning': 433, 'info': 1}`, `noControlCharactersInRegex 0`,
`useIterableCallbackReturn 0`, `noGlobalIsNan 0`, `noAssignInExpressions 4`,
`noDocumentCookie 17`, `suppressions/unused 0`.
3. `pnpm type-check` — erwartet `4 successful, 4 total`.
4. `pnpm --filter @tessera/web test` — erwartet >= 68 Dateien, >= 480 Tests, 0 failed.
5. `pnpm --filter @tessera/api test` — erwartet 72 Dateien, >= 1137 Tests, 0 failed.
6. Byte-Scan von `apps/web/src/lib/safe-next.ts` — erwartet `CTRL_BYTES 0 []`.
7. `git status --porcelain -- apps packages` — erwartet genau die 13 Dateien aus
`files_modified`, keine weitere. Schlaegt das fehl, wurde formatiert oder
umgeraeumt, was D-05 verbietet. (`.planning/` ist hier bewusst
ausgeklammert: Plan und Zusammenfassung gehoeren dazu.)
8. `git diff -- package.json '*/package.json' pnpm-lock.yaml` — erwartet leer:
keine neue Abhaengigkeit, keine Versionsanhebung (D-05).
Kein laufender Stack noetig: alle zwoelf Befunde liessen sich statisch beurteilen,
und die zwei riskanten Stellen (Zeichenraum des Weiterleitungsschutzes,
Parser-Schleifen) sind mit Tests besser abgedeckt als mit einer Bedienprobe.
Die Aenderung am Sprachumschalter ist im Browser nachpruefbar, falls der Nutzer
es wuenscht — sie ist es nicht wert, dafuer einen Stack hochzufahren.
</verification>
<success_criteria>
- Zwoelf Befunde abgeschlossen, jeder mit Urteil und einer Begruendung, die
benennt, was ohne die Entscheidung schiefginge (D-02). Die sieben als
gleichwertig bzw. absichtlich beurteilten Stellen sind als solche benannt und
nicht nachtraeglich zum Fehler erklaert.
- `sanitizeNextPath` weist nach dem Umbau exakt dieselben 54 Codepunkte ab wie
vorher, nachgewiesen ueber alle 65536 Codepunkte aus U+0000..U+FFFF (D-01).
- `apps/web/src/lib/safe-next.ts` enthaelt kein rohes Steuerbyte mehr, und der
Kommentar beschreibt jetzt zutreffend, was in der Zeile darunter steht (D-01).
- Der Weiterleitungsschutz wurde nicht gelockert: Praefixtest, Laengenbegrenzung,
Pruefreihenfolge und die bewusst fehlende Schema-Liste unveraendert (D-04).
- Ausser den drei Stellen, an denen eine Haertung ausdruecklich beschlossen wurde
(Escape-Schreibweise, `Number.isNaN`, `SameSite=Lax`), aendert sich kein
beobachtbares Verhalten (D-03).
- Keine neue Abhaengigkeit, keine Versionsanhebung, keine Formatierung ausserhalb
der 13 genannten Dateien (D-05).
- `pnpm lint` bleibt 5/5 bei 0 Befunden der Stufe error; die Warnungszahl faellt
von 445 auf 433 und waechst an keiner anderen Stelle (D-06).
- `noArrayIndexKey` und `noNonNullAssertion` wurden nicht angefasst — sie gehoeren
in den Folgevorgang. `noUselessSwitchCase` bleibt der dokumentierte
Nicht-Fix aus 260921-bi2.
</success_criteria>
<output>
Create `.planning/quick/260921-i8x-fehlerverdaechtige-lint-klassen-steuerze/260921-i8x-SUMMARY.md` when done.
Die Zusammenfassung enthaelt die Urteilstabelle aus `<verdicts>` mit dem
tatsaechlich Umgesetzten je Zeile, die Vorher-/Nachher-Zahlen aus Abschnitt
`<verification>` und — ausdruecklich — den Satz, dass sieben der zwoelf Befunde
keine Fehler waren.
</output>
@@ -0,0 +1,261 @@
---
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 `<verdicts>` 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 `<verification>`)
| 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 `<verify>`-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.
@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
/**
* DkvParserService.spec — Rueckfall-Tor fuer den exec-Schleifenumbau in
* parseDkvText (quick-260921-i8x, Task 3, T-i8x-04, RED first).
*
* `parseDkvText` hat heute keine eigene Abdeckung — `parsePdf` wird hier
* ueber eine gemockte `pdf-parse`-Attrappe (Muster: tender-mail.service.spec.ts
* fuer vi.mock auf ein Paket) mit einem festen DKV-Beispieltext mit zwei
* Fahrzeugbloecken gefuettert. Dieser Test haelt Kennzeichen, Kartennummern,
* Rechnungsnummer und Rechnungsdatum fest, bevor die `while ((match = ...))`-
* Schleife auf eine verhaltensgleiche `for`-Schleife umgeschrieben wird.
*/
vi.mock('pdf-parse', () => ({
PDFParse: vi.fn().mockImplementation(() => ({
getText: vi.fn(async () => ({ text: SAMPLE_DKV_TEXT })),
destroy: vi.fn(async () => undefined),
})),
}));
const SAMPLE_DKV_TEXT = `DKV Euro Service GmbH + Co. KG
Rechnung
12/123456789/001
15.04.2026
VEHICLE: GP-JL 728E CARD NO.: 1234567890123456
01.04.2026\tESSO\tDONZDORF\t0071132\t189945\t68040 DIESEL\t0009 LTR\t45,910\t0\t0\t60,00\t50,00
VEHICLE: GP ML 720 CARD NO.: 9876543210987654
02.04.2026\tARAL\tSTUTTGART\t0089221\t204983\t72150 DIESEL\t0011 LTR\t38,220\t0\t0\t55,00\t45,00
`;
describe('DkvParserService.parsePdf', () => {
let DkvParserService: typeof import('./dkv-parser.service').DkvParserService;
beforeEach(async () => {
vi.clearAllMocks();
({ DkvParserService } = await import('./dkv-parser.service'));
});
it('liefert zwei Fahrzeugbloecke sowie Rechnungsnummer und -datum aus dem Beispieltext', async () => {
const service = new DkvParserService();
const result = await service.parsePdf(Buffer.from('irrelevant'));
expect(result.vehicles).toHaveLength(2);
expect(result.vehicles[0].kennzeichen).toBe('GP-JL 728E');
expect(result.vehicles[0].cardNumber).toBe('1234567890123456');
expect(result.vehicles[0].transactions).toHaveLength(1);
expect(result.vehicles[0].transactions[0]).toMatchObject({
lieferdatum: '01.04.2026',
ort: 'DONZDORF',
produkt: 'DIESEL',
einheit: 'LTR',
});
expect(result.vehicles[1].kennzeichen).toBe('GP ML 720');
expect(result.vehicles[1].cardNumber).toBe('9876543210987654');
expect(result.vehicles[1].transactions).toHaveLength(1);
expect(result.vehicles[1].transactions[0]).toMatchObject({
lieferdatum: '02.04.2026',
ort: 'STUTTGART',
produkt: 'DIESEL',
einheit: 'LTR',
});
expect(result.rechnungsnummer).toBe('12/123456789/001');
expect(result.rechnungsdatum).toBe('15.04.2026');
});
});
+5 -2
View File
@@ -100,8 +100,11 @@ export class DkvParserService {
const vehicleBlockPattern =
/VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
let match: RegExpExecArray | null;
while ((match = vehicleBlockPattern.exec(text)) !== null) {
for (
let match = vehicleBlockPattern.exec(text);
match !== null;
match = vehicleBlockPattern.exec(text)
) {
const kennzeichen = match[1].trim();
const cardNumber = match[2].trim();
const blockText = match[3];
+5 -2
View File
@@ -135,8 +135,11 @@ function parseDkvText(text: string): DkvVehicleBlock[] {
const vehicleBlockPattern =
/VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
let match: RegExpExecArray | null;
while ((match = vehicleBlockPattern.exec(text)) !== null) {
for (
let match = vehicleBlockPattern.exec(text);
match !== null;
match = vehicleBlockPattern.exec(text)
) {
const kennzeichen = match[1].trim();
const cardNumber = match[2].trim();
const blockText = match[3];
@@ -177,9 +177,8 @@ function getOriginFaviconUrl(pageUrl: string): string {
function parseAttributes(tag: string): Record<string, string> {
const attrs: Record<string, string> = {};
const re = /([a-zA-Z_:.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
let m: RegExpExecArray | null;
while ((m = re.exec(tag)) !== null) {
for (let m = re.exec(tag); m !== null; m = re.exec(tag)) {
const key = m[1].toLowerCase();
const value = m[3] ?? m[4] ?? m[5] ?? '';
+1
View File
@@ -1659,6 +1659,7 @@ export class LdapService {
.replace(/\*/g, '\\2a')
.replace(/\(/g, '\\28')
.replace(/\)/g, '\\29')
// biome-ignore lint/suspicious/noControlCharactersInRegex: RFC 4515 verlangt genau dieses Steuerzeichen als \00-Maskierung; ohne sie kann ein unmaskiertes NUL den Filter beim Verzeichnisserver abschneiden (LDAP-Filter-Injection).
.replace(/\x00/g, '\\00');
}
@@ -151,7 +151,9 @@ export async function mergeCertsAction(
password?: string,
): Promise<FileResponse> {
const form = new FormData();
files.forEach((file) => form.append('files', file));
files.forEach((file) => {
form.append('files', file);
});
form.append('outputFormat', outputFormat);
if (password) form.append('password', password);
return postForm('merge', form) as Promise<FileResponse>;
@@ -30,7 +30,7 @@ function formatDateTime(isoString: string): string {
// new Date() never throws — it returns an Invalid Date object for bad input.
// Check explicitly so malformed strings fall back gracefully instead of
// rendering as "NaN.NaN.NaN, NaN:NaN Uhr" (WR-03).
if (isNaN(d.getTime())) return isoString;
if (Number.isNaN(d.getTime())) return isoString;
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
@@ -48,7 +48,7 @@ interface TriageState {
function formatDate(value: string | null): string {
if (!value) return '–';
const d = new Date(value);
if (isNaN(d.getTime())) return '–';
if (Number.isNaN(d.getTime())) return '–';
return d.toLocaleDateString('de-DE');
}
@@ -41,7 +41,7 @@ interface TenderDetailProps {
function formatDeadline(value: string | null, noDeadlineLabel: string): string {
if (!value) return noDeadlineLabel;
const d = new Date(value);
if (isNaN(d.getTime())) return noDeadlineLabel;
if (Number.isNaN(d.getTime())) return noDeadlineLabel;
return d.toLocaleDateString('de-DE');
}
@@ -62,7 +62,7 @@ function formatValue(value: string | null, noValueLabel: string): string {
function formatPublished(value: string): string {
const d = new Date(value);
if (isNaN(d.getTime())) return '–';
if (Number.isNaN(d.getTime())) return '–';
return d.toLocaleDateString('de-DE');
}
@@ -0,0 +1,61 @@
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { LocaleSwitcher } from './locale-switcher';
/**
* locale-switcher.test — SameSite-Luecke im Sprachumschalter-Cookie
* (quick-260921-i8x, Task 3 Teil C, T-i8x-05, RED first).
*
* Muster: header.test.tsx fuer next-intl/next-navigation-Mocks. Der Setter
* von `document.cookie` wird per `Object.defineProperty` durch eine
* Attrappe ersetzt, die jede geschriebene Zeichenkette sammelt — vor der
* Aenderung faellt nur die SameSite-Zusicherung durch, das ist beabsichtigt
* und zeigt genau die Luecke (T-i8x-05).
*/
vi.mock('next-intl', () => ({
useTranslations: () => (key: string) => {
const translations: Record<string, string> = {
de: 'Deutsch',
en: 'English',
};
return translations[key] ?? key;
},
useLocale: () => 'de',
}));
const refreshMock = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({ refresh: refreshMock }),
}));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe('LocaleSwitcher', () => {
it('schreibt beim Klick genau ein Cookie mit NEXT_LOCALE, path=/, max-age=31536000 und SameSite=Lax', async () => {
const writtenCookies: string[] = [];
Object.defineProperty(document, 'cookie', {
configurable: true,
get: () => '',
set: (value: string) => {
writtenCookies.push(value);
},
});
const user = userEvent.setup();
render(<LocaleSwitcher />);
await user.click(screen.getByRole('button'));
expect(writtenCookies).toHaveLength(1);
const written = writtenCookies[0];
expect(written).toContain('NEXT_LOCALE=en');
expect(written).toContain('path=/');
expect(written).toContain('max-age=31536000');
expect(written).toContain('SameSite=Lax');
});
});
+2 -1
View File
@@ -12,7 +12,8 @@ export function LocaleSwitcher() {
function switchLocale() {
const newLocale = locale === 'de' ? 'en' : 'de';
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
// biome-ignore lint/suspicious/noDocumentCookie: die von Biome vorgeschlagene Cookie-Store-API fehlt in WebKit, die Sprachumschaltung waere damit im Tauri-Wrapper auf Linux und macOS wirkungslos.
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000;SameSite=Lax`;
startTransition(() => {
router.refresh();
});
+54
View File
@@ -1,6 +1,30 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { buildNextParam, sanitizeNextPath } from './safe-next';
// Die 54 Codepunkte, die `sanitizeNextPath` bei der Planung von quick-260921-i8x
// ueber alle 65536 Codepunkte aus U+0000..U+FFFF gemessen abweist (SHA-256 der
// Bitmap: 3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64).
// Hart hinterlegt, nicht aus der Regel abgeleitet (D-01): eine aus derselben
// Regel erzeugte Erwartung wuerde jede Aenderung der Regel mitmachen und
// bewiese damit nichts.
const EXPECTED_REJECTED_CODEPOINTS: number[] = [
...Array.from({ length: 0x20 }, (_, i) => i), // 0x00-0x1f
0x20,
0x5c,
0x7f,
0xa0,
0x1680,
...Array.from({ length: 0x200a - 0x2000 + 1 }, (_, i) => 0x2000 + i), // 0x2000-0x200a
0x2028,
0x2029,
0x202f,
0x205f,
0x3000,
0xfeff,
];
/**
* safe-next.test — Ruecksprung nach Anmeldung (quick-260917-gyd).
*
@@ -70,4 +94,34 @@ describe('sanitizeNextPath', () => {
expect(sanitizeNextPath('/login/')).toBe('/');
expect(sanitizeNextPath('/loginhistory')).toBe('/loginhistory');
});
it('Test A: die abgewiesene Zeichenmenge ist und bleibt genau diese 54 Codepunkte (T-i8x-01)', () => {
const rejected: number[] = [];
for (let cp = 0; cp <= 0xffff; cp++) {
const input = `/a${String.fromCharCode(cp)}b`;
if (sanitizeNextPath(input) === '/') {
rejected.push(cp);
}
}
expect(rejected).toEqual(EXPECTED_REJECTED_CODEPOINTS);
expect(rejected).toHaveLength(54);
});
it('Test B: die Datei enthaelt kein rohes Steuerzeichen', () => {
const filePath = join(import.meta.dirname, 'safe-next.ts');
const bytes = readFileSync(filePath);
let count = 0;
for (const byte of bytes) {
if ((byte < 0x20 && byte !== 0x0a) || byte === 0x7f) {
count++;
}
}
expect(count).toBe(0);
});
it('Test C: der Zwei-Zeichen-Praefixtest bleibt scharf', () => {
expect(sanitizeNextPath('//host')).toBe('/');
expect(sanitizeNextPath('/\\host')).toBe('/');
expect(sanitizeNextPath(`/${String.fromCharCode(0x00)}`)).toBe('/');
});
});
Binary file not shown.