--- phase: 09-cert-manager-module plan: "04" subsystem: api+frontend tags: [cert-manager, splitCerts, node-forge, pkcs7, split-tab, tdd, vitest] dependency_graph: requires: [09-01, 09-02, 09-03] provides: [cert-manager-split-endpoint, split-response-interface, split-tab-ui] affects: - apps/api/src/cert-manager/cert-manager.service.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/SplitTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx tech_stack: added: [] patterns: [tdd-red-green, node-forge-pkcs7-split, parsePemChain-reuse, downloadBase64-pattern, vi.spyOn-mock] key_files: created: [] modified: - apps/api/src/cert-manager/cert-manager.service.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/SplitTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx decisions: - "splitCerts PEM path reuses parsePemChain helper (no duplication); P7B path sniffs first bytes for PEM vs DER" - "format === 'crt' comparison removed — detectFormat only returns 'pem'|'der'|'pfx'|'p7b' (Rule 1 auto-fix, TS2367)" - "SplitTab error classification: 'format'/'invalid'/'unknown' in message -> unknownFormat, else -> generic (mirrors InspectTab)" - "splitCertsAction accepts File directly (not FormData) — caller is always browser File object; postForm wraps it" metrics: duration: "~4 minutes" completed: "2026-07-02T05:19:00Z" tasks_completed: 3 files_created: 0 files_modified: 5 requirements: [CERT-02] status: complete --- # Phase 09 Plan 04: Split Vertical Slice Summary **One-liner:** splitCerts splits fullchain PEM chains and P7B PKCS7 bundles into individually downloadable base64 PEM certs; POST /split wired; SplitTab renders per-cert download list with subject CN and expiry. ## Objective Second functional vertical slice: certificate chain/bundle splitting. Delivers CERT-02 (upload fullchain.pem or P7B, download each cert separately). Reuses parsePemChain + downloadBase64 patterns from Plan 03. ## Tasks Completed | # | Name | Type | Commit | Status | |---|------|------|--------|--------| | 1 | RED — failing splitCerts spec | test | eec6631 | done | | 2 | GREEN — implement splitCerts + wire POST /split | feat | 2c4ada3 | done | | 3 | Split tab UI + splitCertsAction + render test | feat | 33b1bc3 | done | ## What Was Built ### Task 1: RED — failing splitCerts spec Added 3 new splitCerts tests to `cert-manager.service.spec.ts`: - **Fullchain PEM test:** builds two RSA-1024 self-signed certs, concatenates PEMs, calls splitCerts, asserts count=2, each cert content is base64 of single PEM block, both CNs present, ISO notAfter format - **P7B bundle test:** builds a P7B PEM via `forge.pkcs7.createSignedData()` + `forge.pem.encode({ type:'PKCS7', body:... })`, calls splitCerts on a .p7b file, asserts at least 1 cert returned - **Malformed input test:** garbage bytes with .pem extension → expects BadRequestException All 3 tests FAIL against the NotImplementedException stub (RED confirmed). 16 prior tests remain green. ### Task 2: GREEN — splitCerts implementation **`cert-manager.service.ts`:** - Exported `SplitEntry` interface: `{ index, filename, content (base64 PEM), subject.cn, validity.notAfter }` - Exported `SplitResponse` interface: `{ count, certs: SplitEntry[] }` - Implemented `splitCerts({ file }): Promise`: - PEM path: `parsePemChain(buffer.toString('utf-8'))` → array of forge certs - P7B path: sniff first 27 bytes for `-----BEGIN` → `messageFromPem` (PEM-wrapped) or `asn1.fromDer` + `messageFromAsn1` (binary DER) - Unsupported format (pfx/der) → explicit BadRequestException - Each cert: `certificateToPem(cert)` → base64 → SplitEntry with index, filename `cert-N.pem`, subject.cn, notAfter - Outer try/catch → BadRequestException on malformed input (T-09-01) - POST /split controller route was already fully wired from Plan 01 (FileInterceptor, 5MB limit, T-09-03, T-09-04) **Result: all 19 API tests pass (16 prior + 3 new splitCerts).** ### Task 3: SplitTab UI + splitCertsAction + render tests **`actions.ts`:** - Added `SplitEntry` + `SplitResponse` interfaces (mirrors API response) - Added `splitCertsAction(file: File): Promise` — builds FormData, delegates to `postForm('split', form)` **`components/SplitTab.tsx`:** - `'use client'` component with `useState` for loading/result/error - Aufteilen button: disabled when no file or loading; label swaps to `t('actions.processing')` - Empty state when no result and no error - Per-cert `