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