Files
tessera-ctl/.planning/phases/09-cert-manager-module/09-01-PLAN.md
T
schalli 063666af3b
Tessera CI/CD / Lint & Type Check (push) Failing after 41s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Publish Images (push) Has been skipped
docs(09): create cert-manager phase plan (6 plans)
2026-07-01 16:24:31 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
09-cert-manager-module 01 execute 1
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
true
CERT-06
service why dashboard_config
marketplace-activation 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.
task location
Activate the cert-manager module for the tenant Tessera Portal -> Marketplace -> Cert Manager -> Aktivieren (after this plan runs and the API is restarted)
truths artifacts key_links
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
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)
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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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.

<threat_model>

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
</threat_model>
- `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

<success_criteria>

  • 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 </success_criteria>
Create `.planning/phases/09-cert-manager-module/09-01-SUMMARY.md` when done