Drei Aufgaben statt einer Sammelaktion, geschnitten nach Form und Risiko. Die Formen wurden nicht geschaetzt, sondern durch Probeumbauten am echten Baum gemessen (jeder danach zurueckgenommen): - 105 mal forTenant(...) as any: die Zusicherung war nie noetig, tsc meldet ohne sie genau einen Folgefehler. - 13 der 15 Stellen in prisma-tenant.extension.ts fallen ebenso. - Sechs Controller auf einen getypten Request: genau ein Fehler, und der ist ein echter Befund im Bestandscode. - Die drei addCronJob-Stellen sind gemessen nicht aufloesbar und bekommen das Urteil bleibt. Erwartetes Ergebnis 20 bis 40 verbleibende Befunde, nicht null: wer die letzten Stellen erzwingt, tauscht eine ehrliche Warnung gegen eine unehrliche Behauptung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
44 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260921-m34 | 01 | execute | 1 | true |
|
|
|
|
Purpose: Der any-Rueckstand ist der letzte Posten des Rueckstands, den der Nutzer vor neuen Funktionen geraeumt haben will. Er hat keinen bekannten Fehler hinter sich — es ist reine Typarbeit. Der Wert liegt darin, dass die naechste Aenderung an Mandanten-, Rollen- und Upload-Wegen vom Compiler begleitet wird statt von Vertrauen.
Output: apps/api mit deutlich weniger any, einem gemeinsamen Aufrufer-Typ, und einem Register der Stellen, die bewusst stehen bleiben.
Was vorab gemessen wurde (2026-09-21, Planungszeitpunkt)
Ausgangslage, mit npx biome lint --reporter=json gezaehlt (Feld category, nie durch Textsuche im Quelltext):
| Groesse | Wert |
|---|---|
lint/suspicious/noExplicitAny in apps/api/src |
288 (48 Dateien, +1 in apps/web) |
lint/style/noNonNullAssertion in apps/api/src |
56 |
Befunde der Schwere error |
0 |
ts-expect-error / ts-ignore |
0 |
| Lint-Unterdrueckungsmarker | 1 |
as unknown as |
33 |
pnpm type-check / pnpm lint |
4/4 und 5/5, Rueckgabewert 0 |
apps/api Vitest / apps/web Vitest |
72 Dateien / 1143 Tests, 73 / 531 |
Die Befunde fallen in wenige wiederkehrende Formen. Sie wurden nicht geschaetzt, sondern durch Probeumbauten am echten Baum gemessen (jeder Probeumbau danach zurueckgenommen, Baum wieder sauber):
| Form | Anzahl | Messergebnis |
|---|---|---|
forTenant(...) as any / forSystem(...) as any |
105 | Zusicherung an allen 105 Stellen entfernt -> tsc meldet genau einen Folgefehler. Die Zusicherung war nie noetig: prisma.$extends(...) liefert bereits einen vollstaendig getypten Klienten. |
Gefolge davon (: any[], .map((g: any) => ...), x as any[]) |
22 | haengt am any-Klienten und faellt mit ihm |
Innereien von prisma-tenant.extension.ts |
15 | (prisma as any) und die Handannotationen an $allOperations entfernt -> tsc sauber. 13 fallen, 2 (tx: any) sind noch offen. |
(req as any) |
32 | sechs Controller auf einen getypten Request umgestellt -> tsc meldet genau einen Fehler, und der ist ein echter Befund (siehe unten) |
@Req() req: any |
25 | dito |
@CurrentUser() user: any |
13 | mit AuthUser getypt -> 8 Fehler im Produktivcode, 28 in Testdateien (Fixtures ohne username/mustChangePassword) |
catch (e: any) |
18 | Umstellung auf unknown plus Eingrenzung an der Verwendungsstelle |
node-forge in cert-manager.service.ts |
11 | gemischt; @types/node-forge ist installiert |
@UploadedFile() / @UploadedFiles() und ihre Dienst-Gegenstuecke |
11 | Express.Multer.File existiert hier nicht (kein @types/multer, gemessen). Alle FileInterceptor-Aufrufe setzen keine storage-Option -> multer memoryStorage -> file.buffer ist ein Buffer. |
job as any an addCronJob |
3 | Zusicherung entfernt -> tsc meldet 3 Fehler: das lokale job hat die Form { start(): void }, addCronJob verlangt einen echten CronJob. Nicht aufloesbar ohne Verhaltensaenderung. |
| Rest (httpntlm, imap, nodemailer, calendar, web-setup, ...) | 33 | einzeln zu beurteilen |
Zwei Befunde, die die Messung schon jetzt aufgedeckt hat (D-03)
apps/api/src/tenders/tender-matching.service.ts:159— die Handannotation(match: { tender: unknown })verengt den Wert faelschlich aufunknown, sobald der Klient richtig getypt ist. Sie existierte nur, um unter demany-Klienten TS7006 zu vermeiden. Loeschen ist der richtige Umgang, keine Zusicherung.apps/api/src/dashboard/dashboard.controller.ts:74— gibtreq.user?.role(moeglicherweiseundefined) an etwas weiter, dasRoleverlangt. Der Code nimmt an, dass ein Aufrufer immer vorhanden ist. Das ist als Befund zu melden, nicht stumm zu reparieren.
Was am Ende erwartet wird, und warum
Erwartung: etwa 20 bis 40 verbleibende Befunde in apps/api (Ausgang 288). Herleitung, nicht Wunsch:
- Aufgabe 1 nimmt rund 136 (105 gemessen + 13 gemessen + rund 18 Gefolge).
- Aufgabe 2 nimmt rund 90 (13 + 25 + 32 + 1 + 11 + rund 8 Umfeld).
- Aufgabe 3 findet rund 62 vor und loest davon vielleicht 35; der Rest bleibt.
Bleiben werden voraussichtlich: die drei Cron-Stellen (gemessen nicht aufloesbar), ein Teil der node-forge-Stellen, an denen die mitgelieferten Typen die Bibliothek falsch beschreiben, sowie einzelne Stellen an Fremdbibliotheken ohne Typen. Null ist ausdruecklich nicht das Ziel. Wer die letzten Stellen erzwingt, tauscht eine ehrliche Warnung gegen eine unehrliche Behauptung — genau das verbietet D-02. Eine kleinere Zahl mit sauberen Urteilen ist das bessere Ergebnis.
Keine laufende Umgebung noetig
Fuer reine Typarbeit ist kein Stapel noetig: alle Pruefungen dieses Plans sind tsc, Biome und die beiden Testlaeufe. Es wird kein docker compose gestartet. Sollte wider Erwarten eine Laufzeitfrage auftauchen, ist das ein Grund, sie als Befund zu melden (D-03), nicht ein Grund, einen Stapel hochzufahren.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Schritt A — die Innereien des Erweiterungsmoduls. In prisma-tenant.extension.ts sind die Zusicherungen (prisma as any) in forTenant, forSystem und withTenantTransaction unnoetig: PrismaClient traegt $executeRaw und $transaction bereits. Entferne sie. Entferne ebenso die Handannotation an $allOperations — der Parameter wird von Prisma hergeleitet, die Annotation { args: any; query: (args: any) => any } ersetzt eine korrekte Herleitung durch drei any. Stelle .then((results: any[]) => results[1]) auf unknown[] um; der Ergebnistyp der Aufrufstellen kommt aus Prismas Erweiterungstypen, nicht aus diesem Rueckgabewert (gemessen: tsc bleibt danach sauber). Fuer withTenantTransaction bleibt fn: (tx: any): pruefe, ob Prisma.TransactionClient hier passt, und wenn ja, ziehe die vier Aufrufstellen in groups.service.ts und favorites.service.ts mit. Wenn tsc das nicht traegt, ist bleibt mit gemessener Begruendung das richtige Urteil (D-01).
Am Kopfkommentar (Zeile 1-196) wird nichts geaendert. Er dokumentiert Messungen zu Verbindungen und Transaktionen, nicht zu Typen.
Schritt B — die 105 Aufrufstellen. Die Form ist ueberall const tenantPrisma = forTenant(this.prisma, tenantId) as any; beziehungsweise const systemPrisma = forSystem(this.prisma) as any;. Entferne die Zusicherung. Gemessen: tsc meldet danach genau einen Folgefehler, naemlich den aus Schritt C.
Wichtig fuer die Mandantentrennung: der Erkenner in rls-access-inventory.spec.ts sucht nach const <Name> = forTenant( beziehungsweise const <Name> = forSystem(. Das Entfernen der nachgestellten Zusicherung beruehrt diesen Praefix nicht. Aendere die Zuweisungsform nicht, fasse keine zwei Aufrufe zusammen, und verschiebe keinen Aufruf in eine andere Datei — die Erlaubnisliste FORSYSTEM_ALLOWED_CALL_SITES haelt je Datei eine exakte Zahl fest, jede Abweichung macht die Spec rot. Das ist die eigentliche Schutzwirkung dieser Aufgabe und darf nicht beschaedigt werden (T-M34-03).
Schritt C — das Gefolge. Mit einem richtig getypten Klienten werden die Handannotationen, die nur seinetwegen dastanden, von Hilfe zu Schaden. Entferne sie:
const groups: any[] = await tenantPrisma.group.findMany(...)ingroups.service.ts:63undconst masters: any[] = ...indkv.service.ts:755— samt des erklaerenden Kommentars darueber, der jetzt nicht mehr stimmt.- die
.map((g: any) => ...),.map((u: any) => ...),.map((a: any) => ...),(a: any, b: any) =>undfor (const g of groupGrants as any[])-Stellen ingroups.service.ts,module-grants.service.ts,ldap-config.service.ts,tenders.controller.ts:270. - Der gemessene Befund:
tender-matching.service.ts:159traegt(match: { tender: unknown }). Diese Handannotation verengt den Wert falsch, sobald der Klient getypt ist, und erzeugt den einen Fehler aus Schritt B. Loesche die Annotation, damit der hergeleitete Typ durchkommt. Setze hier keine Zusicherung.
Fuer jede Stelle, an der das Entfernen einer Annotation einen neuen tsc-Fehler erzeugt, gilt: der Fehler ist ein Befund. Pruefe, was der Code tatsaechlich annimmt. Wenn die Annahme falsch war, melde sie im SUMMARY und lass das Verhalten unangetastet (D-03). Ersetze sie nicht durch eine Zusicherung, eine Ausrufezeichen-Behauptung oder einen Unterdrueckungskommentar (D-02) — die Zaehlwerte in <verify> fangen genau das ab.
Keine Formatierung ueber den Bestand hinaus, keine Versionsspruenge, keine neuen Abhaengigkeiten, und die Testdatei-Ausnahme in biome.json bleibt unberuehrt (D-04).
cd /home/vicolab/projects/tessera-ctl && npx biome lint --reporter=json --max-diagnostics=2000 apps/api/src | python3 -c "import json,sys; d=json.load(sys.stdin)['diagnostics']; a=sum(1 for x in d if x.get('category')=='lint/suspicious/noExplicitAny'); n=sum(1 for x in d if x.get('category')=='lint/style/noNonNullAssertion'); e=sum(1 for x in d if x.get('severity')=='error'); print('any',a,'nonnull',n,'error',e); sys.exit(0 if a<=155 and n<=56 and e==0 else 1)"
cd /home/vicolab/projects/tessera-ctl && test "$(grep -rho 'ts-expect-error' apps/api/src --include='.ts' | wc -l)" = 0 && test "$(grep -rho 'ts-ignore' apps/api/src --include='.ts' | wc -l)" = 0 && test "$(grep -rho 'biome-ignore' apps/api/src --include='.ts' | wc -l)" = 1 && test "$(grep -rho 'as unknown as' apps/api/src --include='.ts' | wc -l)" -le 33 && echo "keine stillgelegten Stellen"
cd /home/vicolab/projects/tessera-ctl && pnpm type-check 2>&1 | grep -q '4 successful, 4 total' && pnpm lint 2>&1 | grep -q '5 successful, 5 total' && echo "type-check 4/4, lint 5/5"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/api run test 2>&1 | tee /tmp/m34-api.log | tail -5 && grep -q 'Test Files 72 passed (72)' /tmp/m34-api.log && grep -q 'Tests 1143 passed (1143)' /tmp/m34-api.log && echo "api 72/1143 gruen"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/web run test 2>&1 | tee /tmp/m34-web.log | tail -5 && grep -q 'Test Files 73 passed (73)' /tmp/m34-web.log && grep -q 'Tests 531 passed (531)' /tmp/m34-web.log && echo "web 73/531 gruen"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/api exec vitest run src/prisma/rls-access-inventory.spec.ts 2>&1 | tail -4
Die Zaehlung liegt bei hoechstens 155 (Ausgang 288), noNonNullAssertion bei hoechstens 56, error-Befunde bei 0, keine neuen Unterdrueckungsmarker. pnpm type-check 4/4 und pnpm lint 5/5. Beide Testsuiten unveraendert gruen (72/1143 und 73/531), rls-access-inventory.spec.ts ausdruecklich gruen. Der Kopfkommentar von prisma-tenant.extension.ts ist unveraendert. Jede Stelle, an der ein neuer tsc-Fehler auftrat, steht als Befund im SUMMARY.
Schritt A — die Typen anlegen, in apps/api/src/auth/types/auth-user.ts. Vorbild ist ausdruecklich SessionUser/UploadedPng in bug-reports.service.ts: schmale Schnittstellen, die nur beschreiben, was gelesen wird. Ziehe SessionUser und UploadedPng danach auf die neuen Typen zurueck, damit es keine zwei konkurrierenden Beschreibungen desselben Objekts gibt.
AuthUser— leite jedes einzelne Feld ausJwtStrategy.validate()(Zeile 27-38) ab und belege es an den beiden Signierstellenauth.service.ts:171undauth.service.ts:376. Felder:id,username,role,tenantId,mustChangePassword.- Zu
role: die AufzaehlungRoleaus@prisma/clientist der ehrliche Typ, weil der Signierer genau den Spaltenwert schreibt. Gemessen: sechs Controller vertragenrole: Roleohne einen einzigen Fehler. Wenntscirgendwo doch anschlaegt, weil eine Stelle gegen eine Zeichenkette ausserhalb der Aufzaehlung vergleicht, ist das ein Befund nach D-03 und wird gemeldet, nicht mit einer Zusicherung beruhigt (T-M34-02). - Zu
tenantId— das ist die sicherheitsrelevante Entscheidung dieses Plans. Die Messung:tenantId: string | undefinederzeugt acht Fehler im Produktivcode,tenantId: stringkeinen. Das ist kein Argument fuer die bequemere Variante. Pruefe stattdessen die Belege: die SpalteUser.tenantIdist inschema.prismanicht optional; der Bestand beschreibt dasselbe Objekt inSessionUserbereits alstenantId: string; undTenantGuardhaelt fuer SUPER_ADMIN einen Zweig ohne Mandanten vor. Entscheide auf dieser Grundlage und schreibe die Begruendung als Kommentar an den Typ. Wenn die Wahl aufstringfaellt, halte im selben Kommentar ausdruecklich fest, dass der SUPER_ADMIN-Zweig inTenantGuardein Schutzzweig bleibt und nicht wegtypisiert oder entfernt werden darf, weil er sonst bei der naechsten Aenderung als toter Code geloescht wird (T-M34-01). Entferne anTenantGuardin dieser Aufgabe nichts. AuthenticatedRequest— erweitertRequestausexpressumuserundtenantId.TenantGuardsetzttenantIdauf eine Zeichenkette oder aufnullund laeuft auf oeffentlichen Wegen gar nicht; die Optionalitaeten muessen das abbilden.UploadedFileLike—Express.Multer.Filegibt es in diesem Baum nicht (gemessen:@types/multerist nicht installiert), und D-04 verbietet, es nachzuinstallieren. Eine eigene schmale Schnittstelle ist hier trotzdem keine Behauptung, sondern belegbar: kein einzigerFileInterceptor/FilesInterceptor-Aufruf setzt einestorage-Option, damit gilt multer memoryStorage, damit istfile.bufferein Buffer. Pruefe das nach und beschreibe nur die gelesenen Felder. Nimm kein Feld auf, das kein Aufrufer liest.JwtPayload— fuerjwt.strategy.ts:27(payload: any). Die Nutzlast ist an den beiden Signierstellen vollstaendig belegt; beschreibe sie danach.mustChangePasswordwird dort bereits bewusst mit=== truegegen alte Token abgesichert (260921-fi3, D-01) — dieses Verhalten bleibt woertlich erhalten.
Schritt B — anwenden. Ersetze @CurrentUser() user: any (13 Stellen), @Req() req: any (25), (req as any) (32), @Res() res: any (1), @UploadedFile()/@UploadedFiles() samt der Dienst-Gegenstuecke file?: any und files?: any[] in cert-manager.service.ts (11). Zieh das Umfeld mit: resolveTargetTenantId(currentUser: any), resolveTargetUser(currentUser: any), login(user: any), validateUser(): Promise<any>, local.strategy validate(): Promise<any>, normalizePath(request: any), intercept(): Observable<any> (dort ist unknown der ehrliche Typ), dkv.controller _requireTenant(req: any).
Ein Nebengewinn, der mitzunehmen ist: in cert-manager.service.ts stehen heute rund 25 Zusicherungen der Form file.buffer as Buffer und file.originalname as string, die es nur gibt, weil file ein any ist. Mit der schmalen Schnittstelle fallen sie weg — der Zaehlwert as unknown as darf dabei nicht steigen.
Der gemessene Befund: dashboard.controller.ts:74 gibt einen moeglicherweise fehlenden Aufrufer-Wert an etwas weiter, das ihn zwingend verlangt. Das ist eine falsche Annahme im Bestandscode. Melde sie im SUMMARY mit Datei, Zeile und dem, was der Code annimmt. Verhalten unveraendert lassen (D-03): keine neue Pruefung einbauen, die vorher nicht da war, und keinen Wert erfinden.
Schritt C — die Testfixtures. Gemessen: 28 tsc-Fehler in auth.controller.spec.ts und user.controller.spec.ts, weil die Fixtures Teilobjekte wie { id, tenantId, role } uebergeben. Ergaenze die fehlenden Felder in den Fixtures. Das ist reine Fixture-Pflege: die Zahl der Testdateien und Tests darf sich nicht aendern (D-06), und keine Zusicherung in einer Testdatei darf den Fehler stattdessen zudecken. Die Testdatei-Ausnahme in biome.json bleibt unberuehrt (D-04).
cd /home/vicolab/projects/tessera-ctl && npx biome lint --reporter=json --max-diagnostics=2000 apps/api/src | python3 -c "import json,sys; d=json.load(sys.stdin)['diagnostics']; a=sum(1 for x in d if x.get('category')=='lint/suspicious/noExplicitAny'); n=sum(1 for x in d if x.get('category')=='lint/style/noNonNullAssertion'); e=sum(1 for x in d if x.get('severity')=='error'); print('any',a,'nonnull',n,'error',e); sys.exit(0 if a<=75 and n<=56 and e==0 else 1)"
cd /home/vicolab/projects/tessera-ctl && test "$(grep -rho 'ts-expect-error' apps/api/src --include='.ts' | wc -l)" = 0 && test "$(grep -rho 'ts-ignore' apps/api/src --include='.ts' | wc -l)" = 0 && test "$(grep -rho 'biome-ignore' apps/api/src --include='.ts' | wc -l)" = 1 && test "$(grep -rho 'as unknown as' apps/api/src --include='.ts' | wc -l)" -le 33 && echo "keine stillgelegten Stellen"
cd /home/vicolab/projects/tessera-ctl && git diff --stat -- apps/api/src/tenant/tenant.guard.ts > /tmp/m34-guard.txt || exit 1; cat /tmp/m34-guard.txt; test ! -s /tmp/m34-guard.txt && echo "TenantGuard unberuehrt"
cd /home/vicolab/projects/tessera-ctl && pnpm type-check 2>&1 | grep -q '4 successful, 4 total' && pnpm lint 2>&1 | grep -q '5 successful, 5 total' && echo "type-check 4/4, lint 5/5"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/api run test 2>&1 | tee /tmp/m34-api.log | tail -5 && grep -q 'Test Files 72 passed (72)' /tmp/m34-api.log && grep -q 'Tests 1143 passed (1143)' /tmp/m34-api.log && echo "api 72/1143 gruen"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/web run test 2>&1 | tee /tmp/m34-web.log | tail -5 && grep -q 'Test Files 73 passed (73)' /tmp/m34-web.log && grep -q 'Tests 531 passed (531)' /tmp/m34-web.log && echo "web 73/531 gruen"
Die Zaehlung liegt bei hoechstens 75. apps/api/src/auth/types/auth-user.ts existiert und traegt die Begruendung jedes Feldes als Kommentar, mit Verweis auf die Signierstellen und auf schema.prisma. tenant.guard.ts ist unveraendert. SessionUser/UploadedPng in bug-reports.service.ts sind auf die neuen Typen zurueckgezogen, es gibt keine zweite Beschreibung desselben Objekts. noNonNullAssertion hoechstens 56, as unknown as hoechstens 33, keine neuen Unterdrueckungsmarker, 0 error-Befunde. Beide Suiten gruen mit unveraenderten Zahlen. Der Befund aus dashboard.controller.ts:74 steht im SUMMARY.
Schritt A — die 18 Fehlerfaenger. catch (e: any) wird zu catch (e: unknown) plus Eingrenzung an der Verwendungsstelle. Fast alle lesen err.code (Prisma-Fehlercodes wie P2025, P2002) oder err.message. Grenze ein, statt zu behaupten: eine Pruefung auf Objektform und Feld, oder instanceof Error fuer message. Der Bestand hat dafuer bereits ein Vorbild in tender-matching.service.ts:123 ((err as Error).message) — das ist die schwaechere Variante; wo eine echte Eingrenzung ohne Aufwand moeglich ist, nimm die echte. Entscheidend: welcher Zweig genommen wird, darf sich nicht aendern. Ein Fehlerfaenger, der heute bei einem fremden Fehlerobjekt in den einen Zweig laeuft, muss das danach auch tun (D-03). Wo der gefangene Wert gar nicht gelesen wird, ist die Bindung ersatzlos zu streichen — genau das hat 260921-bi2 an einer Stelle bereits so gemacht.
Schritt B — die Stellen mit Fremdbibliotheken, einzeln beurteilt:
addCronJob(..., job as any)indkv-scheduler.service.ts:136,tender-digest.scheduler.ts:86,tender-scheduler.service.ts:135. Gemessen: bleibt. Ohne Zusicherung meldettscan allen drei Stellen, dass das lokalejobdie Form{ start(): void }hat, waehrendaddCronJobeinen vollstaendigenCronJobverlangt. Ursache ist derrequire()-Umweg aus 07-04 (pnpm-Isolation,cronist eine mittelbare Abhaengigkeit). Das aufzuloesen hiesse, die Beschaffung der Klasse zu aendern — das waere eine Verhaltensaenderung und ist hier verboten (D-03), und eine neue Abhaengigkeit ist ebenfalls verboten (D-04). Trage das Urteil samt dieser Begruendung als kurzen Kommentar an jeder der drei Stellen ein.- node-forge in
cert-manager.service.ts(11 Stellen:p7: anyviermal,cert.publicKey as any,cert.siginfo as any,sanExt as any,(n: any),null as anyzweimal).@types/node-forgeist installiert. Pruefe Stelle fuer Stelle, ob der mitgelieferte Typ passt. Wo er passt: typisieren. Wo die mitgelieferten Typen die Bibliothek nachweislich falsch beschreiben — die beidennull as anytragen bereits den Vermerk, dass node-forge 1.4.0 einen fehlenden Schluessel akzeptiert, die Typen das aber ausschliessen — istbleibtdas ehrliche Urteil. Erzwinge dort nichts: eine Umdeutung ueber zwei Stufen waere schlimmer als dasany, weil sie dieselbe Luecke verdeckt und zusaetzlich so aussieht, als sei sie geprueft (D-02). - httpntlm (
exchange-inbox.provider.ts:3und:241,exchange.provider.ts:6) undauthProvider-Rueckrufe (exchange.provider.ts:157,:313). Die Handschnittstelle fuerpostexistiert schon; sie kann enger werden, weil der Code genau weiss, welche Optionen er uebergibt und welche Antwortfelder er liest. Beschreibe nur diese. Der Fehlerparameter eines Rueckrufs ist ehrlichError | null. - imap (
stream as any,node as anyzweimal,} as any) undnodemailer.createTransport(resolved.options as any): einzeln pruefen. Wo eine Eingrenzung reicht, eingrenzen; sonst Urteilbleibtmit Begruendung. (response as any).cookie(...)inauth.service.ts:384:Responseausexpressist in dieser Datei bereits importiert und wird an der Schwesterstelleauth.service.ts:181ohne Zusicherung benutzt. Pruefe, ob die Zusicherung schlicht ueberfluessig ist.data as anyincalendar.service.ts:206und:255: Prisma-JSON-Eingaben.Prisma.InputJsonValueist der vorgesehene Typ; pruefe, ob er traegt.let created: any/const updateData: anyinuser.service.ts:110und:161: Prismas erzeugte Typen decken beides ab.apps/web/src/test/setup.ts:8(expect.extend(matchers as any)): der einzige Befund ausserhalb von apps/api. Die Datei heisstsetup.tsund faellt deshalb nicht unter die Testdatei-Ausnahme inbiome.json— diese Ausnahme wird nicht erweitert (D-04). Beurteile die Stelle: passen die jest-dom-Matcher-Typen auf Vitestsexpect.extend, oder ist das ein bekannter Versatz zwischen den beiden Typwelten? Urteil eintragen, so oder so.
Schritt C — das Urteilsregister. Fuehre im SUMMARY eine Tabelle mit jeder Stelle, die stehen bleibt: Datei, Zeile, Form, Begruendung. Das ist der Zweck der ganzen Aufgabe — ein spaeterer Durchlauf soll diese Stellen nicht noch einmal aufreissen, sondern nachlesen koennen, warum sie so sind. Ergaenze zusaetzlich je eine kurze Begruendungszeile direkt an der Codestelle, damit die Auskunft auch dort steht, wo jemand sie zuerst sucht. Diese Zeilen sind normale erklaerende Kommentare; sie duerfen keinen Marker enthalten, der die Pruefung stilllegt — die Zaehlwerte in <verify> fangen das ab, und der Befund soll ja sichtbar bleiben (D-01, D-05).
Halte ausserdem im SUMMARY fest: Ausgangszahl 288, Endzahl, und die Aufteilung der 288 auf die drei Urteile.
cd /home/vicolab/projects/tessera-ctl && npx biome lint --reporter=json --max-diagnostics=2000 apps/api/src | python3 -c "import json,sys; d=json.load(sys.stdin)['diagnostics']; a=sum(1 for x in d if x.get('category')=='lint/suspicious/noExplicitAny'); n=sum(1 for x in d if x.get('category')=='lint/style/noNonNullAssertion'); e=sum(1 for x in d if x.get('severity')=='error'); print('any',a,'nonnull',n,'error',e); sys.exit(0 if a<=45 and n<=56 and e==0 else 1)"
cd /home/vicolab/projects/tessera-ctl && npx biome lint --reporter=json --max-diagnostics=2000 apps/api/src apps/web/src | python3 -c "import json,sys; d=json.load(sys.stdin)['diagnostics']; [print(x['location']['path']+':'+str(x['location']['start']['line'])) for x in d if x.get('category')=='lint/suspicious/noExplicitAny']" | sort > /tmp/m34-rest.txt; wc -l < /tmp/m34-rest.txt; echo "--- jede dieser Zeilen muss im Urteilsregister des SUMMARY stehen ---"; cat /tmp/m34-rest.txt
cd /home/vicolab/projects/tessera-ctl && test "$(grep -rho 'ts-expect-error' apps/api/src apps/web/src --include='.ts' --include='.tsx' | wc -l)" = 0 && test "$(grep -rho 'ts-ignore' apps/api/src apps/web/src --include='.ts' --include='.tsx' | wc -l)" = 0 && test "$(grep -rho 'biome-ignore' apps/api/src --include='.ts' | wc -l)" = 1 && test "$(grep -rho 'as unknown as' apps/api/src --include='.ts' | wc -l)" -le 33 && echo "keine stillgelegten Stellen"
cd /home/vicolab/projects/tessera-ctl && git diff --stat HEAD -- biome.json > /tmp/m34-biome.txt || exit 1; cat /tmp/m34-biome.txt; test ! -s /tmp/m34-biome.txt && echo "biome.json unberuehrt"
cd /home/vicolab/projects/tessera-ctl && git diff --stat HEAD -- package.json apps/api/package.json apps/web/package.json pnpm-lock.yaml > /tmp/m34-deps.txt || exit 1; cat /tmp/m34-deps.txt; test ! -s /tmp/m34-deps.txt && echo "keine neuen Abhaengigkeiten, keine Versionsspruenge"
cd /home/vicolab/projects/tessera-ctl && pnpm type-check 2>&1 | grep -q '4 successful, 4 total' && pnpm lint 2>&1 | grep -q '5 successful, 5 total' && echo "type-check 4/4, lint 5/5"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/api run test 2>&1 | tee /tmp/m34-api.log | tail -5 && grep -q 'Test Files 72 passed (72)' /tmp/m34-api.log && grep -q 'Tests 1143 passed (1143)' /tmp/m34-api.log && echo "api 72/1143 gruen"
cd /home/vicolab/projects/tessera-ctl && pnpm --dir apps/web run test 2>&1 | tee /tmp/m34-web.log | tail -5 && grep -q 'Test Files 73 passed (73)' /tmp/m34-web.log && grep -q 'Tests 531 passed (531)' /tmp/m34-web.log && echo "web 73/531 gruen"
Die Zaehlung in apps/api liegt bei hoechstens 45. Jede verbleibende Stelle aus der Ausgabe der zweiten Pruefung steht mit Datei, Zeile und Begruendung im Urteilsregister des SUMMARY und traegt eine Begruendungszeile im Code. biome.json, die package.json-Dateien und pnpm-lock.yaml sind unveraendert. noNonNullAssertion hoechstens 56, as unknown as hoechstens 33, keine Unterdrueckungsmarker ueber den einen Bestandsmarker hinaus, 0 error-Befunde. pnpm type-check 4/4, pnpm lint 5/5, beide Suiten gruen mit unveraenderten Zahlen. Das SUMMARY nennt Ausgangszahl 288, Endzahl und die Aufteilung auf die drei Urteile.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser -> API (JWT-Cookie) | Das Sitzungstoken traegt Identitaet, Rolle und Mandant. JwtStrategy.validate() ist die Stelle, an der daraus ein Objekt wird, das jede spaetere Berechtigungsentscheidung traegt. |
| API -> PostgreSQL (RLS) | forTenant()/forSystem() setzen die Sitzungsvariablen, an denen die Zeilenregeln haengen. Wer hier die Bindung verliert, sieht fremde Mandanten oder gar nichts. |
| Browser -> API (multipart) | Hochgeladene Dateien werden als Zertifikate, CSV und Bilder weiterverarbeitet. |
| Aufrufer-Objekt -> TenantGuard/RolesGuard | Die Typen der Felder entscheiden mit, ob eine bestehende Pruefung als lebendig oder als tot gelesen wird. |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-M34-01 | Elevation of Privilege | apps/api/src/auth/types/auth-user.ts -> tenant.guard.ts |
high | mitigate | tenantId wird nicht nach Bequemlichkeit getypt. Aufgabe 2 verlangt die Herleitung aus drei Belegen (Spalte User.tenantId in schema.prisma, Bestandstyp SessionUser, SUPER_ADMIN-Zweig in TenantGuard) und einen Kommentar am Typ, der festhaelt, dass der Schutzzweig in TenantGuard nicht wegtypisiert werden darf. <verify> erzwingt zusaetzlich, dass tenant.guard.ts in dieser Aufgabe unveraendert bleibt (git diff --stat leer). |
| T-M34-02 | Elevation of Privilege | role-Feld, alle Rollenvergleiche |
medium | mitigate | role als Role-Aufzaehlung macht jeden Vergleich gegen eine Zeichenkette ausserhalb der Aufzaehlung zum Compilerfehler. Jeder solche Fehler ist nach D-03 ein zu meldender Befund und darf nicht per Zusicherung beruhigt werden; die Zaehlwerte fuer as unknown as und Ausrufezeichen-Behauptungen in <verify> fangen den Umweg ab. |
| T-M34-03 | Tampering | forTenant()/forSystem(), 105 Aufrufstellen |
high | mitigate | Die Zuweisungsform const X = forTenant( bleibt woertlich erhalten — nur die nachgestellte Zusicherung faellt. Der Erkenner rls-access-inventory.spec.ts und die exakten Zahlen in FORSYSTEM_ALLOWED_CALL_SITES sind die Kontrolle; Aufgabe 1 laesst diese Spec zusaetzlich einzeln laufen. Kein Aufruf wird zusammengefasst, verschoben oder hinzugefuegt. |
| T-M34-04 | Information Disclosure | UploadedFileLike, cert-manager/user/bug-reports/dkv-Uploads |
medium | mitigate | Die Schnittstelle beschreibt ausschliesslich gelesene Felder und ist an der Konfiguration belegt (kein storage-Argument -> memoryStorage -> buffer ist ein Buffer). Sie ersetzt keine Pruefung: Groessengrenzen bleiben in den Interceptor-Optionen, die PNG-Signaturpruefung in bug-reports.service.ts bleibt, und D-03 verbietet jede Verhaltensaenderung an diesen Wegen. |
| T-M34-05 | Repudiation | Urteilsregister | low | mitigate | Ohne Register waere nach diesem Lauf nicht nachvollziehbar, welche Stelle geprueft und bewusst gelassen wurde und welche nur uebersehen wurde. Aufgabe 3 erzeugt das Register aus der maschinellen Restliste, nicht aus dem Gedaechtnis; die zweite Pruefung in <verify> druckt genau diese Liste aus. |
| T-M34-06 | Denial of Service | Fehlerfaenger, Umstellung auf unknown |
medium | mitigate | Eine falsche Eingrenzung koennte einen Fehler kuenftig in einen anderen Zweig laufen lassen und damit einen Hintergrundlauf abbrechen, der heute weiterlaeuft. Aufgabe 3 schreibt ausdruecklich fest, dass die Zweigwahl unveraendert bleiben muss; die 1143 Tests in apps/api decken die Fehlerwege der betroffenen Dienste ab und muessen gruen bleiben. |
| T-M34-SC | Tampering | Lieferkette | low | accept | Dieser Plan installiert nichts. D-04 verbietet neue Abhaengigkeiten und Versionsspruenge; <verify> in Aufgabe 3 erzwingt, dass package.json und pnpm-lock.yaml unveraendert bleiben. Damit entsteht keine neue Lieferkettenflaeche, und das Paket-Echtheitstor ist nicht anwendbar. |
| </threat_model> |
pnpm type-checkmeldet4 successful, 4 total.pnpm lintmeldet5 successful, 5 total, Rueckgabewert 0.- Biome-JSON ueber
apps/api/src:severity == errorist 0;lint/style/noNonNullAssertionhoechstens 56;lint/suspicious/noExplicitAnyunter der Schranke der jeweiligen Aufgabe (155 / 75 / 45). grep-Zaehlungen ueberapps/api/src:ts-expect-error0,ts-ignore0, Lint-Unterdrueckungsmarker 1,as unknown ashoechstens 33.apps/apiVitest: 72 Dateien, 1143 Tests, alle gruen.apps/webVitest: 73 Dateien, 531 Tests, alle gruen.biome.json,package.json(alle drei),pnpm-lock.yamlunveraendert.
Gezaehlt wird ausschliesslich ueber das Feld category im JSON-Bericht von Biome, nie durch Textsuche nach any im Quelltext — eine Textsuche zaehlt Kommentare mit und waere damit selbst verfaelschend.
Vor dem Festschreiben: die erzeugten Dateien und Commit-Texte auf rohe Steuerzeichen pruefen. Das Schreibwerkzeug wandelt Folgen der Form Backslash-u-vier-Ziffern in das tatsaechliche Zeichen um; das ist heute fuenfmal passiert und hat Commits scheitern lassen. Solche Folgen, falls ueberhaupt noetig, ueber python3 mit chr(92) erzeugen und die Rohbytes danach kontrollieren.
<success_criteria>
- Die Zahl der
any-Befunde inapps/apiist von 288 auf hoechstens 45 gefallen, erwartet auf 20 bis 40. - Jede verbleibende Stelle hat ein Urteil mit Begruendung, im SUMMARY und im Code.
- Kein Befund wurde durch Zusicherung, Ausrufezeichen-Behauptung, Unterdrueckungskommentar oder eine unbelegte Handschnittstelle stillgelegt (D-02) — nachgewiesen ueber die Zaehlwerte, nicht behauptet.
- Kein Verhalten der API hat sich geaendert (D-03); jede aufgedeckte falsche Annahme steht als Befund im SUMMARY, mindestens die beiden schon gemessenen (
tender-matching.service.ts:159,dashboard.controller.ts:74). - Der gemeinsame Aufrufer-Typ existiert, ist an den Signierstellen belegt, und
tenant.guard.tsist unveraendert. pnpm type-check4/4 undpnpm lint5/5 nach jeder Aufgabe, nicht nur am Ende (D-05).- Beide Testsuiten gruen mit unveraenderten Zahlen (D-06).
- Drei Commits, einer je Aufgabe, jeder fuer sich gruen. </success_criteria>
Das SUMMARY traegt zwingend:
- Ausgangszahl 288, Endzahl, Aufteilung der 288 auf die drei Urteile aus D-01.
- Das Urteilsregister aller verbleibenden Stellen (Datei, Zeile, Form, Begruendung).
- Die Liste der aufgedeckten falschen Annahmen im Bestandscode (D-03) — jede mit Datei, Zeile und dem, was der Code annimmt. Diese Liste ist ein Ergebnis, kein Anhang: sie ist der eigentliche Fund einer Aufgabe, die ohne bekannten Fehler begonnen hat.
- Die Begruendung der
tenantId-Entscheidung im gemeinsamen Aufrufer-Typ.