Ein neuer Widget-Typ war an sieben Stellen einzutragen; vergass man eine,
fehlte die Kachel im Katalog oder die API lehnte sie mit 400 ab.
- WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS stehen jetzt einmal in
packages/shared; Registry, Katalog und die @IsIn-Whitelist der API
leiten davon ab
- neun wireXWidget()-Funktionen durch ein generisches registerWidget()
ersetzt (idempotent, unbekannter Typ wirft in der Entwicklung)
- der Katalog fuehrt keine zweite Typliste mehr, sondern leitet sie aus
der Registry ab und filtert nach Modulzugriff (fail-closed, wenn die
Modulliste unbekannt ist); der Abruf von /modules/active liegt auf der
Dashboard-Seite, nicht im Dialog
- widget-module-map.ts liest die geteilte Tabelle statt einer Kopie, die
oeffentliche Funktion bleibt unveraendert
Der Katalogfilter ist Komfort (T-M1H-01) — verbindlich bleibt der
serverseitige Filter in DashboardService.getWidgets.
Abweichung vom Plan: apps/web hing entgegen der Planannahme noch nicht
von @tessera/shared ab; die Abhaengigkeit wurde ergaenzt (Lockfile). Die
Dockerfiles kopieren packages/shared bereits, der Produktionsbau von
Next.js und der nest build laufen unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Beim Rundgang aufgefallen: nach dem Umzug zeigt eine Zeile auf eine Datei,
die es auf diesem Server nicht gibt — lokal, weil der Umzug am Host lief und
der Container ein eigenes Volume hat. Derselbe Zustand entsteht im Betrieb,
wenn jemand einen `pg_dump` von vor dem Umzug zurueckspielt: die Bytes
stecken noch in der Spalte `data`, das getrennt gesicherte Volume
`user-files` ist aber leer.
`getBytes` schreibt die Datei in diesem Fall aus `data` neu und liefert sie
aus, statt 404 zu melden. Fehlt beides, bleibt es bei 404. Nach dem
Entfernen der Spalte (eigenes Todo) faellt der Zweig ersatzlos weg.
api-Tests 1186 -> 1188, type-check und lint unveraendert gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Bytes liegen unter user-files/dashboard-images/<userId>/<id>.<ext>, die
Zeile haelt nur noch storagePath (Muster User.avatarPath)
- Dateiname immer servergeneriert: UUID der Zeile + Endung aus dem
ERKANNTEN Mime-Typ, originalName kommt in keinem Pfad vor (T-HK4-01)
- Migration 20260922120000: storagePath dazu, data wird NULLbar, kein DROP
(zweistufig, T-HK4-03); system_read_policy fuer den Umzug
- onApplicationBootstrap zieht Altbestand automatisch um: systemgebunden
lesen, je Zeile mandantengebunden schreiben (Muster DKV-Planer)
- Upload nimmt die Zeile bei fehlgeschlagenem Schreiben zurueck, Loeschen
entfernt die Datei mit, fehlende Datei -> 404 (T-HK4-04)
- 11 neue Dienst-Tests gegen ein echtes Temp-Verzeichnis (kein fs-Mock)
- Zugriffsklassifikation: Stand system-gebunden, Zahlen nachgemessen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- xframe-config.ts: Resolver (https-Pruefung via isHttpsUrl des Bilderrahmens,
Titel bis 100 Zeichen, Neuladen 0/60/300/600/1800/3600 s geklemmt),
XFRAME_SANDBOX ohne allow-top-navigation und allow-modals; 12 Tests zuerst rot
- xframe-widget.tsx: genau ein <iframe> (sandbox, allow="", no-referrer, lazy),
Kopfleiste mit Titel oder Ecksymbol "In neuem Tab oeffnen", Neuladen ueber
key-Wechsel mit Timer-Raeumung, transparente Flaeche im Bearbeitungsmodus
damit die Kachel Ziehgriff bleibt; 12 Tests zuerst rot
- xframe-config-form.tsx: Adresse/Titel mit Uebernahme bei Blur/Enter, http wird
mit Meldung abgewiesen und nicht gespeichert, Intervall-Auswahl, dauerhafter
Hinweis auf verweigertes Einbetten; 8 Tests
- Panel-Zweig samt "— Titel" in der Kopfzeile, Registry (12x12, Fenster-Symbol),
Katalog, Seite, DTO @IsIn, de/en widgets.xframe (15 Schluessel), Umlaut-Allowlist
"neuem"; der Server ruft die Adresse nie ab
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Prisma-Modell DashboardImage (bytea) mit Migration 20260921120000: Tabelle,
Indizes, RLS ENABLE/FORCE und tenant_isolation_policy mit Benutzerdimension
- dashboard-image-rules.ts: detectImageMime ueber Magic Bytes (PNG/JPEG/GIF/
WebP), Grenzen 5 MiB je Datei und 30 je Benutzer
- DashboardImagesService: list/upload/getBytes/remove, je Methode
forTenant(prisma, tenantId, userId); Besitz = Mandant UND Benutzer, sonst 404
- DashboardImagesController unter dashboard/images: GET, POST (FileInterceptor
image, 5 MiB, eine Datei), GET :id mit Content-Type aus dem erkannten Typ,
Cache-Control private, nosniff, Content-Disposition inline ohne Dateinamen,
CSP sandbox; DELETE :id
- CreateWidgetDto kennt 'picture-frame'
- Klassifikationsdokument: neues Paar dashboard-images.service.ts/
dashboardImage; Bereichs- und Summenzeilen nachgemessen (dashboard 12->18,
settings 3->4 und bug-reports waren in der Summe nie mitgezaehlt)
- Befund: Prisma-Bytes verlangt Uint8Array<ArrayBuffer>, multers Buffer wird
ohne Zusicherung abgelehnt - Kopie per new Uint8Array(buffer) statt Cast
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B-06: requireTLS durch doSTARTTLS ersetzt. requireTLS kennt imapflow 1.4.3
nicht und verwirft es still; ein als STARTTLS eingerichtetes Postfach konnte
deshalb unbemerkt im Klartext verbinden. Gewollte Folge: so ein Postfach
scheitert jetzt, wenn der Server kein STARTTLS anbietet. Bei ssl-tls ergibt
der Ausdruck false, was die Unvertraeglichkeit secure=true + doSTARTTLS=true
gar nicht erst entstehen laesst.
B-05: Dateiname aus Content-Disposition kommt jetzt aus dem Feld, in dem
imapflow ihn ablegt (dispositionParameters), statt aus .parameters einer
Zeichenkette. Der alte Ausdruck war zur Laufzeit immer undefined, wodurch
Anhaenge als application/octet-stream (typisch Outlook) nicht erkannt wurden.
Die Zusicherung am Ende von buildClient() entfaellt ersatzlos; sie bestand
nur wegen der unbekannten Option. noExplicitAny in apps/api/src faellt damit
von 15 auf 13.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- B-06: prueft die an ImapFlow uebergebenen Optionen fuer starttls und ssl-tls
- B-05: prueft, dass ein octet-stream-Anhang am Dateinamen aus
Content-Disposition erkannt wird
- Einhaengen des Testdoppels in einen Helfer gezogen; die Umdeutung des
Konstruktors steht damit nur noch an einer Stelle statt an zwoelf
Gegen den heutigen Stand rot: 3 von 12 Faellen scheitern
(doSTARTTLS undefined, Feld requireTLS vorhanden, Anhang nicht eingesammelt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
httpntlm (exchange.provider, exchange-inbox.provider): NtlmOptions und
NtlmResponse beschreiben genau das, was uebergeben und gelesen wird. Die
ueberfluessige Zusicherung (httpntlm as any) faellt weg.
Graph-Rueckrufe (exchange.provider :157/:313): AuthProviderCallback aus dem
SDK selbst statt Handannotation - als import type, also ohne den dynamischen
Import zur Laufzeit zurueckzunehmen.
imapflow: streamToBuffer() nimmt Readable statt NodeJS.ReadableStream (alle
drei Aufrufer reichen client.download().content herein, imapflow deklariert
das als Readable) - damit traegt der Typ destroy() und die Zusicherung
faellt. node.parameters?.name war ebenfalls schon getypt.
nodemailer: ResolvedTransport.options wird SMTPTransport.Options; beide
Zweige bauen reine SMTP-Optionen, createTransport() nimmt sie ohne
Zusicherung.
node-forge: die vier let p7: any werden Captured<PkcsEnvelopedData |
PkcsSignedData> - der MITGELIEFERTE Typ. Die Lesestellen grenzen mit
'certificates' in p7 ein statt zuzusichern; verhaltensgleich, weil der
enveloped-Form das Feld fehlt und beide Schreibweisen dann die leere Liste
liefern. cert.siginfo war bereits getypt.
apps/web/src/test/setup.ts: expect.extend(matchers) traegt ohne Zusicherung
- geprueft im echten Typlauf (setup.ts liegt im include von
apps/web/tsconfig.json, mit einem absichtlichen Fehler nachgewiesen).
BEFUND 4 (D-03, gemeldet, NICHT repariert) imap.provider.ts:78 - der
Ausdruck (node as any).disposition?.parameters?.filename liest .parameters
von einer ZEICHENKETTE: imapflow deklariert disposition als string
(imap-flow.d.ts:448), die Parameter liegen in dispositionParameters (:450).
dispositionFilename ist damit zur Laufzeit immer ''. Folge: Outlook-Anhaenge,
die als application/octet-stream kommen, werden ueber den Dateinamen aus
Content-Disposition NICHT erkannt - nur ueber den aus Content-Type. Umbiegen
waere eine Verhaltensaenderung; die Zusicherung bleibt sichtbar stehen.
BEFUND 5 (D-03, gemeldet, NICHT repariert) imap.provider.ts:402 -
requireTLS kommt in imapflow 1.4.3 NIRGENDS vor, weder in ImapFlowOptions
noch im Laufzeitcode (beides durchsucht). Die Option wird still verworfen;
STARTTLS wird durch sie nicht erzwungen. Genau das } as any hat es
verdeckt. Bleibt stehen, damit der Befund in der Zaehlung sichtbar ist.
BEFUND 6 (D-03, gemeldet, Verhalten unveraendert) httpntlm liefert den
Rumpf als Zeichenkette, nicht als Buffer: httpreq setzt ihn nur bei
gesetzter Option binary auf Buffer (httpreq@1.1.1/lib/httpreq.js:391),
keiner der beiden Aufrufer setzt sie. Der Bestand rief unbesehen
.toString('utf-8') auf - das ging nur gut, weil String.toString() sein
Argument ignoriert. Die Testdoppel reichen dagegen wirklich Buffer herein.
NtlmResponse.body nennt jetzt beide Formen, die Fallunterscheidung liefert
fuer jede exakt dasselbe Ergebnis wie zuvor.
Urteil BLEIBT mit Begruendung im Code an allen 15 verbleibenden Stellen:
3x addCronJob (require-Umweg aus 07-04), 5x node-forge (EC-Zweig und
extensions: any[] sind in @types/node-forge nicht beschrieben, 2x null as
any wo die Typen die Bibliothek nachweislich falsch beschreiben), 2x
imap-Befunde oben, 2x tx: any plus 2x Gefolge (Aufgabe 1), 1x
disposition-Befund.
noExplicitAny in apps/api/src: 31 -> 15 (Ausgang 288, Schranke 45), apps/web
1 -> 0. type-check 4/4, lint 5/5 (0 error), apps/api 72/1143, apps/web
73/531, rls-access-inventory 30/30. noNonNullAssertion 56, as unknown as 33,
ts-expect-error/ts-ignore 0/0, Unterdrueckungsmarker 1. biome.json, alle
package.json und pnpm-lock.yaml unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- auth.service.ts:393: (response as any).cookie war schlicht ueberfluessig.
response ist in derselben Signatur bereits Response aus express, die
Schwesterstelle :190 kommt ohne Zusicherung aus. Ersatzlos entfernt.
- calendar.service.ts: das lokal gebaute data-Objekt traegt jetzt
Prisma.CalendarSourceUncheckedCreateInput bzw. ...UncheckedUpdateInput
statt Record<string, unknown> plus Zusicherung. Damit fallen beide
`data as any` weg, ohne dass ein Feld behauptet wird.
- user.service.ts: `let created: any` -> User (die Zuweisung steht im try,
der catch endet ausnahmslos mit throw). `const updateData: any` wird aus
der Signatur hergeleitet - Omit<UpdateUserInput, 'password'> plus dem
daraus berechneten passwordHash; die Parameterform ist dafuer als
UpdateUserInput benannt und nicht neu erfunden. `const results: any[]`
wird Pick<User, keyof typeof PLATFORM_USER_SELECT>[], die Spaltenauswahl
steht als Konstante daneben.
- tenant.controller.ts:69: Elementtyp aus dem hergeleitet, was die Schleife
hineinlegt (fuenf Tenant-Spalten plus userCount).
BEFUND 3 (D-03, gemeldet, kein Verhalten betroffen) Die naheliegende
Prisma-Schreibweise Prisma.UserGetPayload<{ select: typeof X }> laesst
rls-access-inventory.spec.ts rot werden: der Erkenner zaehlt JEDE
select:-Angabe ausserhalb eines erkannten Modellaufrufs als Verstoss und
unterscheidet Typposition nicht von Aufrufposition. Gemessen beim ersten
Versuch. Der Erkenner ist die Mandantenkontrolle (T-M34-03) und wurde
NICHT aufgeweicht - stattdessen leitet der Zeilentyp ueber Pick<User, ...>
her, was ohne das Wort select auskommt. Begruendung steht am Typ.
noExplicitAny in apps/api/src: 38 -> 31. type-check 4/4, lint 5/5 (0
error), apps/api 72/1143, apps/web 73/531, rls-access-inventory 30/30.
noNonNullAssertion 56, as unknown as 33, Unterdrueckungsmarker 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
catch (e: any) in groups, module-grants, ldap, user, admin-seed, calendar
und vier tenders-Diensten auf catch (e: unknown) umgestellt. Die
Eingrenzung passiert an der Verwendungsstelle, nicht per Zusicherung.
Neu: apps/api/src/prisma/prisma-error.ts mit prismaErrorCode() und
prismaErrorTarget(). Bewusst Form-Pruefungen statt instanceof
Prisma.PrismaClientKnownRequestError - gemessen: samtliche Testdoppel in
apps/api werfen new Error(...) mit angehaengtem .code (groups, user, ldap,
tenders, module-grants, admin-seed) und dashboard.service.spec.ts:451 ein
reines { code: 'P2002' }. Ein instanceof-Test haette all diese Werte in den
anderen Zweig geschickt - Verhaltensaenderung, verboten nach D-03/T-M34-06.
Die Helfer bilden err?.code und err?.meta?.target eins zu eins ab.
ldap.service.ts liest zusaetzlich meta.target; prismaErrorTarget() gibt
unknown zurueck, weil der Bestand dort Array UND Zeichenkette getrennt
behandelt - ein engerer Typ waere eine Behauptung.
calendar.service.ts:341 nutzt instanceof Error statt e?.message: gemessen
wirft validateUrlNotPrivate() ausschliesslich ForbiddenException (der
eigene catch dort setzt jeden Fremdfehler in eine um), also trifft
instanceof dieselben Faelle. Ersatzzweig 'URL not allowed' unveraendert.
noExplicitAny in apps/api/src: 56 -> 38. type-check 4/4, lint 5/5 (0
error), apps/api 72/1143, apps/web 73/531, rls-access-inventory 30/30.
noNonNullAssertion 56, as unknown as 33, Unterdrueckungsmarker 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
@UploadedFile()/@UploadedFiles() in cert-manager.controller auf
UploadedFileLike. Der Dienst nimmt CertFileLike = Pick<UploadedFileLike,
'buffer' | 'originalname'> - genau die zwei Felder, die er liest; mimetype
und size bleiben draussen, weil kein Zweig sie anfasst.
Belegt statt behauptet: keiner der sechs FileInterceptor/FilesInterceptor-
Aufrufe in apps/api/src setzt eine storage-Option, also gilt multers
memoryStorage, also ist buffer ein Buffer. @types/multer bleibt
uninstalliert (D-04).
Damit fallen 25 Zusicherungen der Form file.buffer as Buffer und
file.originalname as string ersatzlos weg - sie standen nur da, weil file
ein any war. as unknown as bleibt bei 33, noNonNullAssertion bei 56.
noExplicitAny in apps/api/src: 66 -> 56 (Ausgang der Aufgabe: 149,
Schranke des Plans: 75). type-check 4/4, lint 5/5, apps/api 72/1143,
apps/web 73/531.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.
Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.
BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.
BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).
Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.
noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
apps/api/src/auth/types/auth-user.ts angelegt: AuthUser, AuthenticatedRequest,
LocalAuthenticatedRequest, LoginUser, JwtPayload, UploadedFileLike. Jedes Feld
traegt seine Herkunft als Kommentar.
tenantId ist string, hergeleitet und nicht gewaehlt: die Spalte User.tenantId
ist in schema.prisma Pflicht, beide Signierstellen schreiben genau sie, und
der Bestand beschreibt dasselbe Objekt in SessionUser schon so. Der
SUPER_ADMIN-Zweig in TenantGuard spricht nicht dagegen - der Waechter liest
AuthUser gar nicht, und dass es den Zweig gibt, steht als null in
AuthenticatedRequest.tenantId weiter im Typsystem. tenant.guard.ts bleibt
unberuehrt.
role ist die Aufzaehlung Role: schema.prisma deklariert die Spalte so, die
SQL-Funktion auth_lookup_user_by_username gibt sie als "Role" zurueck. Die
Handannotation role: string in AuthLookupUserByUsernameRow war eine zweite
Fassung desselben Wertes und faellt damit weg.
SessionUser und UploadedPng in bug-reports.service.ts sind jetzt Pick<> der
neuen Typen statt eigener Beschreibungen.
Fixtures in auth.controller.spec.ts ergaenzt: sie uebergaben einen Aufrufer
ohne username und ohne mustChangePassword - eine Form, die JwtStrategy nie
erzeugt. Testzahlen unveraendert.
noExplicitAny in apps/api/src: 149 -> 137. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
$allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
(rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
gemessen verworfen - bricht das Testdoppel in
prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
.map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
(match: { tender: unknown }), die den Wert nur deshalb auf unknown
verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.
noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Die Fallmarke 'doe-opendata' stand unmittelbar ueber default und fiel
in denselben Zweig — 260921-bi2 liess sie stehen, weil der Kommentar
darunter Absicht dokumentiert. Die Absicht laesst sich ohne die
Fallmarke ausdruecken und wird dabei deutlicher: der erweiterte
Kommentar traegt jetzt beide Aussagen (DÖE-Quelle landet hier UND
kuenftige additive SourceType-Mitglieder sollen ebenfalls hier landen
statt zu scheitern). Kein Verhaltenswechsel — derselbe Zweig wie
vorher. Belegt durch die vorhandenen 18 tender-normalizer-Tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Die urspruengliche findIndex((c) => c === null)-Pruefung erzeugte einen
neuen lint/complexity/useIndexOf-Fund (info) und hob TOTAL dadurch auf 430
statt der erwarteten 429 an — die Endverifikation des Plans deckte das auf
(D-06: TOTAL darf nirgends anders steigen).
@types/node-forge deklariert Bag.cert als "Certificate | undefined", die
node-forge-Laufzeit setzt bei einem unlesbaren Bag aber "null" (nicht
undefined). Ein blosses indexOf(null) ist deshalb nicht typsicher; die
Pruefung testet jetzt ausdruecklich auf beide Werte, was fuer biome kein
Single-Value-Vergleich mehr ist und keinen useIndexOf-Vorschlag ausloest.
TOTAL steht jetzt bei 429 wie geplant, NONNULL unveraendert bei 6, tsc und
beide Testsuiten weiterhin gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
imapflow typisiert uid in FetchMessageObject als Pflichtfeld (lib/imap-flow.d.ts
Zeile 469, Kommentar "Always included in the response"). Die beiden
Zusicherungen msg.uid! sicherten also einen Wert ab, der ohnehin nicht fehlen
kann. Reine Lesbarkeitsaenderung ohne Verhaltensaenderung, tsc bleibt gruen.
Die drei verbliebenen Zusicherungen (tenders.controller.ts:244,
favorites-widget.tsx:149, sidebar.tsx:80) bleiben unveraendert — jede durch
eine konkrete vorgelagerte Zeile garantiert (Provider-Eintrag, gemeinsame
Herleitung aus favorites, Anlegen des Map-Eintrags direkt davor).
NONNULL 8 -> 6, keines davon mehr in imap.provider.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709). Die drei
Zusicherungen in parseCert, mergeCerts und convertCert behaupteten also
etwas Falsches, obwohl die Folge bereits behandelt war: alle vier Pfade
antworteten schon vorher mit 400, nie mit 500 (mit einer selbst gebauten
83-Byte-PFX nachgemessen).
Ersetzt die drei Zusicherungen durch ausdrueckliche Pruefungen mit
praeziser BadRequestException. In mergeCerts wird kein Zertifikat mehr
verschluckt: alle Bags werden auf Vollstaendigkeit geprueft, bevor die
Liste ueber ein Typpraedikat zurueckgegeben wird.
RED-Tests zuerst geschrieben und mit den heutigen Meldungen ("Failed to
extract certificate details" / "Failed to create merged certificate
output" / "Failed to convert certificate to pem: serialization error")
rot bestaetigt, dann die Waechter ergaenzt: GREEN.
Verhaltensaenderung ausdruecklich beabsichtigt (D-02): nur der Text der
Fehlermeldung fuer diese eine Eingabeklasse aendert sich, der Statuscode
bleibt in allen vier Pfaden 400.
NONNULL 11 -> 8, davon 3 in cert-manager.service.ts (Zeilen 133/516/661
bleiben unveraendert — durch Hash-Laenge bzw. vorgelagerte Passwort-
Pruefung garantiert).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
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
JwtStrategy.validate liess mustChangePassword auf dem Weg vom Token zu
request.user fallen; der global registrierte ForcePasswordChangeInterceptor
prueft genau dieses Feld und hat seit seiner Einfuehrung nie etwas
blockiert. validate() reicht das Feld jetzt durch (strenger Vergleich mit
true, Alt-Sitzungen ohne den Anspruch bleiben unveraendert unbetroffen).
Zusaetzlich die Erlaubnisliste des Abfangers von Teilstring-Vergleich auf
exakten Abgleich von Methode UND Pfad umgestellt (Absicherung gegen eine
kuenftige kollidierende Route, heute nicht ausnutzbar).
Nahttest gepinnt, der gegen den alten Quelltext nachweislich scheitert
(6 von 12 neuen Faellen rot vor der Aenderung, gruen danach).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Aufgabe 2: vier sichere Biome-Regeln (useImportType pfadgebunden auf
apps/web+packages, noUselessEscapeInRegex, useConst,
useExponentiationOperator) sowie fuenf ungesicherte Regeln
(useNodejsImportProtocol, useLiteralKeys, useOptionalChain, useTemplate,
useParseIntRadix) angewendet und den gesamten Diff von Hand gelesen
(ldap.service.ts zeichenweise gegen Gross-/Kleinschreibung der
AD-Merkmale, auth.service.ts/jwt.strategy.ts gegen Durchwinken bei
fehlender Sitzung geprueft)
- noUselessSwitchCase bleibt bewusst stehen (tender-normalizer.service.ts:60,
die Fallmarke dokumentiert Absicht)
- Toter Code (D-03): fuenf folgenlose Auffangvariablen entfernt, eine
nicht benutzte Funktion (forSystemQuery, Pruefskript) entfernt, ein
positionsgebundener Dekoratorparameter umbenannt (current-user.decorator.ts),
fuenf Symptomfunde entfernt und als Folgeaufgaben zu melden (siehe unten)
- Sechs weitere, im Plan nicht namentlich gelistete aber
gleich-kategorische Dead-Code-Fundstellen in Testdateien zusaetzlich
bereinigt (groups.service.spec.ts, cert-manager.test.tsx,
ldap.service.spec.ts, prisma-tenant.extension.spec.ts x3) — noetig, um
die vom Plan selbst verlangten Nullstaende bei noUnusedVariables/
noUnusedImports/noUnusedFunctionParameters zu erreichen
Dekoratordaten aus apps/api unveraendert (593 Zeilen, sha256 6e1583f1...).
Endstand 620 Befunde (541 echt, 79 Test) statt der im Plan geschaetzten
621/542 — eine Differenz von 1, weil das Streichen des Namens aus
`catch (e: any)` in calendar.service.ts (Symptom-Fix) den dort ebenfalls
gemeldeten noExplicitAny-Befund miteliminiert; das ist eine erwuenschte
Nebenwirkung, keine Regression. Fehlerstufe 0, beide Testlaeufe
punktgleich gruen (69/1124, 66/459), pnpm type-check 4/4, pnpm lint
--force 5/5.
Folgeaufgaben aus D-03 (nicht in diesem Vorgang behoben):
- force-password-change.interceptor.ts: Freigabeliste prueft nur den Pfad,
nicht die HTTP-Methode
- change-password/page.tsx: nach erzwungenem Wechsel bleibt die Person auf
der Seite stehen (keine Weiterleitung, keine Aktualisierung der
Benutzerablage)
- VehicleTable.tsx: Loeschschaltflaeche hat keinen Besetztzustand, laesst
sich doppelt ausloesen
- SplitTab.tsx: downloadAllAsZip erhielt eine ungenutzte
Uebersetzungsfunktion, Hinweis auf fest verdrahtete Texte im Zip-Pfad
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
WebView2 (Windows) sieht im User-Agent aus wie Edge, WebKitGTK (Linux) wie
Safari — im Postfach war eine Client-Meldung von einer Browser-Meldung
nicht zu unterscheiden. Neuer reiner Helfer origin.ts leitet aus vier
optionalen DTO-Feldern (Desktop-App) bzw. dem User-Agent (Browser) ein
Betreff-Kuerzel und eine Zeile "Herkunft: ..." ab; rein informativ,
laengenbegrenzt, nichts wird gespeichert (T-GZA-01). Browser-Pfad ist
damit Ende-zu-Ende fertig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
- Neuer oeffentlicher Endpunkt (vor download/:platform): base-Origin wird
per safeOrigin validiert (nur http/https, kein Pfad/Query/Fragment/
Userinfo, sonst 400) und nur zum Bau der absoluten Download-URL genutzt,
nie serverseitig abgerufen
- 204 ohne Body bei fremder Plattform/Architektur, fehlendem Manifest,
fehlender Signatur oder fehlendem/ungueltigem updateVersion; sonst
{ version, pub_date (nur RFC 3339), url, signature, notes }
- Manifest-Felder signature (je Plattform) und updateVersion optional in
@tessera/shared, Eintragspruefung akzeptiert nur String-Signaturen
- 10 neue Spec-Tests, Test 11 erweitert (23 gesamt); latest/download
unveraendert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CHANGELOG.md: je ein Stichpunkt unter Neu (Sortierpfeile) und Behoben
(Symbol trotz Zertifikatsfehler/interner Adresse)
- docs/anleitung-anwender.md: Tabellenzeile „Favoriten" nennt die Pfeile
und den Browser-Ersatzweg fuer das Symbol
- docs/mandantentrennung-zugriffsklassifikation.md: Nachtrag zu
favoriteLink — reorder() laeuft ueber withTenantTransaction() ohne
Benutzerdimension in der Sitzung, Stand bleibt gebunden
- prisma-tenant.extension.ts: Kopfkommentar-Nachtrag, favorites.service.ts
(reorder) ist der erste Nutzer-CRUD-Aufrufer von withTenantTransaction()
— nur Kommentartext, Funktionscode unveraendert (prisma-tenant.extension.spec.ts
und rls-access-inventory.spec.ts weiterhin gruen)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- icon-discovery.service.ts: undicis eigenes fetch mit Modul-Singleton
LENIENT_TLS_AGENT (Agent({ connect: { rejectUnauthorized: false } }))
als dispatcher in fetchWithRedirectGuard, der einzigen Ausgangsstelle
fuer HTML-Ermittlung und Icon-Byte-Holen; SSRF-Schutz unveraendert
- undici 7.28.0 (bereits im Lockfile aufgeloest) als direkte Abhaengigkeit
von @tessera/api via pnpm add --offline
- PUT /favorites/order (ReorderFavoritesDto) vor den :id-Routen;
FavoritesService.reorder() setzt position=index fuer die Favoriten
eines Widgets in EINER withTenantTransaction, userId+widgetId in jeder
Bedingung (zweites Netz), eine BadRequestException fuer alle
Abweichungen (T-JDD-06)
- getIcon: X-Content-Type-Options nosniff + restriktive CSP (T-JDD-02)
- 10 neue Tests (3 Dispatcher, 7 reorder); volle API-Suite 68 Dateien/
1101 Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Zeichen-Whitelist /^[A-Za-z0-9._-]+$/ liess Namen wie ".." durch, weil
Punkt und Bindestrich erlaubte Zeichen sind -- path.join(desktopDistDir, '..')
loest aber in den Elternordner auf und unterlaeuft genau die Verteidigung in
der Tiefe (T-18-02), die diese Zeile laut Kommentar herstellen soll. Jetzt
werden "." und ".." explizit abgelehnt UND der aufgeloeste Pfad zusaetzlich
gegen desktopDistDir geprueft (haelt auch kuenftige Varianten ab, falls die
Whitelist anderswo wiederverwendet wird). Neue Testfaelle fuer manifest-Eintraege
namens "..", "." und "../manifest.json".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein kaputter Plattform-Eintrag (z. B. fehlendes name-Feld oder ungueltiger
sha256) fiel bisher erst spaeter unbemerkt durch -- entry.name === undefined
wurde zu "undefined" gecoerct und als Dateiname gesucht. getManifest()
prueft jetzt jeden vorhandenen Plattform-Eintrag (name: string, size: number,
sha256: 64-stelliger Hex-String) und behandelt ein kaputtes Manifest wie ein
fehlendes (404 + Warn-Log), statt die kaputte Form durchzureichen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die duenne Strecke Skript -> Abbild -> API beweisen (D-08, D-10):
- desktop-collect.sh sammelt AppImage/exe ein, schreibt manifest.json
(Version, Kanal, Commit, Groesse, SHA-256) ohne Secrets
- apps/api/src/desktop/: neues Modul mit GET /desktop/latest und
GET /desktop/download/:platform, beide @Public(); Plattform-Whitelist
vor jedem Dateisystemzugriff, Dateiname ausschliesslich aus dem
Manifest (T-18-01, T-18-02)
- packages/shared: DesktopPlatform/-Manifest(File)/-Latest(Response)
Typen
- Dockerfile kopiert desktop-dist/ in die runner-Stufe; desktop-dist/
per .gitkeep + .gitignore versioniert (leeres Verzeichnis, Pakete
bleiben ungetrackt)
- HTTP-Durchstich-Spec (8 Tests) via NestFactory, kein fs-Mock; echtes
Temp-Verzeichnis + unabhaengig berechneter SHA-256
Abweichung: DesktopController braucht @Inject(DesktopService) explizit
— Vitest transpiliert ueber esbuild, das emitDecoratorMetadata nicht
abbildet, sonst bleibt desktopService bei einem echten NestFactory-Bau
undefined (Rule 3, Blocker).
Lokal bewiesen: neu gebautes API-Abbild liefert /desktop/latest (200)
und /desktop/download/linux (200, attachment) aus, auch ueber
/api-proxy/ des Web-Containers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Dockerfiles: globale ARG APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME (Vorgabe dev),
web-builder setzt NEXT_PUBLIC_APP_* vor pnpm build (Bauzeit-Einbettung), runner-Stufen
setzen ENV APP_* fuer die Laufzeit
- .gitea/scripts/publish-images.sh: Kanal und Etiketten allein aus GITHUB_REF, --print-plan
ohne Docker, Zweig live ohne Tag = nichts zu tun (T-KU1-07)
- ci.yml: Trigger main, live und Tags v*; publish mit fetch-depth: 0 und Skriptaufruf
- docker-compose.prod.yml: image ...:${IMAGE_TAG:-beta} fuer web und api
- Falsifizierung lokal: mit Build-Args v9.9.9-test in API-Env, dist-Zeile und Web-Bundle
(1 Datei), ohne Build-Args dev/dev und 0 Treffer im Bundle
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
- auth.service.ts: letzter Satz des Kopfkommentars ueber adminResetPassword nennt T-FH9-05 nicht mehr als offen, sondern verweist auf den seit 260914-ebg (WINDOWS #29) identischen Riegel in UserController.update()/remove()
- Falsifizierung: Rueckbau des Task-1-Commits (git apply -R) macht Test 9 und Test 13 rot (Tests 2 failed | 14 passed (16)), danach byte-identisch wiederhergestellt (git checkout --, git status --porcelain leer)
- Rule 1 Nebenfund: acht neue Tests in user.controller.spec.ts trugen sechs ueberfluessige `as any`-Umschreibungen (UpdateUserDto ist vollstaendig optional, siehe planning_measurements), die die Biome-Warnungen dieser Datei von 25 auf 31 trieben — entfernt, damit die relative Biome-Schwelle der Baseline (25) wieder eingehalten wird, ohne die Schwelle anzuheben
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
- update(): Riegel nach der Mandantengrenze, vor der dto.role-Pruefung — Nicht-SUPER_ADMIN darf SUPER_ADMIN-Ziel nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail)
- remove(): derselbe Riegel nach der Mandantengrenze, vor userService.delete
- acht neue Tests (Test 9-16): drei Angriffsformen, SUPER_ADMIN-gegen-SUPER_ADMIN-Regression, ADMIN-gegen-USER-Regression, Reihenfolge-Ordnungstests je Handler
- RED-Lauf vor dem Riegel: Tests 2 failed | 14 passed (16) (Test 9, Test 13 rot); GREEN danach: Tests 16 passed (16)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
- 30 verbleibende forTenant()-Aufrufstellen in sieben Diensten (calendar 6,
dashboard 9, favorites 5, tender-email-config 3, tender-notification-pref 2,
tender-rss-feed 2, tender-triage 3) reichen userId als drittes Argument
durch. tender-digest.scheduler.ts bleibt zweistellig (Hintergrunddienst,
Etappe 3c), mit Begruendung im Kommentar. Keine Methodensignatur, kein
Controller angefasst, keine anwendungsseitige userId-Filterung entfernt.
- rls-scratch-check.mjs: zwoelf Extraktionsstellen auf die neue Migration
umgeleitet (TenderEmailConfig/TenderNotificationPref/TenderSavedSearch/
TenderTriage/TenderRssFeedSource in runTendersAreaChecks, SearchProvider in
runSearchProviderAreaChecks/runDashboardAreaChecks, DashboardLayout/
WidgetInstance, CalendarSource/FavoriteLink samt regelstand-eindeutig-Gates).
SearchProvider/TenderRssFeedSource jetzt mit extractAllPolicySql (4 Regeln).
runUserDimensionChecks() um die uebrigen neun Tabellen erweitert (neue
Routine runCommandSeparatedPersonalTableCheck fuer die zwei NULL-faehigen
Tabellen inkl. gemeinsame-Zeile-Pruefungen).
- Sechs Loch-Pruefungen umgedreht (dashboardlayout, widgetinstance,
searchprovider, calendarsource, favoritelink-Doppelaussage getrennt) —
alte Messung ohne Benutzer bleibt unter neuem Namen, Umkehrung MIT
Benutzer erwartet das Gegenteil; kein alter Name mehr als Kennung.
- Baseline: 1020/62 Tests weiterhin gruen, Typpruefung sauber, Werkzeug
203/203 bestanden (vorher 146).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- Neue Migration 20260911120000_rls_user_dimension_personal_tables: current_user_id()
(NULLIF-gefaltet), zehn persoenliche Tabellen umgestellt (acht als eine Regel,
SearchProvider/TenderRssFeedSource als je vier befehlsgetrennte Regeln), vier
Verwaltungstabellen bewusst unveraendert. Lokal angewendet (migrate deploy,
Prisma-Binary aus apps/api/node_modules/.bin), schema.prisma unveraendert.
- forTenant(prisma, tenantId, userId?): beide set_config in EINER getaggten
Anweisung, $transaction-Array bleibt bei zwei Eintraegen (WINDOWS #20),
Leerstring ohne Benutzer statt Weglassen.
- tender-saved-search.service.ts: alle vier forTenant()-Aufrufe reichen userId
durch; Detektor-Regex bestaetigt 4 Treffer.
- rls-scratch-check.mjs: current_user_id() aus der neuen Migration geschnitten
(nicht getippt), drei Funktionsfaelle gemessen, neue runUserDimensionChecks()
mit generiertem Client fuer TenderSavedSearch (vier Wahrheiten + Spaltenabgleich),
die alte Loch-Pruefung tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar
umgedreht (alte Messung unter neuem Namen erhalten, neue Umkehrung MIT Benutzer).
sqlStateOf() um Message-Fallback ergaenzt (RLS-Ablehnung ueber generierten
Client traegt den SQLSTATE nur im Fehlertext, nicht in .meta.code).
- Baseline: 1020/62 Tests, Typpruefung sauber, Werkzeug 146/146 bestanden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- Klassifikationsdokument: Klassen-Verteilung (35/21/14/2, 72 Paare),
Uebersichtsabsatz (zweite methodische Luecke, geschlossen) und
Hintergrunddienst-Nachtrag (ldap/getAllActiveConfigs reicht auch in
LdapFieldMapping hinein) aus Tabelle/Greps abgeleitet, nicht abgeschrieben
- Kritikschrift: Nachtrag (260911-mkj) unter (n4) im Bereich tenant, Vermerk
im Etappe-2-Abschluss dass #27 geschlossen ist
- WINDOWS.md: #27 fixed (Nachweis: vierte Erkennungsform, Proben,
Zwischenmessung 7/3/1); neuer Eintrag #33 fuer die Empfaenger ausserhalb
der vier Erkennungsformen (tenders.seed.ts, backfill-tender-source.ts)
- Spec: WINDOWS #TBD-MKJ-Platzhalter durch #33 ersetzt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- rls-access-inventory.spec.ts: analyzeFile in analyzeSource(source, relPath)
herausgeloest, parseSchemaRelations() liest schema.prisma zur Testzeit,
vierte Erkennung loest include:/select:/_count:/Relationsfilter ueber
SCHEMA_RELATIONS auf das Zielmodell auf und traegt es als eigene
Fundstelle (gebunden/ungebunden nach Empfaenger) ein
- drei Waechter (Rohzahl vs. erkannte Modellaufrufe, Stale-Check fuer
RELATION_SPEC_EXCEPTIONS, unresolvedRelationSpecValues leer), zwei
Schema-Tests, acht gepinnte Proben (WINDOWS #27 ungebunden/gebunden,
reale ldap-Form, verschachtelte where-Kette, Negativprobe, _count: true,
unbekannter Empfaenger, Konstantenaufloesung)
- Bestandsaufnahme (docs/mandantentrennung-zugriffsklassifikation.md): sieben
neue Paare, drei fortgeschriebene Staende (davon ldapFieldMapping mit
Klassenwechsel auf beides), Kopfabsatz "Erkennungsluecke GESCHLOSSEN"
ersetzt den alten "seit 260911-e2s vermessen"-Absatz, 65 -> 72 Paare
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- .planning/WINDOWS.md: drei neue offene Eintraege (#30 Startpfad des
Mailmoduls, #31 verschluckte Leere favorites, #32 verschluckte Leere
settings); Platzhalter WINDOWS #TBD-GWH in settings.service.ts und
mail.module.ts durch #30 ersetzt
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeilen
favorites (0/8, war 7/0) und settings (1/3, war 4/0) neu gemessen;
Summenzeile 68/178 (Endstand Etappe 2); Bestandsaufnahme (favoriteLink
gebunden, smtpConfig gemischt, neue Zeile widgetInstance/gebunden);
Klassen-Verteilung 65 Paare (33/17/13/2); Hintergrunddienst-Abschnitt mit
sechstem Fall (mail.module.ts, WINDOWS #30) und erfuellter
Befund-K-Bedingung; neuer Punkt in "Was diese Etappe NICHT entscheidet"
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtraege unter Befund
K in (t4) und im Uebergaben-Absatz von (d4) -- Reihenfolgebedingung
erfuellt; ## Etappe 2 -- Abschluss mit den derivierten Endzahlen
- docs/anleitung-entwicklung.md: 23 RLS-Tabellen statt sieben, FavoriteLink
nicht mehr als Tabelle ohne Regel, tenantPrisma statt manuellem
tenantId-Filter als gelebter Stil
- rls-access-inventory.spec.ts wieder gruen (11/11), volle Suite 994/994,
Werkzeug 137/137; zwei Dokument-Falsifizierungen durchgefuehrt und
zurueckgenommen (siehe SUMMARY)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- favorites.service.ts: alle fuenf Methoden nehmen tenantId als ersten
Parameter, laufen je ueber EINEN Klienten tenantPrisma (7 gebundene
Favoritenzugriffe, 5 Aufrufstellen); create() prueft vor der Icon-Suche,
dass das Ziel-Widget dem Aufrufer gehoert (T-GWH-05, Befund F aus Aufgabe 1
bestaetigt den Fremdschluessel-Durchgriff) -- Widget not found fuer alle
drei Faelle (existiert nicht/Kollege/fremder Mandant)
- favorites.controller.ts: reicht tenantId an alle fuenf Aufrufe durch,
extractContext unveraendert (dashboard-Praezedenzfall)
- settings.service.ts: getSmtpConfig/saveSmtpConfig/getDecryptedSmtpConfig
je EIN Klient (3 gebundene Zugriffe, 3 Aufrufstellen) -- Befund K damit
erfuellt; Startpfad umbenannt in loadAnySmtpConfigForStartupTransport(),
bleibt bewusst ungebunden (sechster Fall der Hintergrunddienst-Falle,
WINDOWS #TBD-GWH -- Aufgabe 3 vergibt die Nummer)
- mail.module.ts: ruft den umbenannten Startpfad auf, Kommentar nennt beide
Zustaende statt "single-tenant default"
- Vier Falsifizierungsnachweise durchgefuehrt und zurueckgenommen (siehe
SUMMARY): (a) 3 Faelle rot, (b) 4 Faelle rot, (c) 9 Faelle rot, (d) 4 Faelle
rot
Bekannt und erwartet (siehe SUMMARY, Praezedenzfall 260911-fh9): zwischen
dieser Aufgabe und Aufgabe 3 ist rls-access-inventory.spec.ts rot (2
Faelle) -- die Klassifikationstabelle ist noch nicht nachgezogen, das ist
Aufgabe 3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- favorites.service.spec.ts (NEU, 23 Faelle): ungebundener Nachbau hat KEIN
favoriteLink/widgetInstance-Modell; erwartet die Zielsignatur
list/create/update/remove/getIconBytes(tenantId, ...) und den
Widget-Besitzriegel in create -- scheitert erwartungsgemaess an
"Cannot read properties of undefined" gegen den heutigen Dienst (22/23 rot)
- settings.service.spec.ts (NEU, 20 Faelle): ungebundener Nachbau bietet fuer
smtpConfig NUR findFirst, gebundener Klient NUR findUnique/upsert; erwartet
loadAnySmtpConfigForStartupTransport() (Startpfad-Umbenennung) und den
Null-Klienten-Nachweis fuer den Startpfad -- 18/20 rot
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- runFavoritesAreaChecks (8 Pruefungen) und runSettingsAreaChecks (9 Pruefungen)
erweitern rls-scratch-check.mjs auf 137 bestandene Pruefungen
- FavoriteLink-Wegwerftabelle traegt den Fremdschluessel auf WidgetInstance
(Befund C, WINDOWS #27), SmtpConfig-Wegwerftabelle den Eindeutigkeitsindex
- Ergebnis Pruefung 7: der Fremdschluessel prueft am Zeilenschutz vorbei
(steuert den Besitzriegel in Aufgabe 2); Pruefung 8: ungebundenes upsert
wirft PrismaClientUnknownRequestError, dieselbe Klasse wie 260910-krx
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Abschnitte
## Bereich favorites (f1-f5), ## Bereich settings (s1-s5) und
## Etappe 2 -- Abschluss; Nachtraege unter Befund K in (t4) und im
Uebergaben-Absatz von (d4) -- die Reihenfolgebedingung ist erfuellt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- auth.controller.spec.ts: NEU. Mandantenquelle je Handler (das Claim fuer
me/changePassword; fuer die oberste Rolle der Mandant des Ziels aus dem
Fan-out), unbekanntes Ziel, null-Durchreichung von me, Rollen-Metadaten
(ROLES_KEY) und Public-Metadaten (IS_PUBLIC_KEY) fuer alle sieben
Handler — 23 Faelle; Falsifizierungsnachweis am Fan-out-Zweig
durchgefuehrt und zurueckgenommen
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile auth
auf 3/10 (war 8/5), Summenzeile 78/167, Bestandsaufnahme-Zeile
auth.service.ts/user auf gebunden (keine gemischt-Zeile mehr fuer diese
Datei), Klassen-Verteilung unveraendert bei 64 Paaren mit
Stand-Vermerk, Hintergrunddienst-Abschnitt ohne sechsten Fall,
Etappe-3-Anmeldeweg-Punkt in "Was diese Etappe NICHT entscheidet";
beide Dokument-Falsifizierungen durchgefuehrt und zurueckgenommen
- .planning/WINDOWS.md: zwei neue offene Eintraege (#28 verschluckte Leere
im Frontend, Familie #23/#25/#26; #29 Rechteausweitung ADMIN->SUPER_ADMIN
im Schwesterweg PATCH /users/:id, T-FH9-05)
Baseline wiederhergestellt: 951/951 Tests gruen in 60 Dateien (927+23
neue Faelle plus der in Aufgabe 2 erwartungsgemaess rote Test), Werkzeug
120/120, Typpruefung sauber.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- auth.service.ts: die drei Nach-Anmeldungs-Methoden binden je ueber genau
einen Klienten tenantPrisma; adminResetPassword verweigert einem
Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04); die drei
$queryRaw-Anmeldesuchen bleiben unveraendert auf dem ungebundenen Klienten
- auth.controller.ts: me/changePassword reichen user.tenantId aus dem Claim
durch; adminResetPassword verzweigt ueber resolveTargetTenantId nach Rolle
(ADMIN: eigener Mandant; SUPER_ADMIN: gebundener Fan-out
UserService.findByIdForPlatformAdmin) — schliesst die Rechteausweitung
ueber die Mandantengrenze (T-FH9-01)
- auth.module.ts: importiert UserModule, zyklusfrei gemessen
- auth.service.spec.ts: Identitaets-Attrappe ersetzt durch zwei
unterscheidbare Klienten (__makeBoundClient); 29 Faelle, drei
Falsifizierungsnachweise durchgefuehrt und zurueckgenommen
Bekannt und erwartet: rls-access-inventory.spec.ts ist nach diesem Commit
kurzzeitig rot (Bestandsaufnahme-Zeile auth.service.ts/user zeigt noch
"gemischt", gemessen ist jetzt "gebunden") — wird in Aufgabe 3 desselben
Plans geschlossen (927/928 Tests gruen, ein bekannter, in Aufgabe 3
behobener Fehlschlag, kein neuer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- rls-scratch-check.mjs: dreizehnter Abschnitt runAuthAreaChecks, getrennt
von runAuthLookupChecks — misst die Grenze zwischen Anmeldeweg (drei
SECURITY-DEFINER-Funktionen, unveraendert) und Nach-Anmeldung (getMe,
changePassword, adminResetPassword) ueber den generierten Client; neuer
Helfer readSchemaModelScalarFieldNames() filtert Relationsfelder
ueber ihren Typ heraus
- zehn neue, namentlich benannte Pruefungen (120/120 insgesamt), sechs davon
ueber den generierten Client an der auf 15 Spalten erweiterten
Wegwerf-Tabelle "User"; pg_proc bestaetigt SECURITY DEFINER/STABLE/festen
Suchpfad/LIMIT 1 fuer alle drei Anmeldefunktionen
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
"## Bereich auth" (h1)-(h5) vor "## Verweis" — Signaltabelle je Pfad,
getMe-Kette als "verschluckt" statt "laut", Etappe-3-Vorbehalt,
Schwesterweg-Luecke in user.controller.ts
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
findAll/findOne/remove zaehlen Benutzer je Mandant jetzt ueber drei
gebundene Aufrufstellen (tenantPrisma.user.count mit where: { tenantId
}, in remove zusaetzlich isActive: true) statt ueber den
Relationszaehler, der nach dem Scharfschalten unbemerkt unter der
Regel von User gelaufen waere (260911-e2s, Aufgabe 1, Pruefungen 5-7).
Fan-out-Muster aus UserService.findAllForPlatformAdmin uebernommen; die
vier tenant-Zugriffe bleiben ungebunden (Tenant ohne Regel). Antwortform,
Meldungen und Statuscodes unveraendert.
tenant.controller.spec.ts legt die Testlage aus dem Nichts an (20
Faelle): Zwei-Klienten-Nachweis ueber __makeBoundClient, Rollen-
Metadaten-Test (Klasse SUPER_ADMIN, kein Handler ueberschreibt), Wachhund
gegen mehrfache Klientenerzeugung. Falsifizierungsnachweis durchgefuehrt:
der probeweise ungebundene Zaehler in findOne macht 2 Faelle rot mit
"Cannot read properties of undefined (reading 'count')" — die dkv-Form
der Falsifizierung, nicht nur eine falsche Zahl —, danach zurueckgenommen.
Klassifikation und Entwicklungsanleitung nachgezogen: 64 Paare (ein
neues, tenant.controller.ts/user), Uebersichtszeile 8/3, Klassen-
Verteilung 32 muss-mandantengebunden, Erkennungsluecke fuer
Relationseinbindungen im Kopf der Bestandsaufnahme benannt, "Zwei
belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster
Punkt aufgeloest). Beide Dokument-Falsifizierungsnachweise durchgefuehrt
(falsche Klasse macht rls-access-inventory.spec.ts rot, falsche
Uebersichtszahl macht das herleitende Gate rot), zurueckgenommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR