feat(quick-260921-iwr): Rundenliste der Stoppuhr per Test belegt, MergeTab-Entfernen abgesichert

- Wirkungslose eslint-disable-Zeile in stopwatch-widget.tsx ersetzt durch
  Sachhinweis: Zeilen halten keinen Zustand, Rundennummer wird aus Laenge
  und Position berechnet. Keine neue Unterdrueckung, ARRAYKEY bleibt bei 19.
- Neuer Testfall in stopwatch-widget.test.tsx: zwei Runden nacheinander,
  neuere Runde steht oben, Rundennummern 2/1 stimmen zu ihrer eigenen Zeit.
- Neue MergeTab.test.tsx: Entfernen der mittleren Datei laesst genau erste
  und dritte Datei mit eigenem Namen und eigenem Entfernen-Knopf uebrig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
2026-09-21 13:55:42 +02:00
parent cfba3c9532
commit 8716fa5234
4 changed files with 527 additions and 1 deletions
@@ -0,0 +1,413 @@
---
phase: quick-260921-iwr
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/api/src/cert-manager/cert-manager.service.ts
- apps/api/src/cert-manager/cert-manager.service.spec.ts
- apps/api/src/inbox/imap.provider.ts
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
- apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
estimate:
tokens: 78000
raw_tokens: 39000
tasks: 3
confidence: low
must_haves:
truths:
- "Jede der 30 Fundstellen traegt ein Urteil mit einer Begruendung, die die Folge benennt, nicht die Regel (D-01)."
- "Biome meldet danach ARRAYKEY=19, NONNULL=6, ERRORS=0, TOTAL=429 — genau die 5 tatsaechlich geaenderten Stellen weniger (D-06)."
- "Eine hochgeladene PFX-Datei mit unlesbarem Zertifikats-Bag wird mit einer praezisen 400-Meldung abgewiesen statt mit einer irrefuehrenden."
- "Kein Zertifikat wird still falsch ausgegeben und keine Eingabepruefung wurde abgeschwaecht (D-04)."
- "pnpm lint bleibt 5/5, pnpm type-check 4/4, beide Testsuiten mindestens auf Ausgangsstand."
artifacts:
- apps/api/src/cert-manager/cert-manager.service.ts
- apps/api/src/cert-manager/cert-manager.service.spec.ts
- apps/api/src/inbox/imap.provider.ts
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
- .planning/quick/260921-iwr-listenschluessel-per-positionsnummer-und/260921-iwr-SUMMARY.md
key_links:
- "Die drei bag.cert-Waechter haengen an node-forge lib/pkcs12.js Zeile 708 (bag.cert = null bei unlesbarem X.509) — das ist der einzige Grund, warum die Zusicherung sachlich falsch ist."
- "Die 400-Zusage haengt an den catch-Bloecken in Zeile 232, 486, 533 und 625: BadRequestException wird unveraendert durchgereicht, alles andere wird zu 400 umgewandelt."
- "Die Unbedenklichkeit aller 19 Positionsschluessel haengt an einer einzigen Tatsache: keine der betroffenen Zeilen haelt Zustand, den React pro Schluessel fuehrt."
---
<objective>
Die 30 gemeldeten Fundstellen zweier Lint-Klassen einzeln beurteilen und nur dort eingreifen,
wo die Beurteilung einen echten Grund liefert.
Purpose: Letzte Fehlerklasse vor den beiden neuen Widgets. Es geht um ein belastbares Urteil je
Stelle, nicht um eine niedrigere Zahl.
Output: 5 begruendete Aenderungen, 25 begruendet stehengelassene Fundstellen, neue Tests fuer die
beiden Stellen, an denen die Beurteilung nicht offensichtlich war.
</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/api/src/cert-manager/cert-manager.service.ts
@apps/api/src/inbox/imap.provider.ts
@apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
</context>
<planning_observations>
Beim Planen am 2026-09-21 nachgemessen und nachgewiesen — der Ausfuehrende muss das nicht
wiederholen, aber er darf es nicht ungeprueft umstossen:
**Ausgangsstand (identisch zur Vorgabe des Orchestrators):**
TOTAL=434, ERRORS=0, ARRAYKEY=19, NONNULL=11; web 68 Dateien / 481 Tests,
api 72 / 1137, pnpm type-check 4/4, pnpm lint 5/5.
**Die Vermutung zu admin/ldap ist widerlegt.** Die Liste, die dort wirklich waechst und
schrumpft, ist `config.fieldMappings` in Zeile 712 — sie benutzt bereits `key={mapping.id}`.
Die 6 gemeldeten Stellen sind etwas anderes: reine Meldungslisten
(`nameCollisions`, `errors`, `emailConflicts`, `skippedNoLogin`, `entryFailures`, `errors`),
die als zustandslose Absaetze gerendert und beim naechsten Lauf komplett ersetzt werden
(setSyncResult(null) in Zeile 284, dann setSyncResult(data) in Zeile 293).
**node-forge kann bag.cert auf null setzen** — lib/pkcs12.js Zeile 703-709: wenn
`certificateFromAsn1` wirft, faengt node-forge das ab und setzt `bag.cert = null`. Die drei
Zusicherungen 207/469/602 behaupten also etwas, das nicht gilt.
**Die Folge davon ist aber bereits behandelt.** Mit einer eigens gebauten 83-Byte-PFX-Datei
(Zertifikats-Bag mit wohlgeformtem, aber unlesbarem Inhalt) gegen den echten Service gemessen:
| Pfad | heutiges Ergebnis |
|------|-------------------|
| parseCert | 400 "Failed to extract certificate details" |
| mergeCerts pem | 400 "Failed to create merged certificate output" |
| mergeCerts pfx | 400 "Failed to create merged certificate output" |
| convertCert | 400 "Failed to convert certificate to pem: serialization error" |
Kein 500, kein Absturz. Zusaetzlich direkt an node-forge geprueft: `certificateToPem(null)` und
`toPkcs12Asn1(null, [cert, null], pw)` werfen beide eine TypeError — die Bibliothek laesst ein
fehlendes Zertifikat **nie** still durchrutschen. Das in D-04 befuerchtete "still falsches
Zertifikat" ist damit ausgeschlossen, nicht nur unwahrscheinlich.
Daraus folgt die Einstufung: die drei Stellen sind **keine Verfuegbarkeits- und keine
Integritaetsluecke**. Was bleibt, ist eine sachlich falsche Zusicherung und eine irrefuehrende
Fehlermeldung. Der Eingriff ist deshalb als Diagnose-/Lesbarkeitsarbeit zu fuehren, nicht als
Fehlerbehebung.
**imapflow typisiert `uid` als Pflichtfeld** (lib/imap-flow.d.ts Zeile 469 in
`FetchMessageObject`, Kommentar "Always included in the response"). Das `!` ist dort schlicht
ueberfluessig. Entfernen wurde beim Planen probeweise durchgefuehrt: `tsc --noEmit` in apps/api
endet mit 0. Die Aenderung wurde danach zurueckgenommen, der Baum ist sauber.
**ESLint existiert in diesem Repo nicht** (kein Paket, keine Konfigurationsdatei; Treffer nur in
mitgelieferten Fremdpaketen unter .next/standalone). Der Unterdrueckungskommentar in
stopwatch-widget.tsx Zeile 277 wirkt daher nicht.
**Zaehlbefehl** (in jedem verify benutzt, Kategorien kommen aus dem JSON-Feld `category`,
nie aus dem Quelltext):
`npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const c=r=>real.filter(x=>x.category===r).length;console.log("TOTAL="+ds.length+" ERRORS="+ds.filter(x=>x.severity==="error").length+" ARRAYKEY="+c("lint/suspicious/noArrayIndexKey")+" NONNULL="+c("lint/style/noNonNullAssertion"));});'`
Er meldet im Ausgangsstand `TOTAL=434 ERRORS=0 ARRAYKEY=19 NONNULL=11`.
</planning_observations>
<tasks>
<task type="tracer">
<name>Task 1: Die 19 Positionsschluessel beurteilen und die zwei nicht offensichtlichen Faelle nachmessen</name>
<files>
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx,
apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx,
apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.test.tsx
</files>
<read_first>
apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx,
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx,
apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
</read_first>
<behavior>
- MergeTab: drei Dateien auswaehlen, die mittlere ueber ihren Entfernen-Knopf loeschen; danach
stehen genau die erste und die dritte Datei in der Liste, in dieser Reihenfolge, mit ihren
eigenen Namen. Das ist der Nachweis, dass der Positionsschluessel beim Schrumpfen der Liste
nichts verwechselt.
- Stopwatch: zwei Runden nacheinander aufzeichnen; die Liste zeigt die neuere Runde oben, die
Rundennummern lauten von oben nach unten 2 und 1, und die beiden Zeiten stehen bei der
jeweils richtigen Nummer. Das ist der Nachweis fuer die Liste, die von vorn waechst.
</behavior>
<action>
Beurteile alle 19 Fundstellen einzeln und halte je Stelle ein Urteil mit Folgenbegruendung fest
(D-01). Die Beurteilung aus den planning_observations ist nachgewiesen und darf uebernommen
werden; wenn du beim Lesen etwas anderes siehst, hat deine Beobachtung Vorrang und du sagst es.
Erwartete Einstufung, nach Muster gruppiert:
(a) Zwoelf Stellen sind Ladeplatzhalter aus Array.from mit fester Laenge ohne Inhalt —
InvoiceHistoryTable 129 und 131, VehicleTable 342 und 344, ResultsList 240 und 242,
InboxConfigForm 251, EmailAlertConfigForm 225, RssFeedListForm 151, SourceConfigForm 129.
Dort gibt es ausser der Position ueberhaupt keine Identitaet, die Laenge ist konstant, die
Reihenfolge aendert sich nie. Harmlos.
(b) Sechs Stellen in admin/ldap sind Meldungslisten, die beim naechsten Lauf komplett ersetzt
werden und deren Zeilen nur Text enthalten. Harmlos. Halte dabei ausdruecklich fest, dass die
Liste, die dort tatsaechlich waechst und schrumpft, bereits mapping.id benutzt.
(c) grants/page.tsx 246: die Gruppierung in der useMemo ab Zeile 168 fasst nur unmittelbar
aufeinanderfolgende Module gleicher Kategorie zusammen, dieselbe Kategorie kann also mehrfach
vorkommen. Der Positionsanteil im Schluessel ist deshalb noetig; ihn zu entfernen wuerde
doppelte Schluessel erzeugen. Die Haken haengen ohnehin nicht am Schluessel, sondern an der
aeusseren Menge grants ueber cellKey(mod.id, g.id). Harmlos, und der Index bleibt bewusst.
(d) MergeTab 87 und stopwatch-widget 276: die einzigen beiden Listen, die sich waehrend der
Anzeige wirklich veraendern. Belege deren Unbedenklichkeit mit den beiden Tests aus dem
behavior-Block, statt sie nur zu behaupten.
Aendere keinen einzigen Schluessel im Produktivcode. Fuer die zwoelf Platzhalter und die sechs
Meldungslisten gibt es keine stabile Identitaet, die ungenutzt herumlaege; bei grants waere die
Aenderung sogar schaedlich; bei den Runden der Stoppuhr saesse eine echte Identitaet nur in der
gespeicherten Form laps als Zahlenliste, deren Umbau bereits abgelegte Widget-Konfigurationen
und bestehende Tests brechen wuerde — das waere eine Verhaltensaenderung ohne Gegenwert und
unterbleibt (D-02).
Entferne in stopwatch-widget.tsx die wirkungslose Unterdrueckungszeile ueber dem Schluessel
(sie nennt eine Regel eines Linters, den dieses Projekt nicht einsetzt) und setze an ihre
Stelle einen kurzen Sachhinweis, warum die Position hier als Schluessel vertretbar ist: die
Zeilen halten keinen Zustand, und die angezeigte Rundennummer wird ohnehin aus Laenge und
Position berechnet. Fuege keine neue Unterdrueckung hinzu (D-06) — die Fundstelle bleibt
ausdruecklich in der Zaehlung stehen.
Lege MergeTab.test.tsx neu an. Orientiere dich an den Mustern in stopwatch-widget.test.tsx
fuer render, fireEvent und die Uebersetzungsattrappe. Die Dateiauswahl laeuft ueber das
verborgene Dateifeld; setze die Dateien per fireEvent.change mit echten File-Objekten. Den
Entfernen-Knopf findest du ueber sein aria-label, das den Dateinamen enthaelt. Ergaenze den
Rundentest in stopwatch-widget.test.tsx als zusaetzlichen Fall, ohne bestehende Faelle zu
veraendern.
ARRAYKEY bleibt danach bei 19. Das ist das erwartete Ergebnis, kein Versaeumnis.
</action>
<verify>
<automated>cd apps/web && npx vitest run src/app/\(portal\)/modules/cert-manager/components/MergeTab.test.tsx src/components/dashboard/widgets/stopwatch-widget.test.tsx</automated>
<automated>cd apps/web && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 69 Dateien, mindestens 484 Tests, alle gruen</automated>
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const c=r=>real.filter(x=>x.category===r).length;console.log("ARRAYKEY="+c("lint/suspicious/noArrayIndexKey")+" ERRORS="+ds.filter(x=>x.severity==="error").length);});' # erwartet: ARRAYKEY=19 ERRORS=0</automated>
</verify>
<done>
Alle 19 Fundstellen haben ein Urteil mit Folgenbegruendung. Kein Schluessel im Produktivcode
geaendert. Die wirkungslose Unterdrueckungszeile ist weg, keine neue hinzugekommen. Die beiden
Tests belegen, dass Entfernen in der Mitte und Wachsen von vorn keine Zeile verwechseln. Die
Web-Suite ist gewachsen und gruen, ARRAYKEY steht unveraendert bei 19.
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Die sechs Zusicherungen in cert-manager.service.ts beurteilen, drei davon zu echten Waechtern machen</name>
<files>
apps/api/src/cert-manager/cert-manager.service.ts,
apps/api/src/cert-manager/cert-manager.service.spec.ts
</files>
<read_first>
apps/api/src/cert-manager/cert-manager.service.ts,
apps/api/src/cert-manager/cert-manager.service.spec.ts
</read_first>
<precondition>node_modules ist installiert und node-forge 1.4.0 aufloesbar (die Tests bauen die Pruefdatei mit forge.asn1 selbst).</precondition>
<behavior>
Pruefgegenstand ist eine PFX-Datei, deren Zertifikats-Bag wohlgeformt, aber kein lesbares
X.509 ist. node-forge setzt bag.cert dann auf null.
- parseCert mit dieser Datei wirft BadRequestException (Status 400) und die Meldung benennt,
dass der Zertifikats-Bag kein lesbares X.509-Zertifikat enthaelt. Heute lautet sie
"Failed to extract certificate details" — der Test ist vor dem Waechter rot.
- mergeCerts mit dieser Datei wirft 400 mit derselben praezisen Aussage, sowohl bei
outputFormat pem als auch bei pfx. Heute lautet sie beide Male
"Failed to create merged certificate output" — vor dem Waechter rot.
- convertCert mit dieser Datei wirft 400 mit der praezisen Aussage. Heute lautet sie
"Failed to convert certificate to pem: serialization error" — vor dem Waechter rot.
- Eine gueltige PFX-Datei verhaelt sich unveraendert: parseCert, mergeCerts und convertCert
liefern weiterhin ihr bisheriges Ergebnis. Die bestehenden Faelle in der Spezifikation
bleiben gruen.
</behavior>
<action>
Beurteile alle sechs Fundstellen einzeln (D-01) und benenne bei jeder sicheren die Zeile, die
sie garantiert.
Sicher und unveraendert zu lassen:
Zeile 133 — die Zerlegung des Fingerabdrucks in Zweiergruppen. Ein SHA-1- oder
SHA-256-Hexwert ist immer 40 beziehungsweise 64 Zeichen lang, die Suche findet also immer
etwas. Garantiert durch die Laenge des Hashes, nicht durch Eingabedaten.
Zeile 516 — das Kennwort beim Erzeugen einer PFX-Ausgabe im Zusammenfuehren. Garantiert durch
die Pruefung in Zeile 438 bis 440, die bei fehlendem oder leerem Kennwort vorher mit 400
abbricht.
Zeile 661 — dasselbe im Umwandeln. Garantiert durch die Pruefung in Zeile 564 bis 566.
Zu aendern sind die drei Stellen, an denen die Zusicherung sachlich falsch ist, weil node-forge
bag.cert auf null setzen kann (lib/pkcs12.js Zeile 703 bis 709): Zeile 207 in parseCert,
Zeile 469 in mergeCerts, Zeile 602 in convertCert.
Schreibe zuerst die Tests aus dem behavior-Block und weise nach, dass sie mit den heutigen
Meldungen fehlschlagen. Baue die Pruefdatei im Test selbst mit forge.asn1 auf. Ihr Aufbau, von
aussen nach innen: eine SEQUENCE aus der INTEGER-Version 3 und einem ContentInfo; das
ContentInfo besteht aus der OID data und einem kontextspezifischen Element 0, das eine
OCTETSTRING mit dem DER des AuthenticatedSafe traegt; das AuthenticatedSafe ist eine SEQUENCE
aus einem weiteren ContentInfo gleicher Bauart, dessen OCTETSTRING das DER der SafeContents
traegt; die SafeContents sind eine SEQUENCE aus einem SafeBag; der SafeBag ist eine SEQUENCE
aus der OID certBag und einem kontextspezifischen Element 0, das eine SEQUENCE aus der OID
x509Certificate und einem kontextspezifischen Element 0 mit einer OCTETSTRING enthaelt; in
dieser OCTETSTRING steht das DER einer SEQUENCE mit einem einzigen INTEGER 1 — wohlgeformt,
aber kein Zertifikat. Als Kennwort dient die leere Zeichenkette. Die Datei ist rund 83 Byte
gross. Pruefe im Test vorab, dass genau ein Bag entsteht und dessen cert null ist; damit ist
belegt, dass der Waechter den gemeinten Fall trifft und nicht einen anderen.
Ersetze dann an den drei Stellen die Zusicherung durch eine ausdrueckliche Pruefung, die eine
BadRequestException mit einer praezisen Meldung wirft. In Zeile 207 und 602 lies bag.cert in
eine lokale Variable, pruefe sie und wirf bei fehlendem Wert. In Zeile 469 darf kein Eintrag
verschluckt werden: bilde die Bag-Liste auf ihre Zertifikate ab, pruefe, ob darunter ein
fehlendes ist, und wirf in dem Fall unter Nennung des Dateinamens — erst danach gib die Liste
zurueck, eingeengt ueber ein Typpraedikat statt ueber eine Zusicherung. Kein Filtern, kein
Ueberspringen, kein Ersatzwert (D-04).
Die BadRequestException wird von den catch-Bloecken in Zeile 232, 486 und 625 unveraendert
durchgereicht, die praezise Meldung kommt also beim Aufrufer an.
Ausdrueckliche Verhaltensaenderung nach D-02, vorab benannt und beabsichtigt: fuer genau diese
Eingabeklasse aendert sich der Text der Fehlermeldung. Der Statuscode bleibt in allen vier
Pfaden 400, der Vertrag nach aussen bleibt damit unveraendert. Gewollt ist, dass die Meldung
den tatsaechlichen Grund nennt statt eines irrefuehrenden Sammelbegriffs.
Halte in der Zusammenfassung fest, dass dies Diagnose- und Lesbarkeitsarbeit ist: die drei
Zusicherungen waren sachlich falsch, ihre Folge war aber bereits behandelt — nachgewiesen mit
400 statt 500 in allen vier Pfaden und damit, dass node-forge bei einem fehlenden Zertifikat
ausnahmslos wirft und nie still eine unvollstaendige Ausgabe erzeugt. Es war keine
Verfuegbarkeits- und keine Integritaetsluecke.
Das Kennwort darf weiterhin nirgends in eine Meldung oder ins Protokoll geraten (T-09-02).
</action>
<verify>
<automated>cd apps/api && npx vitest run src/cert-manager/cert-manager.service.spec.ts</automated>
<automated>cd apps/api && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 72 Dateien, mindestens 1141 Tests, alle gruen</automated>
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const n=real.filter(x=>x.category==="lint/style/noNonNullAssertion");console.log("NONNULL="+n.length+" CERTMGR="+n.filter(x=>x.location.path.includes("cert-manager.service.ts")).length);});' # erwartet: NONNULL=8 CERTMGR=3</automated>
</verify>
<done>
Alle sechs Fundstellen beurteilt, bei den drei sicheren ist die garantierende Zeile genannt.
Die drei falschen Zusicherungen sind ausdrueckliche Waechter mit praeziser 400-Meldung; die
Tests waren vorher rot und sind jetzt gruen. Keine Eingabepruefung abgeschwaecht, kein
Eintrag verschluckt, kein Ersatzwert eingefuehrt. Die api-Suite ist gewachsen und gruen.
NONNULL steht bei 8, davon 3 in cert-manager.service.ts.
</done>
</task>
<task type="auto">
<name>Task 3: Die restlichen fuenf Zusicherungen beurteilen, zwei ueberfluessige entfernen</name>
<files>apps/api/src/inbox/imap.provider.ts</files>
<read_first>
apps/api/src/inbox/imap.provider.ts,
apps/api/src/tenders/tenders.controller.ts,
apps/api/src/tenders/tenders.module.ts,
apps/web/src/components/dashboard/widgets/favorites-widget.tsx,
apps/web/src/components/layout/sidebar.tsx
</read_first>
<action>
Beurteile die fuenf verbliebenen Fundstellen einzeln (D-01) und benenne bei jeder sicheren die
Zeile oder den Umstand, der sie garantiert.
Zu aendern — imap.provider.ts Zeile 214 und 310, beide `uid: msg.uid!`: imapflow typisiert uid
in FetchMessageObject als Pflichtfeld (lib/imap-flow.d.ts Zeile 469, Kommentar "Always
included in the response"). Die Zusicherung sichert damit einen Wert ab, der ohnehin nicht
fehlen kann; sie ist ueberfluessig. Entferne an beiden Stellen nur das Ausrufezeichen und
sonst nichts. Das ist eine reine Lesbarkeitsaenderung ohne Verhaltensaenderung; tsc belegt sie.
Sicher und unveraendert zu lassen:
tenders.controller.ts Zeile 244 — der Fragezeichen-Parameter im Konstruktor ist nur dort, um
die Reihenfolge der Konstruktorargumente nicht zu brechen. Es gibt kein Optional-Merkmal, und
TenderIngestionService steht in tenders.module.ts unter providers, wird von Nest also immer
eingesetzt. Garantiert durch den Provider-Eintrag.
favorites-widget.tsx Zeile 149 — die Reihenfolge stammt aus sortedFavorites, und das ist laut
Zeile 90 bis 97 nur eine sortierte Kopie von favorites. Die Zuordnungstabelle wird aus
denselben Eintraegen gebaut, jede gesuchte Kennung ist also enthalten. Garantiert durch die
Herleitung in Zeile 90 bis 97.
sidebar.tsx Zeile 80 — unmittelbar darueber, in Zeile 79, wird der Eintrag angelegt, falls er
fehlt. Garantiert durch die Zeile direkt davor.
Diese drei bleiben in der Zaehlung stehen, und das wird in der Zusammenfassung gesagt (D-06).
Ersetze sie nicht durch gleichwertige Abfragen, nur um die Zahl zu druecken — sie sind sicher,
und ein Umbau waere Geraeusch ohne Gewinn.
Fuege nirgends eine Unterdrueckung hinzu und aendere keine Abhaengigkeiten (D-05).
</action>
<verify>
<automated>cd apps/api && npx tsc --noEmit; echo "tsc=$?" # erwartet: tsc=0</automated>
<automated>cd apps/api && npx vitest run 2>&1 | tail -4 # erwartet: mindestens 72 Dateien, mindestens 1141 Tests, alle gruen</automated>
<automated>npx biome lint --reporter=json . 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const ds=JSON.parse(s).diagnostics||[];const isTest=p=>p.includes(".spec.")||p.includes(".test.")||p.includes("__tests__")||p.includes("__fixtures__")||p.includes("__mocks__");const real=ds.filter(x=>!isTest(x.location.path));const n=real.filter(x=>x.category==="lint/style/noNonNullAssertion");console.log("NONNULL="+n.length+" IMAP="+n.filter(x=>x.location.path.includes("imap.provider.ts")).length);});' # erwartet: NONNULL=6 IMAP=0</automated>
</verify>
<done>
Alle fuenf beurteilt. Die beiden ueberfluessigen Ausrufezeichen in imap.provider.ts sind weg,
tsc ist gruen. Die drei sicheren stehen unveraendert und ihre Begruendung nennt jeweils die
garantierende Zeile. NONNULL steht bei 6, keines davon mehr in imap.provider.ts.
</done>
</task>
</tasks>
<threat_model>
ASVS Level 1, block_on high.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser zu API, Datei-Upload | Zertifikatsdateien kommen ungeprueft vom Benutzer und werden von node-forge zerlegt |
| Browser zu Admin-Oberflaeche | admin/ldap konfiguriert Verzeichnisbindungen; eine falsch zugeordnete Zeile wirkt hier unmittelbar auf Zugaenge |
| IMAP-Server zu API | Nachrichtenkopfdaten stammen aus einer fremden Quelle |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-iwr-01 | Tampering | admin/ldap/page.tsx Listen | medium | accept | Beim Planen widerlegt: die veraenderliche Liste nutzt bereits mapping.id, die 6 gemeldeten Listen sind zustandslose Meldungen, die komplett ersetzt werden. Kein Bedienpfad, auf dem ein Eintrag an der falschen Zeile haengen bleibt. |
| T-iwr-02 | Denial of Service | cert-manager.service.ts, bag.cert bei Upload | high | mitigate | Zusicherung kann durch eine praeparierte PFX-Datei verletzt werden. Gemessen: alle vier Pfade enden bereits mit 400, nie 500. Task 2 setzt ausdrueckliche Waechter mit praeziser Meldung; Test mit echter Pruefdatei sichert das ab. |
| T-iwr-03 | Tampering | mergeCerts, Zertifikatsliste aus PFX-Bags | high | mitigate | Ein fehlendes Zertifikat koennte stillschweigend ein unvollstaendiges Buendel erzeugen. An node-forge nachgemessen: sowohl certificateToPem als auch toPkcs12Asn1 werfen bei einem fehlenden Eintrag. Task 2 lehnt zusaetzlich vor der Serialisierung ausdruecklich ab, ohne zu filtern (D-04). |
| T-iwr-04 | Information Disclosure | Fehlermeldungen mit Dateinamen | low | accept | Der Dateiname ist benutzergesteuert, erscheint aber bereits heute so in Zeile 467. Kennwoerter bleiben aus Meldung und Protokoll ausgeschlossen (T-09-02). |
| T-iwr-05 | Spoofing | imap.provider.ts, uid aus Serverantwort | low | accept | uid ist in imapflow ein Pflichtfeld; das Entfernen des Ausrufezeichens aendert nichts am Wert, nur an der ueberfluessigen Zusicherung. |
| T-iwr-SC | Tampering | Paketinstallationen | n/a | accept | Dieser Vorgang installiert nichts und aendert keine Abhaengigkeit (D-05). Keine Paketpruefung noetig. |
</threat_model>
<verification>
Nach allen drei Aufgaben, aus dem Wurzelverzeichnis:
1. `npx biome lint --reporter=json .` durch den Zaehlbefehl aus den planning_observations —
erwartet `TOTAL=429 ERRORS=0 ARRAYKEY=19 NONNULL=6`. TOTAL faellt um genau 5, also um die
Zahl der tatsaechlich geaenderten Stellen, und steigt nirgends anders an (D-06).
2. `pnpm lint` — erwartet 5 successful, 5 total, 0 error-severity.
3. `pnpm type-check` — erwartet 4 successful, 4 total.
4. `cd apps/web && npx vitest run` — mindestens 69 Dateien, mindestens 484 Tests, alle gruen.
5. `cd apps/api && npx vitest run` — mindestens 72 Dateien, mindestens 1141 Tests, alle gruen.
6. `git status --porcelain` — nur die in files_modified genannten Pfade plus die Zusammenfassung.
Kein laufender Stapel noetig: beide beurteilten Klassen sind im Test vollstaendig nachweisbar,
fuer die Listenschluessel ist der Komponententest ohnehin das schaerfere Werkzeug als der Browser.
</verification>
<success_criteria>
- Alle 30 Fundstellen haben ein Urteil mit einer Begruendung, die die Folge benennt (D-01).
- Die 5 geaenderten Stellen sind verschwunden, die 25 stehengelassenen stehen weiter in der
Zaehlung und sind als bewusst stehengelassen benannt (D-06).
- Die einzige Verhaltensaenderung — praezisere Fehlermeldung bei unlesbarem Zertifikats-Bag, Status
weiterhin 400 — ist vorab benannt und ist die beabsichtigte (D-02).
- Keine Eingabepruefung abgeschwaecht, kein Eintrag verschluckt, kein Ersatzwert eingefuehrt (D-04).
- Keine neue Unterdrueckung, keine neue Abhaengigkeit, keine Versionsanhebung, keine
Neuformatierung (D-05, D-06).
</success_criteria>
<output>
Schreibe `.planning/quick/260921-iwr-listenschluessel-per-positionsnummer-und/260921-iwr-SUMMARY.md`.
Sie muss eine Tabelle mit allen 30 Fundstellen enthalten: Datei, Zeile, Regel, Urteil
(echter Fehler / harmlos / Lesbarkeit) und die einzeilige Begruendung. Ausserdem die
Vorher-Nachher-Zahlen aus dem Zaehlbefehl und die ausdrueckliche Aussage, welche Fundstellen
bewusst stehen bleiben und warum.
Hinweis zur Werkzeugfalle: Das Write-Werkzeug wandelt Folgen der Form Backslash-u-vier-Ziffern in
das jeweilige Zeichen um. Schreibe in Zusammenfassung und Commit-Nachricht keine solchen Folgen.
Falls doch eine gebraucht wird, erzeuge sie ueber python3 mit chr(92) fuer den Backslash und
pruefe anschliessend die Rohbytes der Datei mit `git diff --check` und `LC_ALL=C grep -n '[^[:print:][:space:]]'`.
</output>