refactor(quick-260921-m34): Aufgabe 3c - Randschicht beurteilt, drei Befunde gemeldet, 15 bleiben mit Urteil

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
This commit is contained in:
2026-09-21 17:29:28 +02:00
parent 3892c5f3c6
commit d8fb9ae07d
9 changed files with 188 additions and 31 deletions
@@ -1,9 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import type { AuthProviderCallback } from '@microsoft/microsoft-graph-client';
import { CalendarEvent, CalendarProvider } from '../calendar.service';
/** Optionen, die ntlmPost() unten uebergibt — nichts darueber hinaus. */
interface NtlmOptions {
url: string;
username: string;
password: string;
domain: string;
workstation: string;
body: string;
headers: Record<string, string>;
}
/**
* Antwortform von httpntlm.post, beschrieben aus dem, was gelesen wird.
*
* `body` ist `Buffer | string`: httpreq (unter httpntlm) liefert eine
* Zeichenkette, solange `binary` nicht gesetzt ist (gemessen,
* httpreq@1.1.1/lib/httpreq.js:391) — hier wird es nicht gesetzt. Siehe die
* ausfuehrliche Begruendung in inbox/exchange-inbox.provider.ts.
*/
interface NtlmResponse {
statusCode: number;
body?: Buffer | string;
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const httpntlm = require('httpntlm') as {
post: (opts: any, cb: (err: Error | null, res: any) => void) => void;
post: (opts: NtlmOptions, cb: (err: Error | null, res: NtlmResponse) => void) => void;
};
const NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
@@ -47,15 +72,14 @@ function extractAttr(xml: string, tag: string, attr: string): string {
return attrMatch ? attrMatch[1] : '';
}
function ntlmPost(opts: {
url: string; username: string; password: string;
domain: string; workstation: string; body: string;
headers: Record<string, string>;
}): Promise<{ statusCode: number; body: string }> {
function ntlmPost(opts: NtlmOptions): Promise<{ statusCode: number; body: string }> {
return new Promise((resolve, reject) => {
httpntlm.post(opts, (err, res) => {
if (err) return reject(err);
resolve({ statusCode: res.statusCode, body: res.body?.toString('utf-8') ?? '' });
resolve({
statusCode: res.statusCode,
body: typeof res.body === 'string' ? res.body : (res.body?.toString('utf-8') ?? ''),
});
});
});
}
@@ -154,7 +178,7 @@ export class ExchangeProvider implements CalendarProvider {
);
const client = GraphClient.init({
authProvider: (done: (error: any, token: string) => void) => {
authProvider: (done: AuthProviderCallback) => {
// Use the password as the access token (OAuth bearer token)
// Users configure their OAuth token in the password field for Graph API
done(null, source.password || '');
@@ -310,7 +334,7 @@ export class ExchangeProvider implements CalendarProvider {
);
const client = GraphClient.init({
authProvider: (done: (error: any, token: string) => void) => {
authProvider: (done: AuthProviderCallback) => {
done(null, source.password || '');
},
});
@@ -1,5 +1,18 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import * as forge from 'node-forge';
/**
* Was `forge.pkcs7.messageFromPem()` bzw. `messageFromAsn1()` zurueckgeben —
* der mitgelieferte Typ aus `@types/node-forge`, nicht ein eigener.
*
* Nur die signierte Form traegt `certificates`; die Lesestellen grenzen
* deshalb mit `'certificates' in p7` ein. Das ist verhaltensgleich zum
* bisherigen `p7.certificates ?? []`: bei einer enveloped-Nachricht fehlt
* das Feld, und beide Schreibweisen liefern dann die leere Liste.
*/
type P7Message = forge.pkcs7.Captured<
forge.pkcs7.PkcsEnvelopedData | forge.pkcs7.PkcsSignedData
>;
import type { UploadedFileLike } from '../auth/types/auth-user';
/**
@@ -231,14 +244,16 @@ export class CertManagerService {
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
let p7: P7Message;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
const p7Certs: forge.pki.Certificate[] = p7.certificates ?? [];
const p7Certs: forge.pki.Certificate[] =
'certificates' in p7 ? p7.certificates : [];
if (p7Certs.length === 0) {
throw new Error('No certificate found in P7B/PKCS7');
}
@@ -270,6 +285,14 @@ export class CertManagerService {
);
// Key type and size
// BLEIBT als any, mit Begruendung (260921-m34, Aufgabe 3c, D-01/D-02):
// @types/node-forge kennt nur `PublicKey = rsa.PublicKey | ed25519.Key`
// (index.d.ts:232). Der EC-Zweig unten liest `curve` und
// `params.curve.q.bitLength()` — Felder, die node-forge zur Laufzeit
// liefert, die der mitgelieferte Typ aber GAR NICHT kennt. Eine
// Umdeutung ueber zwei Stufen wuerde dieselbe Luecke verdecken und
// zusaetzlich so aussehen, als sei sie geprueft. Ein ehrliches any mit
// dieser Zeile ist hier das bessere Ergebnis.
const pubKey = cert.publicKey as any;
let keyType = 'RSA';
let keyBits = 0;
@@ -283,13 +306,24 @@ export class CertManagerService {
}
// Subject Alternative Names
//
// BLEIBEN als any, mit Begruendung (260921-m34, Aufgabe 3c, D-01/D-02):
// @types/node-forge deklariert `Certificate.extensions` als `any[]`
// (index.d.ts:435) und sagt damit ueber den Inhalt einer Erweiterung
// NICHTS aus. Jede Schnittstelle, die wir hier selbst fuer `altNames`
// schrieben, waere unbelegt — der Compiler koennte sie an keiner
// Stelle gegen etwas pruefen, sie saehe aber geprueft aus. Die drei
// any-Stellen dieses Blocks bleiben deshalb sichtbar stehen, statt
// gegen eine Behauptung getauscht zu werden.
const sanExt = cert.extensions?.find((e: any) => e.name === 'subjectAltName');
const san: string[] = ((sanExt as any)?.altNames ?? []).map((n: any) =>
n.type === 2 ? (n.value as string) : `IP:${(n.ip ?? n.value) as string}`,
);
// Signature algorithm — OID → human-readable name
const sigOid = (cert.siginfo as any)?.algorithmOid ?? '';
// @types/node-forge deklariert siginfo.algorithmOid als string — die
// Zusicherung war ueberfluessig. Das ?. bleibt woertlich erhalten.
const sigOid = cert.siginfo?.algorithmOid ?? '';
const signatureAlgorithm = REVERSE_OIDS[sigOid] ?? sigOid;
// Fingerprints
@@ -369,7 +403,9 @@ export class CertManagerService {
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
// Der mitgelieferte Typ traegt hier: messageFromPem/messageFromAsn1
// liefern beide Captured<PkcsEnvelopedData | PkcsSignedData>.
let p7: P7Message;
if (isPemP7b) {
// PEM-wrapped PKCS7 (e.g. -----BEGIN PKCS7-----)
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
@@ -378,7 +414,7 @@ export class CertManagerService {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
certs = (p7.certificates as forge.pki.Certificate[]) ?? [];
certs = 'certificates' in p7 ? p7.certificates : [];
if (certs.length === 0) {
throw new Error('No certificates found in P7B/PKCS7 bundle');
}
@@ -505,14 +541,15 @@ export class CertManagerService {
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
let p7: P7Message;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
return (p7.certificates as forge.pki.Certificate[]) ?? [];
return 'certificates' in p7 ? p7.certificates : [];
}
});
} catch (err) {
@@ -543,6 +580,12 @@ export class CertManagerService {
// Open Question 1 resolution: toPkcs12Asn1(null, certs, password) works in node-forge 1.4.0
// null as the private key produces a cert-only PKCS12 bundle (no key bag — cert bag only)
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
// BLEIBT (260921-m34, Aufgabe 3c): node-forge 1.4.0 nimmt hier einen
// fehlenden Schluessel an und erzeugt ein reines
// Zertifikatsbuendel; @types/node-forge schliesst null aus. Die
// mitgelieferten Typen beschreiben die Bibliothek an dieser Stelle
// also nachweislich falsch — ein erzwungener Typ waere eine
// Behauptung ueber etwas, das nicht stimmt.
null as any, // cert-only PFX — null key accepted by node-forge 1.4.0
certs,
password!,
@@ -648,14 +691,16 @@ export class CertManagerService {
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
// siehe P7Message oben — mitgelieferter Typ, keine Behauptung.
let p7: P7Message;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
const p7Certs: forge.pki.Certificate[] = p7.certificates ?? [];
const p7Certs: forge.pki.Certificate[] =
'certificates' in p7 ? p7.certificates : [];
if (p7Certs.length === 0) {
throw new Error('No certificate found in P7B/PKCS7');
}
@@ -698,6 +743,12 @@ export class CertManagerService {
} else {
// PFX — cert-only PKCS12 bundle (Open Question 1: null key works in node-forge 1.4.0)
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
// BLEIBT (260921-m34, Aufgabe 3c): node-forge 1.4.0 nimmt hier einen
// fehlenden Schluessel an und erzeugt ein reines
// Zertifikatsbuendel; @types/node-forge schliesst null aus. Die
// mitgelieferten Typen beschreiben die Bibliothek an dieser Stelle
// also nachweislich falsch — ein erzwungener Typ waere eine
// Behauptung ueber etwas, das nicht stimmt.
null as any, // cert-only PFX — null key accepted by node-forge 1.4.0
[cert],
password!,
@@ -133,6 +133,14 @@ export class DkvSchedulerService implements OnModuleInit {
// Cast required: our minimal CronJob type doesn't match cron's full type signature.
// At runtime the object IS a full CronJob — SchedulerRegistry only calls stop() on it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// URTEIL: BLEIBT (260921-m34, Aufgabe 3, D-01). Gemessen: ohne die
// Zusicherung meldet tsc, dass das lokale `job` nur die Form
// `{ start(): void }` hat, waehrend addCronJob() einen vollstaendigen
// CronJob verlangt. Ursache ist der require()-Umweg aus 07-04 (pnpm-
// Isolation, `cron` ist nur eine mittelbare Abhaengigkeit). Das
// aufzuloesen hiesse, die Beschaffung der Klasse zu aendern — eine
// Verhaltensaenderung — oder `cron` direkt aufzunehmen — eine neue
// Abhaengigkeit. Beides ist hier verboten (D-03/D-04).
this.schedulerRegistry.addCronJob(jobName, job as any);
job.start();
+28 -3
View File
@@ -1,6 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const httpntlm = require('httpntlm') as { post: (opts: any, cb: (err: Error | null, res: any) => void) => void };
const httpntlm = require('httpntlm') as {
post: (opts: NtlmOptions, cb: (err: Error | null, res: NtlmResponse) => void) => void;
};
import type { InboxAttachment, InboxConfig, InboxEmail, InboxMessage } from './inbox-provider.interface';
import type { InboxProvider } from './inbox-provider.interface';
@@ -236,11 +238,34 @@ interface NtlmOptions {
rejectUnauthorized?: boolean;
}
/**
* Antwortform von httpntlm.post, beschrieben aus dem, was der Aufrufer
* unten liest — mehr nicht.
*
* BEFUND (260921-m34, Aufgabe 3c, D-03): `body` ist bewusst
* `Buffer | string`. httpntlm reicht an httpreq durch, und httpreq gibt den
* Rumpf als ZEICHENKETTE zurueck, solange die Option `binary` nicht gesetzt
* ist (gemessen in httpreq@1.1.1/lib/httpreq.js:391) — keiner der beiden
* Aufrufer in diesem Baum setzt sie. Die Testdoppel reichen dagegen einen
* Buffer herein. Der Bestand rief hier unbesehen `.toString('utf-8')` auf;
* das funktioniert bei einer Zeichenkette nur, weil String.toString() sein
* Argument ignoriert. Beide Formen kommen also wirklich vor, der Typ nennt
* beide, und die Fallunterscheidung unten liefert fuer jede exakt dasselbe
* Ergebnis wie zuvor. Verhalten unveraendert.
*/
interface NtlmResponse {
statusCode: number;
body?: Buffer | string;
}
function ntlmPost(opts: NtlmOptions): Promise<{ statusCode: number; body: string }> {
return new Promise((resolve, reject) => {
(httpntlm as any).post(opts, (err: Error | null, res: any) => {
httpntlm.post(opts, (err, res) => {
if (err) return reject(err);
resolve({ statusCode: res.statusCode, body: res.body?.toString('utf-8') ?? '' });
resolve({
statusCode: res.statusCode,
body: typeof res.body === 'string' ? res.body : (res.body?.toString('utf-8') ?? ''),
});
});
});
}
+32 -7
View File
@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import type { Readable } from 'node:stream';
import { ImapFlow, MessageStructureObject } from 'imapflow';
import type { InboxAttachment, InboxConfig, InboxEmail, InboxMessage } from './inbox-provider.interface';
import type { InboxProvider } from './inbox-provider.interface';
@@ -14,9 +15,7 @@ const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25 MB
* Converts a Node.js Readable stream into a Buffer.
* Accumulates chunks up to MAX_ATTACHMENT_BYTES; throws if limit exceeded.
*/
async function streamToBuffer(
stream: NodeJS.ReadableStream,
): Promise<Buffer> {
async function streamToBuffer(stream: Readable): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
let total = 0;
@@ -24,8 +23,12 @@ async function streamToBuffer(
stream.on('data', (chunk: Buffer) => {
total += chunk.length;
if (total > MAX_ATTACHMENT_BYTES) {
// Destroy the stream to prevent further data emission
(stream as any).destroy?.();
// Destroy the stream to prevent further data emission.
// Readable statt NodeJS.ReadableStream: alle drei Aufrufer reichen
// client.download().content herein, und imapflow deklariert das als
// Readable (imap-flow.d.ts:521). Readable traegt destroy(), also
// braucht der Aufruf keine Zusicherung mehr.
stream.destroy?.();
reject(
new Error(
`Attachment exceeds maximum allowed size of ${MAX_ATTACHMENT_BYTES} bytes (T-07-05)`,
@@ -59,10 +62,23 @@ function collectPdfParts(
const type = node.type?.toLowerCase() ?? '';
// Some mail clients (e.g. Outlook) send PDFs as application/octet-stream.
// Fall back to checking the filename from Content-Disposition or Content-Type parameters.
// BLEIBT als any, mit Befund (260921-m34, Aufgabe 3c, D-01/D-03):
// imapflow deklariert `disposition` als ZEICHENKETTE (imap-flow.d.ts:448,
// also "attachment"/"inline"), und die zugehoerigen Parameter liegen in
// einem eigenen Feld `dispositionParameters` (:450). Der Ausdruck unten
// liest `.parameters` von einer Zeichenkette und ist damit zur Laufzeit
// IMMER undefined — dispositionFilename ist stets ''. Das ist ein Befund
// im Bestandscode, kein Typproblem: ihn hier auf `dispositionParameters`
// umzubiegen waere eine Verhaltensaenderung (Outlook-Anhaenge als
// application/octet-stream wuerden ab dann erstmals erkannt), und die ist
// in dieser Aufgabe verboten. Gemeldet im SUMMARY, Entscheidung beim
// Menschen. Die Zusicherung bleibt sichtbar stehen, damit der Befund
// nicht verschwindet.
const dispositionFilename =
((node as any).disposition?.parameters?.filename as string | undefined)?.toLowerCase() ?? '';
const typeFilename =
((node as any).parameters?.name as string | undefined)?.toLowerCase() ?? '';
// Hier dagegen war die Zusicherung schlicht ueberfluessig: imapflow
// deklariert `parameters?: { [key: string]: string }` (imap-flow.d.ts:438).
const typeFilename = node.parameters?.name?.toLowerCase() ?? '';
const looksLikePdf =
type === 'application/pdf' ||
(type === 'application/octet-stream' &&
@@ -374,6 +390,15 @@ export class ImapProvider implements InboxProvider {
: undefined,
// T-07-03: suppress imapflow verbose logs — they include auth credentials
logger: false,
// BLEIBT als Zusicherung, mit Befund (260921-m34, Aufgabe 3c, D-03):
// `requireTLS` oben kommt in imapflow 1.4.3 NIRGENDS vor — weder in
// ImapFlowOptions (lib/imap-flow.d.ts) noch im Laufzeitcode
// (lib/imap-flow.js), beides durchsucht. Die Option wird also still
// verworfen; STARTTLS wird nicht durch sie erzwungen. Genau diese
// Zusicherung hat das bisher verdeckt. Sie bleibt trotzdem stehen:
// die Option zu entfernen waere eine stille Reparatur einer falschen
// Annahme (verboten), und der `any`-Befund haelt die Stelle in der
// Zaehlung sichtbar, bis ein Mensch entscheidet. Gemeldet im SUMMARY.
} as any);
}
}
+10 -2
View File
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import type SMTPTransport from 'nodemailer/lib/smtp-transport';
import { SettingsService } from '../settings/settings.service';
/**
@@ -69,7 +70,14 @@ export type BugReportMail = Pick<OutgoingMail, 'subject' | 'text' | 'attachments
interface ResolvedTransport {
source: 'tenant' | 'env';
options: nodemailer.TransportOptions & Record<string, unknown>;
/**
* SMTPTransport.Options statt TransportOptions & Record<string, unknown>:
* beide Zweige von resolveTransport() bauen reine SMTP-Optionen (host,
* port, secure, requireTLS, auth) — und genau das nimmt createTransport()
* ohne Zusicherung entgegen. Die bisherige Kombination war zu weit und
* brauchte deshalb ein `as any` an der Uebergabe.
*/
options: SMTPTransport.Options;
from: string;
}
@@ -162,7 +170,7 @@ export class MailService {
let transport: nodemailer.Transporter | null = null;
try {
const resolved = await this.resolveTransport(tenantId);
transport = nodemailer.createTransport(resolved.options as any);
transport = nodemailer.createTransport(resolved.options);
await transport.sendMail({
from: resolved.from,
to: mail.to,
@@ -83,6 +83,14 @@ export class TenderDigestScheduler implements OnModuleInit {
// type signature. At runtime the object IS a full CronJob —
// SchedulerRegistry only calls stop() on it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// URTEIL: BLEIBT (260921-m34, Aufgabe 3, D-01). Gemessen: ohne die
// Zusicherung meldet tsc, dass das lokale `job` nur die Form
// `{ start(): void }` hat, waehrend addCronJob() einen vollstaendigen
// CronJob verlangt. Ursache ist der require()-Umweg aus 07-04 (pnpm-
// Isolation, `cron` ist nur eine mittelbare Abhaengigkeit). Das
// aufzuloesen hiesse, die Beschaffung der Klasse zu aendern — eine
// Verhaltensaenderung — oder `cron` direkt aufzunehmen — eine neue
// Abhaengigkeit. Beides ist hier verboten (D-03/D-04).
this.schedulerRegistry.addCronJob(this.JOB_NAME, job as any);
job.start();
@@ -132,6 +132,14 @@ export class TenderSchedulerService implements OnApplicationBootstrap {
// Cast required: our minimal CronJob type doesn't match cron's full type signature.
// At runtime the object IS a full CronJob — SchedulerRegistry only calls stop() on it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// URTEIL: BLEIBT (260921-m34, Aufgabe 3, D-01). Gemessen: ohne die
// Zusicherung meldet tsc, dass das lokale `job` nur die Form
// `{ start(): void }` hat, waehrend addCronJob() einen vollstaendigen
// CronJob verlangt. Ursache ist der require()-Umweg aus 07-04 (pnpm-
// Isolation, `cron` ist nur eine mittelbare Abhaengigkeit). Das
// aufzuloesen hiesse, die Beschaffung der Klasse zu aendern — eine
// Verhaltensaenderung — oder `cron` direkt aufzunehmen — eine neue
// Abhaengigkeit. Beides ist hier verboten (D-03/D-04).
this.schedulerRegistry.addCronJob(this.JOB_NAME, job as any);
job.start();