--- phase: 09-cert-manager-module plan: "05" subsystem: api+frontend tags: [cert-manager, convertCert, node-forge, p7b, convert-tab, tdd, vitest, file-response] dependency_graph: requires: [09-01, 09-02, 09-03, 09-04] provides: [cert-manager-convert-endpoint, file-response-interface, convert-tab-ui] affects: - apps/api/src/cert-manager/cert-manager.service.ts - apps/api/src/cert-manager/cert-manager.controller.ts - apps/api/src/cert-manager/cert-manager.service.spec.ts - apps/web/src/app/(portal)/modules/cert-manager/actions.ts - apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx tech_stack: added: [] patterns: [tdd-red-green, node-forge-der-hex-base64, file-response-download, vi.spyOn-downloadBase64] key_files: created: [] modified: - apps/api/src/cert-manager/cert-manager.service.ts - apps/api/src/cert-manager/cert-manager.controller.ts - apps/api/src/cert-manager/cert-manager.service.spec.ts - apps/web/src/app/(portal)/modules/cert-manager/actions.ts - apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx decisions: - "DER output: bytesToHex → Buffer.from(hex,'hex') → base64 to avoid utf-8 corruption (Pitfall 1, T-09-06)" - "P7B output: pkcs7.createSignedData + asn1.toDer + pem.encode (PEM-wrapped PKCS7)" - "FORMAT_MIME map at module level for pem/der/p7b mimeType lookup" - "Controller convert: adds @Body('pemText') so text-paste path works; rejects when neither file nor pemText" - "ConvertTab shows empty state + format selector simultaneously (mirrors InspectTab button-always-visible pattern)" metrics: duration: "~8 minutes" completed: "2026-07-02T07:39:00Z" tasks_completed: 3 files_created: 0 files_modified: 6 requirements: [CERT-04] status: complete --- # Phase 09 Plan 05: Convert Vertical Slice Summary **One-liner:** convertCert converts PEM/DER/P7B with byte-identical round trips using bytesToHex→hex→base64 for DER safety; POST /convert wired; ConvertTab renders format selector + Konvertieren button + blob download. ## Objective Third functional vertical slice: certificate format conversion. Delivers CERT-04 (PEM/DER/P7B targets). Reuses parse helpers from Plans 03–04 and downloadBase64 pattern from Plan 04. ## Tasks Completed | # | Name | Type | Commit | Status | |---|------|------|--------|--------| | 1 | RED — failing convertCert spec | test | 37db58b | done | | 2 | GREEN — implement convertCert + wire POST /convert | feat | 5969464 | done | | 3 | Convert tab UI + convertCertAction + render test | feat | f89d656 | done | ## What Was Built ### Task 1: RED — failing convertCert spec Added 4 new convertCert tests to `cert-manager.service.spec.ts`: - **PEM→DER round-trip:** calls `convertCert({ pemText, targetFormat: 'der' })`, decodes base64 DER, re-parses via `forge.asn1.fromDer` + `forge.pki.certificateFromAsn1`, asserts CN matches original - **DER→PEM round-trip:** calls `convertCert({ file: { originalname: 'c.der', buffer }, targetFormat: 'pem' })`, decodes base64 PEM, re-parses via `forge.pki.certificateFromPem`, asserts CN matches - **PEM→P7B:** calls `convertCert({ pemText, targetFormat: 'p7b' })`, decodes base64 PKCS7 PEM, parses via `forge.pkcs7.messageFromPem`, asserts ≥1 certificate enclosed - **Malformed input:** expects BadRequestException for garbage PEM text All 4 fail against NotImplementedException stub (RED confirmed). 19 prior tests remain green. ### Task 2: GREEN — convertCert implementation **`cert-manager.service.ts`:** - Exported `FileResponse` interface: `{ filename, content (base64), mimeType }` - Added `FORMAT_MIME` map: `{ pem: 'application/x-pem-file', der: 'application/x-x509-ca-cert', p7b: 'application/x-pkcs7-certificates' }` - Implemented `convertCert({ file?, pemText?, targetFormat, password? }): Promise`: - Validates `targetFormat` against FORMAT_MIME (400 if unsupported) - Input resolution: PEM text → `parsePemChain`; DER file → `forge.asn1.fromDer(toForgeBuffer(...))`; PFX → `pkcs12FromAsn1` + getBags; P7B → sniff + `messageFromPem`/`messageFromAsn1` - PEM output: `certificateToPem(cert)` → `Buffer.from(str, 'utf-8').toString('base64')` - DER output: `forge.util.bytesToHex(toDer(certificateToAsn1(cert)).getBytes())` → `Buffer.from(hex, 'hex').toString('base64')` — avoids Pitfall 1 / T-09-06 - P7B output: `pkcs7.createSignedData()` + `addCertificate(cert)` + `asn1.toDer(toAsn1())` + `pem.encode({ type:'PKCS7' })` → base64 - All forge ops in try/catch → BadRequestException **`cert-manager.controller.ts`:** - Added `@Body('pemText') pemText?: string` to `convertCert` endpoint - Changed guard from "reject if no file" to "reject if no file AND no pemText" - Passes `pemText` through to service **Result: all 23 API tests pass (19 prior + 4 new convertCert).** ### Task 3: ConvertTab UI + convertCertAction + render tests **`actions.ts`:** - Added `FileResponse` interface (mirrors API response) - Added `convertCertAction(input, targetFormat): Promise` — builds FormData with file/pemText/password + targetFormat, delegates to `postForm('convert', form)` **`components/ConvertTab.tsx`:** - `'use client'` component with `useState` for loading/error/targetFormat - Native `