diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 1a431ce..79a3a59 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -297,11 +297,25 @@ Decimal phases appear between their surrounding integers in numeric order. **Plans**: 6 plans Plans: +**Wave 1** + - [ ] 09-01-PLAN.md — API foundation: install node-forge + Vitest runner, scaffold module, seed registry (CERT-06), shared node-forge helpers - [ ] 09-02-PLAN.md — Frontend shell: tab page, DropZone, conditional password field, download helpers, certManager i18n (de/en) + +**Wave 2** *(blocked on Wave 1 completion)* + - [ ] 09-03-PLAN.md — Inspect slice: parseCert (PEM/DER/PFX/P7B) + POST /parse + Inspect tab (CERT-01, CERT-05 read) + +**Wave 3** *(blocked on Wave 2 completion)* + - [ ] 09-04-PLAN.md — Split slice: splitCerts (fullchain/P7B) + POST /split + Split tab download list (CERT-02) + +**Wave 4** *(blocked on Wave 3 completion)* + - [ ] 09-05-PLAN.md — Convert slice: convertCert (PEM/DER/P7B round-trips) + POST /convert + Convert tab (CERT-04) + +**Wave 5** *(blocked on Wave 4 completion)* + - [ ] 09-06-PLAN.md — Merge/PFX slice: mergeCerts (PEM chain + password PFX) + POST /merge + Merge tab + PFX convert option (CERT-03, CERT-05 write) **UI hint**: yes diff --git a/.planning/STATE.md b/.planning/STATE.md index 256fdc8..61fc86f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ milestone_name: milestone current_phase: 08 current_phase_name: dashboard-widgets-vollimplementierung status: executing -stopped_at: context exhaustion at 75% (2026-07-01) -last_updated: "2026-07-01T09:34:09.492Z" +stopped_at: context exhaustion at 76% (2026-07-01) +last_updated: "2026-07-01T14:28:23.420Z" last_activity: 2026-07-01 -last_activity_desc: Completed quick task 260701-abc: Fix i18n missing keys (marketplace.accessDenied + calendar form) +last_activity_desc: Phase 08 execution resumed (wave continue) progress: total_phases: 8 completed_phases: 7 @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-06-18) Phase: 08 (dashboard-widgets-vollimplementierung) — EXECUTING Plan: 1 of 4 -Status: Executing Phase 08 +Status: Ready to execute Last activity: 2026-07-01 — Phase 08 execution resumed (wave continue) Progress: [█████████░] 93% @@ -148,6 +148,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-01T09:34:09.479Z -Stopped at: context exhaustion at 75% (2026-07-01) +Last session: 2026-07-01T11:37:27.848Z +Stopped at: context exhaustion at 76% (2026-07-01) Resume file: .planning/phases/08-dashboard-widgets-vollimplementierung/08-CONTEXT.md diff --git a/.planning/phases/09-cert-manager-module/09-06-PLAN.md b/.planning/phases/09-cert-manager-module/09-06-PLAN.md index 6859dec..306781e 100644 --- a/.planning/phases/09-cert-manager-module/09-06-PLAN.md +++ b/.planning/phases/09-cert-manager-module/09-06-PLAN.md @@ -14,7 +14,7 @@ files_modified: - apps/web/src/app/(portal)/modules/cert-manager/page.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx autonomous: true -requirements: [CERT-03, CERT-05] +requirements: [CERT-03, CERT-04, CERT-05] must_haves: truths: diff --git a/.planning/phases/09-cert-manager-module/09-PATTERNS.md b/.planning/phases/09-cert-manager-module/09-PATTERNS.md new file mode 100644 index 0000000..882a477 --- /dev/null +++ b/.planning/phases/09-cert-manager-module/09-PATTERNS.md @@ -0,0 +1,559 @@ +# Phase 9: Cert Manager Module - Pattern Map + +**Mapped:** 2026-07-01 +**Files analyzed:** 11 new/modified files +**Analogs found:** 10 / 11 + +--- + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `apps/api/src/cert-manager/cert-manager.module.ts` | module | request-response | `apps/api/src/domaincheck/domaincheck.module.ts` | exact | +| `apps/api/src/cert-manager/cert-manager.seed.ts` | utility | — | `apps/api/src/domaincheck/domaincheck.seed.ts` | exact | +| `apps/api/src/cert-manager/cert-manager.controller.ts` | controller | file-I/O | `apps/api/src/dkv/dkv.controller.ts` | role-match | +| `apps/api/src/cert-manager/cert-manager.service.ts` | service | transform | `apps/api/src/domaincheck/domaincheck.service.ts` | role-match | +| `apps/api/src/cert-manager/dto/parse-cert.dto.ts` | dto | — | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | role-match | +| `apps/api/src/app.module.ts` (modify) | config | — | current file | exact | +| `apps/web/src/app/(portal)/modules/cert-manager/page.tsx` | component | request-response | `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` | exact | +| `apps/web/src/app/(portal)/modules/cert-manager/actions.ts` | utility | request-response | `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` | exact | +| `apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx` | component | file-I/O | `apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx` | role-match | +| `apps/web/src/messages/de.json` (modify) | config | — | existing `domaincheck` namespace | exact | +| `apps/web/src/messages/en.json` (modify) | config | — | existing `domaincheck` namespace | exact | + +--- + +## Pattern Assignments + +### `apps/api/src/cert-manager/cert-manager.module.ts` (module, OnModuleInit) + +**Analog:** `apps/api/src/domaincheck/domaincheck.module.ts` + +**Full pattern** (lines 1-38): +```typescript +import { Logger, Module, OnModuleInit } from '@nestjs/common'; +import { ModuleRegistryModule } from '../module-registry/module-registry.module'; +import { ModuleRegistryService } from '../module-registry/module-registry.service'; +import { CertManagerController } from './cert-manager.controller'; +import { seedCertManagerModule } from './cert-manager.seed'; +import { CertManagerService } from './cert-manager.service'; + +@Module({ + imports: [ModuleRegistryModule], + controllers: [CertManagerController], + providers: [CertManagerService], +}) +export class CertManagerModule implements OnModuleInit { + private readonly logger = new Logger(CertManagerModule.name); + + constructor( + private readonly moduleRegistryService: ModuleRegistryService, + ) {} + + async onModuleInit(): Promise { + try { + await seedCertManagerModule(this.moduleRegistryService); + this.logger.log('Cert-Manager module seeded in registry'); + } catch (error) { + this.logger.error('Failed to seed cert-manager module', error); + } + } +} +``` + +--- + +### `apps/api/src/cert-manager/cert-manager.seed.ts` (utility, seed) + +**Analog:** `apps/api/src/domaincheck/domaincheck.seed.ts` + +**Full pattern** (lines 1-25): +```typescript +import { ModuleRegistryService } from '../module-registry/module-registry.service'; + +export async function seedCertManagerModule( + moduleRegistryService: ModuleRegistryService, +): Promise { + await moduleRegistryService.seedModule({ + slug: 'cert-manager', + name: 'Cert Manager', + version: '1.0.0', + category: 'security-tools', + description: { + de: 'Zertifikate analysieren, konvertieren und verwalten', + en: 'Inspect, convert and manage certificates', + }, + isSystem: true, + }); +} +``` + +**Critical note:** `isSystem: true` registers the module in the `Module` table but does NOT auto-activate it per tenant. The `TenantModuleActivation` record must be created manually via the marketplace UI before any API endpoint responds (otherwise `ModuleGuard` returns 403). + +--- + +### `apps/api/src/cert-manager/cert-manager.controller.ts` (controller, file-I/O) + +**Analog:** `apps/api/src/dkv/dkv.controller.ts` + +**Imports pattern** (lines 1-17 of dkv.controller.ts): +```typescript +import { + BadRequestException, + Body, + Controller, + Post, + Req, + UploadedFile, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; +import { UseModule } from '../module-registry/module.guard'; +import { CertManagerService } from './cert-manager.service'; +``` + +**Module guard pattern** (from `apps/api/src/domaincheck/domaincheck.controller.ts` lines 6-8): +```typescript +@Controller('modules/cert-manager') +@UseModule('cert-manager') +export class CertManagerController { + constructor(private readonly certManagerService: CertManagerService) {} +``` + +**Single file upload pattern** (from `apps/api/src/dkv/dkv.controller.ts` lines 210-229): +```typescript +@Post('parse') +@UseInterceptors(FileInterceptor('file', { + limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB +})) +async parseCert( + @Req() req: any, + @UploadedFile() file: any, + @Body('password') password?: string, + @Body('pemText') pemText?: string, +) { + if (!file && !pemText) { + throw new BadRequestException('No file or PEM text provided'); + } + return this.certManagerService.parseCert({ file, pemText, password }); +} +``` + +**Multi-file upload pattern for merge** (FilesInterceptor — plural): +```typescript +@Post('merge') +@UseInterceptors(FilesInterceptor('files', 20, { + limits: { fileSize: 5 * 1024 * 1024 }, +})) +async mergeCerts( + @UploadedFiles() files: any[], + @Body('outputFormat') outputFormat: string, + @Body('password') password?: string, +) { + if (!files || files.length < 2) { + throw new BadRequestException('At least 2 files required for merge'); + } + return this.certManagerService.mergeCerts({ files, outputFormat, password }); +} +``` + +**Tenant extraction helper** (from `apps/api/src/dkv/dkv.controller.ts` lines 233-240): +```typescript +private _requireTenant(req: any): string { + const tenantId = req.tenantId as string | undefined; + if (!tenantId) { + throw new BadRequestException('No tenant context'); + } + return tenantId; +} +``` + +**Binary download pattern** (from `apps/api/src/dkv/dkv.controller.ts` lines 143-160) — NOTE: cert-manager uses JSON base64 response instead of `res.send(buffer)` because endpoints are POST-only and cannot use GET anchors for authenticated downloads: +```typescript +// cert-manager variant: return base64 JSON (not res.send) +return { + filename: 'certificate.pem', + content: Buffer.from(pemString, 'utf-8').toString('base64'), + mimeType: 'application/x-pem-file', +}; +``` + +--- + +### `apps/api/src/cert-manager/cert-manager.service.ts` (service, transform) + +**Analog:** `apps/api/src/domaincheck/domaincheck.service.ts` + +**Service structure pattern** (lines 1-16 of domaincheck.service.ts): +```typescript +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import * as forge from 'node-forge'; + +@Injectable() +export class CertManagerService { + private readonly logger = new Logger(CertManagerService.name); + + async parseCert(input: { file?: any; pemText?: string; password?: string }) { + try { + // ... node-forge operations + } catch (error) { + this.logger.warn(`parseCert failed: ${error}`); + throw new BadRequestException( + 'Invalid certificate. Check the file format or password.', + ); + } + } +} +``` + +**Error handling pattern:** All node-forge operations throw synchronously on malformed input. Wrap every service method in `try/catch` → `throw new BadRequestException(...)`. Never log the `password` parameter. + +**Binary encoding rule:** For DER/PFX/P7B buffers, use `buffer.toString('binary')` (never `'utf-8'`) when passing to `forge.util.createBuffer()`. + +--- + +### `apps/api/src/cert-manager/dto/parse-cert.dto.ts` (dto) + +**Analog:** `apps/api/src/domaincheck/dto/check-domain.dto.ts` + +```typescript +// parse-cert.dto.ts — for JSON body (text paste path only) +export class ParseCertDto { + pemText!: string; + password?: string; +} + +// merge-certs.dto.ts — body fields alongside FilesInterceptor +export class MergeCertsDto { + outputFormat!: 'pem' | 'pfx'; + password?: string; +} + +// convert-cert.dto.ts — body fields alongside FileInterceptor +export class ConvertCertDto { + targetFormat!: 'pem' | 'der' | 'pfx' | 'p7b'; + password?: string; +} +``` + +--- + +### `apps/api/src/app.module.ts` (modify — add CertManagerModule) + +**Analog:** Current file, lines 14-41 + +**Pattern:** Add import + add to imports array, following domaincheck: +```typescript +// Add to imports at top: +import { CertManagerModule } from './cert-manager/cert-manager.module'; + +// Add to @Module({ imports: [...] }) array (after DomaincheckModule): +CertManagerModule, +``` + +--- + +### `apps/web/src/app/(portal)/modules/cert-manager/page.tsx` (component, request-response) + +**Analog:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` + +**Header + layout pattern** (lines 1-64 of domaincheck/page.tsx): +```typescript +'use client'; + +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; + +export default function CertManagerPage() { + const t = useTranslations('certManager'); + const [activeTab, setActiveTab] = useState<'inspect' | 'split' | 'merge' | 'convert'>('inspect'); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + return ( +
+ {/* Header */} +
+

