--- phase: 09-cert-manager-module plan: 01 type: execute wave: 1 depends_on: [] files_modified: - apps/api/package.json - apps/api/vitest.config.ts - apps/api/src/cert-manager/cert-manager.module.ts - apps/api/src/cert-manager/cert-manager.seed.ts - apps/api/src/cert-manager/cert-manager.service.ts - apps/api/src/cert-manager/cert-manager.controller.ts - apps/api/src/cert-manager/dto/parse-cert.dto.ts - apps/api/src/cert-manager/dto/merge-certs.dto.ts - apps/api/src/cert-manager/dto/convert-cert.dto.ts - apps/api/src/cert-manager/cert-manager.service.spec.ts - apps/api/src/app.module.ts autonomous: true requirements: [CERT-06] user_setup: - service: marketplace-activation why: "isSystem:true seeds the module in the registry but does NOT auto-activate it per tenant. ModuleGuard returns 403 until an admin activates cert-manager via the Marketplace UI." dashboard_config: - task: "Activate the cert-manager module for the tenant" location: "Tessera Portal -> Marketplace -> Cert Manager -> Aktivieren (after this plan runs and the API is restarted)" must_haves: truths: - "The API boots and seeds a Module registry row with slug 'cert-manager' on startup" - "The cert-manager Vitest suite runs via `pnpm --filter @tessera/api test`" - "Shared node-forge helpers (format detection, fingerprint, PEM-chain split, buffer conversion) exist and are unit-tested" artifacts: - "apps/api/vitest.config.ts (node environment)" - "apps/api/src/cert-manager/cert-manager.module.ts (OnModuleInit seed)" - "apps/api/src/cert-manager/cert-manager.seed.ts (seedCertManagerModule)" - "apps/api/src/cert-manager/cert-manager.service.ts (shared helpers + operation method stubs)" - "apps/api/src/cert-manager/cert-manager.controller.ts (4 POST endpoints, @UseModule guard)" - "apps/api/src/cert-manager/cert-manager.service.spec.ts (RED/GREEN helper + seed tests)" key_links: - "CertManagerModule registered in app.module.ts imports array" - "seedCertManagerModule -> moduleRegistryService.seedModule({ slug: 'cert-manager' })" - "@Controller('modules/cert-manager') + @UseModule('cert-manager') -> ModuleGuard" --- Establish the API foundation for the cert-manager module: install node-forge and a Vitest runner for `@tessera/api`, scaffold the NestJS module following the domaincheck analog exactly, seed the module into the registry (CERT-06), and implement + unit-test the shared node-forge helpers every later slice depends on. This is the first vertical slice's enabling half: after this plan the module registers itself at startup so it can be activated in the Marketplace and its endpoints become reachable. Purpose: All later feature slices (Inspect, Split, Convert, Merge/PFX) build on this module skeleton, the shared crypto helpers, and the API test harness. Output: Registered cert-manager module + running API test suite + tested shared helpers. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-cert-manager-module/09-CONTEXT.md @.planning/phases/09-cert-manager-module/09-RESEARCH.md @.planning/phases/09-cert-manager-module/09-PATTERNS.md @apps/api/src/domaincheck/domaincheck.module.ts @apps/api/src/domaincheck/domaincheck.seed.ts @apps/api/src/domaincheck/domaincheck.controller.ts @apps/api/src/app.module.ts ## Artifacts this plan produces - New file: `apps/api/vitest.config.ts` — Vitest config, `test.environment: 'node'`, `test.include: ['src/**/*.spec.ts']` - New npm scripts in `apps/api/package.json`: `test` (`vitest run`), `test:watch` (`vitest`) - New deps in `apps/api`: `node-forge@^1.4.0`, `@types/node-forge@^1.3.14` (dev), `vitest@^3` (dev), `@vitest/*` as needed - New symbol: `CertManagerModule` (class, implements OnModuleInit) - New symbol: `seedCertManagerModule(moduleRegistryService)` (async function) - New symbol: `CertManagerService` (@Injectable) with methods: `parseCert`, `splitCerts`, `mergeCerts`, `convertCert` (operation stubs) and helpers `detectFormat`, `toForgeBuffer`, `getFingerprint`, `parsePemChain` - New symbol: `CertManagerController` (@Controller('modules/cert-manager'), @UseModule('cert-manager')) with routes `POST parse`, `POST split`, `POST merge`, `POST convert` - New DTO classes: `ParseCertDto`, `MergeCertsDto`, `ConvertCertDto` - New test file: `apps/api/src/cert-manager/cert-manager.service.spec.ts` Task 1: Install node-forge + Vitest runner for @tessera/api apps/api/package.json, apps/api/vitest.config.ts - apps/api/package.json (current scripts + deps — no test runner exists yet) - apps/web/vitest.config.ts (reference Vitest config shape; API uses environment 'node' instead of 'jsdom', no react plugin) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Installation section + Package Legitimacy Audit — node-forge is Approved) Install runtime + dev deps in the API workspace: run `pnpm --filter @tessera/api add node-forge@^1.4.0`, then `pnpm --filter @tessera/api add -D @types/node-forge@^1.3.14 vitest@^3`. node-forge is Approved in the Package Legitimacy Audit (npm, 35.3M/wk, github.com/digitalbazaar/forge) — no legitimacy checkpoint required. Create apps/api/vitest.config.ts using `defineConfig` from `vitest/config` with: `test.environment` set to `'node'`, `test.globals` set to `true`, `test.include` set to `['src/**/*.spec.ts']`. Do NOT add jsdom or the react plugin (API is server-only). Add two scripts to apps/api/package.json: `"test": "vitest run"` and `"test:watch": "vitest"`. Do not add watch flags to the `test` script (must exit). pnpm --filter @tessera/api test --run 2>&1 | grep -Eiq 'no test files|passed|Test Files' && echo VITEST_OK - `node -e "const p=require('./apps/api/package.json'); process.exit(p.dependencies['node-forge']?0:1)"` exits 0 - `node -e "const p=require('./apps/api/package.json'); process.exit(p.devDependencies['@types/node-forge']&&p.devDependencies['vitest']?0:1)"` exits 0 - `apps/api/package.json` `scripts.test` equals `vitest run` - `apps/api/vitest.config.ts` exists and contains `environment: 'node'` - `pnpm --filter @tessera/api test --run` exits 0 (0 tests or passing tests, never a runner error) node-forge + @types/node-forge + vitest installed in @tessera/api; `pnpm --filter @tessera/api test` runs Vitest in a node environment and exits cleanly. Task 2: Scaffold cert-manager module + shared node-forge helpers with failing spec apps/api/src/cert-manager/cert-manager.module.ts, apps/api/src/cert-manager/cert-manager.seed.ts, apps/api/src/cert-manager/cert-manager.service.ts, apps/api/src/cert-manager/cert-manager.controller.ts, apps/api/src/cert-manager/dto/parse-cert.dto.ts, apps/api/src/cert-manager/dto/merge-certs.dto.ts, apps/api/src/cert-manager/dto/convert-cert.dto.ts, apps/api/src/cert-manager/cert-manager.service.spec.ts, apps/api/src/app.module.ts - apps/api/src/domaincheck/domaincheck.module.ts (OnModuleInit + seed pattern to copy exactly) - apps/api/src/domaincheck/domaincheck.seed.ts (seedModule call shape) - apps/api/src/domaincheck/domaincheck.service.ts (Injectable + Logger structure) - apps/api/src/domaincheck/dto/check-domain.dto.ts (DTO style) - .planning/phases/09-cert-manager-module/09-PATTERNS.md (Pattern Assignments: full module.ts, seed.ts, service structure, DTO shapes) - .planning/phases/09-cert-manager-module/09-RESEARCH.md (Pattern 5 node-forge helpers + Pattern 6 format detection + Pitfall 1 binary encoding) - Test: seedCertManagerModule calls moduleRegistryService.seedModule once with an object whose slug is 'cert-manager', category is 'security-tools', isSystem is true (CERT-06). Use a mock ModuleRegistryService. - Test: detectFormat('cert.pfx', anyBuffer) returns 'pfx'; detectFormat('cert.p7b', anyBuffer) returns 'p7b'; detectFormat('cert.der', anyBuffer) returns 'der'; detectFormat('cert.pem', pemBuffer) returns 'pem'. - Test: detectFormat('cert.cer', buffer starting with '-----BEGIN') returns 'pem'; detectFormat('cert.cer', binaryBuffer) returns 'der' (ambiguous .cer resolved by content sniff). - Test: getFingerprint(cert, 'sha256') returns an uppercase colon-separated hex string (matches /^[0-9A-F]{2}(:[0-9A-F]{2})+$/) computed over DER bytes, for a self-signed cert generated in beforeAll via forge.pki.rsa.generateKeyPair + forge.pki.createCertificate. - Test: parsePemChain(concatenation of two cert PEMs) returns an array of length 2. Create the module directory apps/api/src/cert-manager/ mirroring domaincheck. cert-manager.module.ts: copy domaincheck.module.ts structure — @Module imports [ModuleRegistryModule], controllers [CertManagerController], providers [CertManagerService], implements OnModuleInit, constructor injects ModuleRegistryService, onModuleInit calls seedCertManagerModule and logs success/failure via a Logger named CertManagerModule. cert-manager.seed.ts: export async seedCertManagerModule(moduleRegistryService) calling moduleRegistryService.seedModule with 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 (per PATTERNS seed pattern — implements CERT-06). cert-manager.service.ts: @Injectable with a Logger named CertManagerService. Implement the shared helpers concretely: detectFormat(filename, buffer) per RESEARCH Pattern 6; toForgeBuffer(buffer) returning forge.util.createBuffer(buffer.toString('binary')) (NEVER 'utf-8' — Pitfall 1); getFingerprint(cert, algorithm) per RESEARCH Pattern 5 (hash the DER bytes, uppercase colon-joined hex); parsePemChain(pem) using the BEGIN/END CERTIFICATE regex. Add operation method stubs parseCert, splitCerts, mergeCerts, convertCert that each throw a NestJS NotImplementedException for now (filled by later slices). Never log the password parameter. cert-manager.controller.ts: @Controller('modules/cert-manager') decorated with @UseModule('cert-manager') from ../module-registry/module.guard; constructor injects CertManagerService. Declare the four POST routes (parse, split, merge, convert) delegating to the service; parse/split/convert use FileInterceptor('file', { limits: { fileSize: 5*1024*1024 } }), merge uses FilesInterceptor('files', 20, { limits: { fileSize: 5*1024*1024 } }) per PATTERNS controller pattern. Reject missing input with BadRequestException. Route bodies may delegate to the (still-stubbed) service methods. dto/parse-cert.dto.ts, dto/merge-certs.dto.ts, dto/convert-cert.dto.ts: per PATTERNS DTO shapes (ParseCertDto { pemText, password? }, MergeCertsDto { outputFormat: 'pem'|'pfx', password? }, ConvertCertDto { targetFormat: 'pem'|'der'|'pfx'|'p7b', password? }). Register the module: add `import { CertManagerModule } from './cert-manager/cert-manager.module';` to apps/api/src/app.module.ts and add `CertManagerModule` to the @Module imports array (after DomaincheckModule). Create cert-manager.service.spec.ts implementing the Behavior tests above. Write the tests FIRST and confirm they fail (RED) against empty helpers, then implement the helpers until they pass (GREEN). Generate the test cert(s) in a beforeAll using node-forge (self-signed), so no key material is committed. pnpm --filter @tessera/api test cert-manager --run - `pnpm --filter @tessera/api test cert-manager --run` exits 0 with the seed, detectFormat, getFingerprint, and parsePemChain tests passing - `grep -q "slug: 'cert-manager'" apps/api/src/cert-manager/cert-manager.seed.ts` - `grep -q "@UseModule('cert-manager')" apps/api/src/cert-manager/cert-manager.controller.ts` - `grep -q "CertManagerModule" apps/api/src/app.module.ts` - `grep -q "toString('binary')" apps/api/src/cert-manager/cert-manager.service.ts` and no occurrence of `toString('utf-8')` in a forge.util.createBuffer call - `pnpm --filter @tessera/api type-check` exits 0 The cert-manager module is scaffolded per the domaincheck analog, registered in app.module.ts, seeds slug 'cert-manager' (CERT-06), and the shared node-forge helpers are implemented and green under Vitest. ## Trust Boundaries | Boundary | Description | |----------|-------------| | client -> API upload | Untrusted certificate bytes cross into node-forge parsing | | npm registry -> build | Third-party crypto dependency (node-forge) enters the build | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-09-SC | Tampering | node-forge / @types/node-forge install | high | mitigate | node-forge Approved in Package Legitimacy Audit (npm, 35.3M/wk, DigitalBazaar); pinned `^1.4.0`; no [ASSUMED]/[SUS] packages so no legitimacy checkpoint | | T-09-04 | Elevation of Privilege | CertManagerController routes | high | mitigate | Global JwtAuthGuard + TenantGuard (app.module) plus `@UseModule('cert-manager')` ModuleGuard on the controller — unauthenticated/unactivated requests get 401/403 | | T-09-03 | Denial of Service | FileInterceptor / FilesInterceptor upload | high | mitigate | `limits: { fileSize: 5 * 1024 * 1024 }` on every upload interceptor caps memory per request | | T-09-02 | Information Disclosure | service/controller password param | high | mitigate | `password` is never passed to a logger; service Logger only logs failure category text | - `pnpm --filter @tessera/api test cert-manager --run` — seed + helper suite green - `pnpm --filter @tessera/api type-check` — API compiles with new module - Manual (deferred to phase gate): restart API, activate cert-manager in Marketplace, confirm no startup errors and the registry row exists - node-forge + Vitest installed in @tessera/api; `pnpm --filter @tessera/api test` runs - cert-manager module registered and seeding slug 'cert-manager' (CERT-06) - Shared helpers implemented and unit-tested; binary encoding uses 'binary' not 'utf-8' - API type-checks clean Create `.planning/phases/09-cert-manager-module/09-01-SUMMARY.md` when done