refactor(quick-260921-m34): Aufgabe 2c - Hochladewege getypt, 25 Zusicherungen fallen mit

@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
This commit is contained in:
2026-09-21 17:11:28 +02:00
parent 7c9d7c1223
commit 52668c2c88
2 changed files with 48 additions and 40 deletions
@@ -9,6 +9,7 @@ import {
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { UseModule } from '../module-registry/module.guard';
import type { UploadedFileLike } from '../auth/types/auth-user';
import { CertManagerService } from './cert-manager.service';
/**
@@ -39,7 +40,7 @@ export class CertManagerController {
}),
)
async parseCert(
@UploadedFile() file: any,
@UploadedFile() file: UploadedFileLike | undefined,
@Body('password') password?: string,
@Body('pemText') pemText?: string,
) {
@@ -60,7 +61,7 @@ export class CertManagerController {
}),
)
async splitCerts(
@UploadedFile() file: any,
@UploadedFile() file: UploadedFileLike | undefined,
@Body('password') password?: string,
) {
if (!file) {
@@ -81,7 +82,7 @@ export class CertManagerController {
}),
)
async mergeCerts(
@UploadedFiles() files: any[],
@UploadedFiles() files: UploadedFileLike[],
@Body('outputFormat') outputFormat: string,
@Body('password') password?: string,
) {
@@ -107,7 +108,7 @@ export class CertManagerController {
}),
)
async convertCert(
@UploadedFile() file: any,
@UploadedFile() file: UploadedFileLike | undefined,
@Body('targetFormat') targetFormat: string,
@Body('password') password?: string,
@Body('pemText') pemText?: string,
@@ -1,5 +1,15 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import * as forge from 'node-forge';
import type { UploadedFileLike } from '../auth/types/auth-user';
/**
* Eine hochgeladene Zertifikatsdatei, so weit dieser Dienst sie liest:
* Inhalt und eingereichter Name (der Name geht ausschliesslich in
* `detectFormat` und in Fehlermeldungen). Abgeleitet aus `UploadedFileLike`
* statt daneben erfunden (quick-260921-m34); `mimetype` und `size` bleiben
* bewusst draussen, weil kein Zweig dieses Dienstes sie liest.
*/
type CertFileLike = Pick<UploadedFileLike, 'buffer' | 'originalname'>;
// ---------------------------------------------------------------------------
// CertDetails — the structured result returned by parseCert
@@ -162,7 +172,7 @@ export class CertManagerService {
* - Wrong PFX password → generic 400 message (password value never logged or echoed)
*/
async parseCert(input: {
file?: any;
file?: CertFileLike;
pemText?: string;
password?: string;
}): Promise<CertDetails> {
@@ -179,11 +189,11 @@ export class CertManagerService {
}
cert = certs[0];
} else if (file) {
const format = this.detectFormat(file.originalname as string, file.buffer as Buffer);
const format = this.detectFormat(file.originalname, file.buffer);
if (format === 'pem') {
// ── PEM file ───────────────────────────────────────────────────────
const pemStr = (file.buffer as Buffer).toString('utf-8');
const pemStr = file.buffer.toString('utf-8');
const certs = this.parsePemChain(pemStr);
if (certs.length === 0) {
throw new Error('No certificate block found in PEM file');
@@ -192,12 +202,12 @@ export class CertManagerService {
} else if (format === 'der') {
// ── DER binary file ────────────────────────────────────────────────
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
cert = forge.pki.certificateFromAsn1(asn1);
} else if (format === 'pfx') {
// ── PFX/PKCS12 file ───────────────────────────────────────────────
// wrong password → forge throws → caught below → BadRequestException (T-09-02)
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
const bags = certBags[forge.pki.oids.certBag] ?? [];
@@ -217,15 +227,15 @@ export class CertManagerService {
cert = parsedCert;
} else {
// ── P7B/PKCS7 file — PEM-wrapped or binary DER (Pitfall 4) ────────
const isPemP7b = (file.buffer as Buffer)
const isPemP7b = file.buffer
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8'));
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
const p7Certs: forge.pki.Certificate[] = p7.certificates ?? [];
@@ -332,7 +342,7 @@ export class CertManagerService {
* - File size limit 5 MB enforced by FileInterceptor in the controller
*/
async splitCerts(input: {
file?: any;
file?: CertFileLike;
password?: string;
}): Promise<SplitResponse> {
const { file } = input;
@@ -344,28 +354,28 @@ export class CertManagerService {
let certs: forge.pki.Certificate[];
try {
const format = this.detectFormat(file.originalname as string, file.buffer as Buffer);
const format = this.detectFormat(file.originalname, file.buffer);
if (format === 'pem') {
// ── PEM chain (fullchain.pem, .crt — both map to 'pem' in detectFormat) ─
const pemStr = (file.buffer as Buffer).toString('utf-8');
const pemStr = file.buffer.toString('utf-8');
certs = this.parsePemChain(pemStr);
if (certs.length === 0) {
throw new Error('No certificate blocks found in PEM file');
}
} else if (format === 'p7b') {
// ── P7B/PKCS7 bundle — PEM-wrapped or binary DER (Pitfall 4) ─────────
const isPemP7b = (file.buffer as Buffer)
const isPemP7b = file.buffer
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
if (isPemP7b) {
// PEM-wrapped PKCS7 (e.g. -----BEGIN PKCS7-----)
p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8'));
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
// Binary DER PKCS7
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
certs = (p7.certificates as forge.pki.Certificate[]) ?? [];
@@ -434,7 +444,7 @@ export class CertManagerService {
* No fallback to lower-level certBag construction was needed.
*/
async mergeCerts(input: {
files?: any[];
files?: CertFileLike[];
outputFormat: string;
password?: string;
}): Promise<FileResponse> {
@@ -453,26 +463,23 @@ export class CertManagerService {
let certs: forge.pki.Certificate[];
try {
certs = (files as any[]).flatMap((file: any) => {
const format = this.detectFormat(
file.originalname as string,
file.buffer as Buffer,
);
certs = files.flatMap((file) => {
const format = this.detectFormat(file.originalname, file.buffer);
if (format === 'pem') {
const pemStr = (file.buffer as Buffer).toString('utf-8');
const pemStr = file.buffer.toString('utf-8');
const parsed = this.parsePemChain(pemStr);
if (parsed.length === 0) {
throw new Error(`No certificate block found in ${file.originalname as string}`);
throw new Error(`No certificate block found in ${file.originalname}`);
}
return parsed;
} else if (format === 'der') {
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
return [forge.pki.certificateFromAsn1(asn1)];
} else if (format === 'pfx') {
// Extract all certs from the PFX bag
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
const bags = certBags[forge.pki.oids.certBag] ?? [];
@@ -488,21 +495,21 @@ export class CertManagerService {
const missingCertIndex = bagCerts.findIndex((c) => c === undefined || c === null);
if (missingCertIndex !== -1) {
throw new BadRequestException(
`Certificate bag in "${file.originalname as string}" does not contain a readable X.509 certificate`,
`Certificate bag in "${file.originalname}" does not contain a readable X.509 certificate`,
);
}
return bagCerts.filter((c): c is forge.pki.Certificate => c !== undefined && c !== null);
} else {
// P7B/PKCS7 — PEM-wrapped or binary DER (Pitfall 4)
const isPemP7b = (file.buffer as Buffer)
const isPemP7b = file.buffer
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8'));
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
return (p7.certificates as forge.pki.Certificate[]) ?? [];
@@ -571,7 +578,7 @@ export class CertManagerService {
* - Password is never passed to the logger (T-09-02)
*/
async convertCert(input: {
file?: any;
file?: CertFileLike;
pemText?: string;
targetFormat: string;
password?: string;
@@ -602,10 +609,10 @@ export class CertManagerService {
}
cert = certs[0];
} else if (file) {
const format = this.detectFormat(file.originalname as string, file.buffer as Buffer);
const format = this.detectFormat(file.originalname, file.buffer);
if (format === 'pem') {
const pemStr = (file.buffer as Buffer).toString('utf-8');
const pemStr = file.buffer.toString('utf-8');
const certs = this.parsePemChain(pemStr);
if (certs.length === 0) {
throw new Error('No certificate block found in PEM file');
@@ -613,11 +620,11 @@ export class CertManagerService {
cert = certs[0];
} else if (format === 'der') {
// CRITICAL: binary encoding, never utf-8 (RESEARCH Pitfall 1)
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
cert = forge.pki.certificateFromAsn1(asn1);
} else if (format === 'pfx') {
// PFX/PKCS12 — extract first cert bag (wrong password → BadRequestException)
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p12Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password ?? '');
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
const bags = certBags[forge.pki.oids.certBag] ?? [];
@@ -637,15 +644,15 @@ export class CertManagerService {
cert = parsedCert;
} else {
// P7B — extract first cert
const isPemP7b = (file.buffer as Buffer)
const isPemP7b = file.buffer
.slice(0, 27)
.toString('ascii')
.includes('-----BEGIN');
let p7: any;
if (isPemP7b) {
p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8'));
p7 = forge.pkcs7.messageFromPem(file.buffer.toString('utf-8'));
} else {
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer));
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
}
const p7Certs: forge.pki.Certificate[] = p7.certificates ?? [];