{t('title')}

+

{t('description')}

+
+ + {/* Shared Input Card */} +
+ {/* DropZone + OR divider + Textarea + conditional Password field */} +
+ + {/* Tab Navigation */} +
+ {(['inspect', 'split', 'merge', 'convert'] as const).map((tab) => ( + + ))} +
+ + {/* Tab Content Card */} +
+ {error &&

{error}

} + {/* Tab-specific content */} +
+
+ ); +} +``` + +**Layout differences from domaincheck:** Use `max-w-4xl` (not `max-w-2xl`) per UI-SPEC. Tab navigation is manually rendered (no shadcn Tabs component — UI-SPEC confirms no shadcn). + +--- + +### `apps/web/src/app/(portal)/modules/cert-manager/actions.ts` (utility, request-response) + +**Analog:** `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` + +**Full pattern** (lines 1-33 of domaincheck/actions.ts): +```typescript +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + +// JSON body endpoint (PEM text paste) +export async function parseCertAction(pemText: string, password?: string) { + const response = await fetch(`${API_URL}/modules/cert-manager/parse`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pemText, password }), + credentials: 'include', + }); + if (!response.ok) { + const errorBody = await response.text().catch(() => ''); + throw new Error(`${response.status} ${errorBody}`.trim()); + } + return response.json(); +} + +// Multipart endpoint (file upload) +export async function parseCertFileAction(file: File, password?: string) { + const form = new FormData(); + form.append('file', file); + if (password) form.append('password', password); + + const response = await fetch(`${API_URL}/modules/cert-manager/parse`, { + method: 'POST', + // Do NOT set Content-Type — fetch sets multipart/form-data + boundary + body: form, + credentials: 'include', + }); + if (!response.ok) { + const errorBody = await response.text().catch(() => ''); + throw new Error(`${response.status} ${errorBody}`.trim()); + } + return response.json(); +} + +// Multi-file upload (merge) +export async function mergeCertsAction(files: File[], outputFormat: string, password?: string) { + const form = new FormData(); + files.forEach(file => form.append('files', file)); // same field name, multiple values + form.append('outputFormat', outputFormat); + if (password) form.append('password', password); + + const response = await fetch(`${API_URL}/modules/cert-manager/merge`, { + method: 'POST', + body: form, + credentials: 'include', + }); + if (!response.ok) { + const errorBody = await response.text().catch(() => ''); + throw new Error(`${response.status} ${errorBody}`.trim()); + } + return response.json(); // { filename, content (base64), mimeType } +} +``` + +**Base64 download helper** (no analog exists — new pattern): +```typescript +export function downloadBase64(filename: string, content: string, mimeType: string) { + const bytes = Uint8Array.from(atob(content), c => c.charCodeAt(0)); + const blob = new Blob([bytes], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} +``` + +--- + +### `apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx` (component, file-I/O) + +**Analog:** `apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx` + +**Hidden file input + click-to-browse pattern** (lines 92-109 of CsvImportButton.tsx): +```typescript +'use client'; +import { useRef, useState } from 'react'; + +export function DropZone({ onFile, accept }: { onFile: (file: File) => void; accept: string }) { + const fileInputRef = useRef(null); + const [isDragOver, setIsDragOver] = useState(false); + const [currentFile, setCurrentFile] = useState(null); + + return ( +
{ e.preventDefault(); setIsDragOver(true); }} + onDragLeave={() => setIsDragOver(false)} + onDrop={(e) => { + e.preventDefault(); + setIsDragOver(false); + const file = e.dataTransfer.files[0]; + if (file) { setCurrentFile(file); onFile(file); } + }} + onClick={() => fileInputRef.current?.click()} + className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${ + isDragOver ? 'border-primary bg-primary/5' : 'border-border' + }`} + > + { + const file = e.target.files?.[0]; + if (file) { setCurrentFile(file); onFile(file); } + e.target.value = ''; // allow re-selecting same file + }} + /> + {currentFile ? ( +

{currentFile.name}

+ ) : ( +

Datei hierher ziehen oder klicken

+ )} +
+ ); +} +``` + +**Error display pattern** (from CsvImportButton.tsx lines 178-180): +```typescript +{importError && ( +

{importError}

+)} +``` + +**Loading button pattern** (from CsvImportButton.tsx lines 192-208): +```typescript + +``` + +--- + +### `apps/web/src/messages/de.json` and `en.json` (modify) + +**Analog:** Existing `domaincheck` namespace at line 337 in de.json + +**Pattern:** Add `certManager` namespace at the same level as `domaincheck`. The full namespace content is specified in `09-RESEARCH.md` under "i18n Namespace Structure". No structural changes — append only. + +--- + +## Shared Patterns + +### Module Guard (`@UseModule`) +**Source:** `apps/api/src/module-registry/module.guard.ts` lines 75-80 +**Apply to:** `cert-manager.controller.ts` — decorate the controller class (not individual handlers) +```typescript +import { UseModule } from '../module-registry/module.guard'; + +@Controller('modules/cert-manager') +@UseModule('cert-manager') // applies ModuleGuard to ALL routes in this controller +export class CertManagerController { ... } +``` + +### Error Handling (NestJS) +**Source:** `apps/api/src/dkv/dkv.controller.ts` lines 152-158 + domaincheck.service.ts lines 80-88 +**Apply to:** `cert-manager.controller.ts` and `cert-manager.service.ts` +```typescript +// Controller: re-throw known exceptions, let unknown bubble +if (error instanceof NotFoundException || error instanceof BadRequestException) { + throw error; +} +throw error; + +// Service: wrap node-forge ops in try/catch → BadRequestException +try { + const cert = forge.pki.certificateFromPem(pemString); +} catch (_err) { + throw new BadRequestException('Invalid certificate format'); +} +``` + +### Client State + Loading Pattern +**Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` lines 20-36 +**Apply to:** `cert-manager/page.tsx` +```typescript +const [isLoading, setIsLoading] = useState(false); +const [error, setError] = useState(null); + +const handleAction = async () => { + setIsLoading(true); + setError(null); + try { + const result = await someAction(); + setResult(result); + } catch (err) { + setError(err instanceof Error ? err.message : t('certManager.error.generic')); + } finally { + setIsLoading(false); + } +}; +``` + +### i18n String Pattern +**Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` line 2, 20 +**Apply to:** All frontend files in `cert-manager/` +```typescript +import { useTranslations } from 'next-intl'; +const t = useTranslations('certManager'); +// Usage: t('title'), t('tabs.inspect'), t('actions.processing') +``` + +### Fetch with Auth (Multipart) +**Source:** `apps/web/src/app/(portal)/modules/domaincheck/actions.ts` lines 18-23 +**Apply to:** All fetch calls in `cert-manager/actions.ts` +- JSON body: set `Content-Type: application/json` +- Multipart (FormData): do NOT set `Content-Type` — let fetch set boundary automatically +- Always include `credentials: 'include'` for cookie auth + +--- + +## No Analog Found + +| File | Role | Data Flow | Reason | +|------|------|-----------|--------| +| `apps/api/src/cert-manager/cert-manager.service.ts` (node-forge internals) | service | transform | No crypto/binary-processing service exists in codebase — node-forge API patterns must follow RESEARCH.md Pattern 5 | + +--- + +## Metadata + +**Analog search scope:** `apps/api/src/domaincheck/`, `apps/api/src/dkv/`, `apps/api/src/module-registry/`, `apps/web/src/app/(portal)/modules/domaincheck/`, `apps/web/src/app/(portal)/modules/dkv-fleet/` +**Files scanned:** 10 +**Pattern extraction date:** 2026-07-01 diff --git a/.planning/phases/09-cert-manager-module/09-RESEARCH.md b/.planning/phases/09-cert-manager-module/09-RESEARCH.md index fcdec58..aea00ba 100644 --- a/.planning/phases/09-cert-manager-module/09-RESEARCH.md +++ b/.planning/phases/09-cert-manager-module/09-RESEARCH.md @@ -599,17 +599,17 @@ interface FileResponse { --- -## Open Questions +## Open Questions (RESOLVED) 1. **PFX cert-only creation (A2)** - What we know: node-forge `toPkcs12Asn1(key, certs, password)` is documented - What's unclear: whether `null` for key is accepted without throwing - - Recommendation: Implement and test early in Wave 0; if null throws, use `forge.pkcs12` lower-level API to create cert-only PKCS12 bag + - **RESOLVED:** Plan 06 implements: attempt `toPkcs12Asn1(null, certs, password)` first; if node-forge throws on null key, fall back to constructing a cert-only PKCS12 bag via lower-level `forge.pkcs12` certBag API. Resolution happens at execution time in Wave 5 Task 1 — no pre-execution blocker. 2. **Merge: does user also supply a private key file?** - What we know: CONTEXT.md says "cert + optional private key" for PFX - What's unclear: How the private key is provided (separate file? paste?) - - Recommendation: Planner should scope the merge endpoint to accept an optional private key as a third file field. If omitted, create cert-only PFX. + - **RESOLVED:** Private key support is explicitly deferred per CONTEXT.md `` section (private key handling, key generation, PKCS#8 import). Initial implementation is cert-only PFX merge. No private key file field in Wave 5 merge endpoint. This is a conscious scope decision, not an oversight. --- diff --git a/.planning/phases/09-cert-manager-module/09-VALIDATION.md b/.planning/phases/09-cert-manager-module/09-VALIDATION.md new file mode 100644 index 0000000..f67bd25 --- /dev/null +++ b/.planning/phases/09-cert-manager-module/09-VALIDATION.md @@ -0,0 +1,82 @@ +--- +phase: 9 +slug: cert-manager-module +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-07-01 +--- + +# Phase 9 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Vitest 3.x | +| **Config file** | `apps/api/vitest.config.ts` / `apps/web/vitest.config.ts` | +| **Quick run command** | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | +| **Full suite command** | `pnpm --filter api test --run && pnpm --filter web test --run` | +| **Estimated runtime** | ~30 seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `pnpm --filter api test --run apps/api/src/modules/cert-manager` +- **After every plan wave:** Run `pnpm --filter api test --run && pnpm --filter web test --run` +- **Before `/gsd-verify-work`:** Full suite must be green +- **Max feedback latency:** 30 seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 09-01-01 | 01 | 0 | CERT-01 | — | node-forge installs without native build | unit | `pnpm --filter api test --run` | ❌ W0 | ⬜ pending | +| 09-01-02 | 01 | 1 | CERT-01 | — | PEM/DER/PFX parsed → subject/issuer/validity/SANs/fingerprint returned | unit | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | +| 09-02-01 | 02 | 1 | CERT-02 | T-09-01 | Split returns correct number of certs; each is valid PEM | unit | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | +| 09-03-01 | 03 | 2 | CERT-03 | T-09-01 | Merge produces valid PEM chain; PFX password-protected | unit | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | +| 09-04-01 | 04 | 2 | CERT-04 | — | Round-trip PEM→DER→PEM produces identical cert | unit | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | +| 09-05-01 | 05 | 2 | CERT-05 | T-09-02 | Wrong PFX password returns 400, not 500 | unit | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | +| 09-06-01 | 06 | 3 | CERT-06 | — | Module appears in registry with slug cert-manager | integration | `pnpm --filter api test --run apps/api/src/modules/cert-manager` | ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `apps/api/src/modules/cert-manager/__tests__/cert-processor.service.spec.ts` — unit test stubs for CERT-01 through CERT-05 +- [ ] `apps/api/src/modules/cert-manager/__tests__/cert-manager.controller.spec.ts` — controller test stubs +- [ ] `node-forge` package installed in `apps/api` + +*Wave 0 installs node-forge and creates RED test stubs before implementation begins.* + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Module activatable via Marketplace UI | CERT-06 | Requires DB + running app + UI interaction | Navigate to Marketplace, activate cert-manager, verify /modules/cert-manager route loads | +| File download (binary formats DER/PFX) | CERT-04 | Browser Blob-URL behavior requires visual check | Upload PEM cert, convert to DER, verify download triggers correct binary file | +| Password prompt UX for PFX open | CERT-05 | Interactive UI flow | Upload password-protected PFX, verify modal appears, enter correct password, verify parse success | + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < 30s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending