merge(09-01): resolve app.module.ts conflict (CertManagerModule + FavoritesModule)
Tessera CI/CD / Lint & Type Check (push) Failing after 37s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Publish Images (push) Has been skipped

This commit is contained in:
2026-07-01 23:25:48 +02:00
13 changed files with 1488 additions and 22 deletions
@@ -0,0 +1,155 @@
---
phase: 09-cert-manager-module
plan: "01"
subsystem: api
tags: [cert-manager, node-forge, vitest, nestjs, module-registry]
dependency_graph:
requires: []
provides: [cert-manager-module-scaffold, cert-manager-registry-seed, cert-manager-vitest-runner]
affects: [apps/api/src/app.module.ts]
tech_stack:
added: [node-forge@^1.4.0, "@types/node-forge@^1.3.14", vitest@^3]
patterns: [OnModuleInit-seed, UseModule-guard, FileInterceptor, FilesInterceptor, TDD-red-green]
key_files:
created:
- 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
modified:
- apps/api/package.json
- apps/api/src/app.module.ts
- pnpm-lock.yaml
decisions:
- "Use any type for multer file params (consistent with dkv/user controllers; @types/multer not installed)"
- "Added passWithNoTests: true to vitest config so runner exits 0 before test files exist"
- "Binary encoding uses toString('binary') never 'utf-8' per RESEARCH.md Pitfall 1"
metrics:
duration: "~8 minutes"
completed: "2026-07-01T21:21:45Z"
tasks_completed: 2
files_created: 9
files_modified: 3
requirements: [CERT-06]
status: complete
---
# Phase 09 Plan 01: API Foundation + Vitest Runner Summary
node-forge installed in @tessera/api; cert-manager module scaffolded per domaincheck pattern; module seeds 'cert-manager' into registry (CERT-06); shared crypto helpers implemented and unit-tested (11 tests, 100% GREEN).
## Objective
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, seed the module into the registry (CERT-06), and implement + unit-test the shared node-forge helpers every later slice depends on.
## Tasks Completed
| # | Name | Type | Commit | Status |
|---|------|------|--------|--------|
| 1 | Install node-forge + Vitest runner for @tessera/api | chore | a13a8a7 | done |
| 2 (RED) | Scaffold cert-manager module + helpers — failing spec | test | a06694f | done |
| 2 (GREEN) | Scaffold cert-manager module + helpers — implementation | feat | 8bb5cf2 | done |
## What Was Built
### Task 1: Vitest runner for @tessera/api
- Installed `node-forge@^1.4.0` (runtime), `@types/node-forge@^1.3.14` and `vitest@^3` (dev)
- Created `apps/api/vitest.config.ts` with `environment: 'node'`, `globals: true`, `include: src/**/*.spec.ts`, `passWithNoTests: true`
- Added `test: vitest run` and `test:watch: vitest` scripts to `apps/api/package.json`
- Verification: `pnpm --filter @tessera/api test` exits 0
### Task 2: Cert Manager Module Scaffold (TDD RED→GREEN)
**RED:** Spec written first (`cert-manager.service.spec.ts`) — failed with "Cannot find module" since files didn't exist.
**GREEN:** All 11 tests pass after implementation:
- `cert-manager.module.ts` — OnModuleInit calls `seedCertManagerModule`, Logger named `CertManagerModule`
- `cert-manager.seed.ts` — seeds `{ slug: 'cert-manager', name: 'Cert Manager', version: '1.0.0', category: 'security-tools', isSystem: true }`
- `cert-manager.service.ts` — shared helpers:
- `detectFormat(filename, buffer)` — extension + content sniff; resolves `.cer` ambiguity
- `toForgeBuffer(buffer)` — uses `buffer.toString('binary')` (never 'utf-8')
- `getFingerprint(cert, algorithm)` — DER bytes → sha1/sha256 → uppercase colon-hex
- `parsePemChain(pem)` — regex split → array of forge.pki.Certificate
- Operation stubs: `parseCert`, `splitCerts`, `mergeCerts`, `convertCert` → `NotImplementedException`
- `cert-manager.controller.ts` — `@Controller('modules/cert-manager')` + `@UseModule('cert-manager')`; 4 POST routes with `FileInterceptor`/`FilesInterceptor` (5 MB limit), `BadRequestException` on missing input
- `dto/` — `ParseCertDto`, `MergeCertsDto`, `ConvertCertDto`
- `app.module.ts` — `CertManagerModule` added to imports array after `DomaincheckModule`
## Verification Results
```
pnpm --filter @tessera/api test
✓ src/cert-manager/cert-manager.service.spec.ts (11 tests) 89ms
Test Files 1 passed (1)
Tests 11 passed (11)
pnpm --filter @tessera/api type-check
→ Exit 0 (no errors)
```
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] `Express.Multer.File` type not available**
- **Found during:** Task 2 type-check
- **Issue:** `@types/multer` is not installed in the project. Using `Express.Multer.File` in controller/service caused 8 TypeScript errors.
- **Fix:** Changed all file parameter types to `any` — consistent with existing `dkv.controller.ts` and `user.controller.ts` which also use `any` for `@UploadedFile()` parameters.
- **Files modified:** `cert-manager.controller.ts`, `cert-manager.service.ts`
- **Commit:** 8bb5cf2
**2. [Rule 1 - Bug] Vitest exits with code 1 when no test files exist**
- **Found during:** Task 1 verification
- **Issue:** Vitest 3.x exits with code 1 ("No test files found, exiting with code 1") when include pattern matches zero files — causes `pnpm --filter @tessera/api test` to fail before any test files are created.
- **Fix:** Added `passWithNoTests: true` to `vitest.config.ts`
- **Files modified:** `apps/api/vitest.config.ts`
- **Commit:** a13a8a7
## Known Stubs
| File | Stub | Reason |
|------|------|--------|
| `cert-manager.service.ts` | `parseCert` throws `NotImplementedException` | Implemented in Phase 09 Plan 02 (Inspect slice) |
| `cert-manager.service.ts` | `splitCerts` throws `NotImplementedException` | Implemented in Phase 09 Plan 03 (Split slice) |
| `cert-manager.service.ts` | `mergeCerts` throws `NotImplementedException` | Implemented in Phase 09 Plan 05 (Merge/PFX slice) |
| `cert-manager.service.ts` | `convertCert` throws `NotImplementedException` | Implemented in Phase 09 Plan 04 (Convert slice) |
These stubs are intentional — this plan's goal is module scaffolding and helper verification. Operation implementations are in subsequent plan slices per wave decomposition.
## Threat Surface Scan
No new threat surface beyond what is described in the plan's `<threat_model>`:
- T-09-04: `@UseModule('cert-manager')` guard is in place on the controller
- T-09-03: `limits: { fileSize: 5 * 1024 * 1024 }` on all FileInterceptor/FilesInterceptor calls
- T-09-02: Password not passed to any logger
- T-09-SC: node-forge@^1.4.0 installed (Approved per Package Legitimacy Audit)
## User Setup Required
Before cert-manager API endpoints respond (not 403): activate the module via **Tessera Portal → Marketplace → Cert Manager → Aktivieren** after API restart. The seed (`isSystem: true`) registers the module in the registry but does NOT auto-activate per tenant (RESEARCH.md Pitfall 2).
## Self-Check: PASSED
| Check | Result |
|-------|--------|
| apps/api/vitest.config.ts | FOUND |
| apps/api/src/cert-manager/cert-manager.module.ts | FOUND |
| apps/api/src/cert-manager/cert-manager.seed.ts | FOUND |
| apps/api/src/cert-manager/cert-manager.service.ts | FOUND |
| apps/api/src/cert-manager/cert-manager.controller.ts | FOUND |
| apps/api/src/cert-manager/dto/parse-cert.dto.ts | FOUND |
| apps/api/src/cert-manager/dto/merge-certs.dto.ts | FOUND |
| apps/api/src/cert-manager/dto/convert-cert.dto.ts | FOUND |
| apps/api/src/cert-manager/cert-manager.service.spec.ts | FOUND |
| Commit a13a8a7 | FOUND |
| Commit a06694f | FOUND |
| Commit 8bb5cf2 | FOUND |
| 11 tests passing | VERIFIED |
| type-check clean | VERIFIED |
+7 -2
View File
@@ -7,6 +7,8 @@
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"postinstall": "test -f prisma/schema.prisma && prisma generate || true"
},
"dependencies": {
@@ -21,7 +23,6 @@
"@nestjs/platform-express": "^11.0.0",
"@nestjs/schedule": "^6.1.3",
"@prisma/client": "^6.0.0",
"prisma": "^6.0.0",
"@tessera/shared": "workspace:*",
"argon2": "^0.44.0",
"class-transformer": "^0.5.1",
@@ -32,12 +33,14 @@
"httpntlm": "^1.8.13",
"imapflow": "^1.4.3",
"ldapts": "^8.1.8",
"node-forge": "^1.4.0",
"node-ical": "0.26.1",
"nodemailer": "^9.0.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pdf-parse": "^2.4.5",
"prisma": "^6.0.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.0.0",
"tsdav": "2.2.2",
@@ -48,9 +51,11 @@
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"@types/node-forge": "^1.3.14",
"@types/nodemailer": "^8.0.1",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"typescript": "^5.5.0"
"typescript": "^5.5.0",
"vitest": "^3"
}
}
+2
View File
@@ -12,6 +12,7 @@ import { MailModule } from './mail/mail.module';
import { CalendarModule } from './calendar/calendar.module';
import { DashboardModule } from './dashboard/dashboard.module';
import { DkvModule } from './dkv/dkv.module';
import { CertManagerModule } from './cert-manager/cert-manager.module';
import { FavoritesModule } from './favorites/favorites.module';
import { DomaincheckModule } from './domaincheck/domaincheck.module';
import { ModuleRegistryModule } from './module-registry/module-registry.module';
@@ -34,6 +35,7 @@ import { UserModule } from './user/user.module';
LdapModule,
ModuleRegistryModule,
DomaincheckModule,
CertManagerModule,
DashboardModule,
CalendarModule,
SettingsModule,
@@ -0,0 +1,114 @@
import {
BadRequestException,
Body,
Controller,
Post,
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';
/**
* CertManagerController — 4 POST endpoints for certificate operations.
*
* All routes are protected by:
* - Global JwtAuthGuard (authentication)
* - Global TenantGuard (tenant context)
* - @UseModule('cert-manager') ModuleGuard (module activation check)
*
* File size limit: 5 MB per file (T-09-03 — DoS mitigation).
* Password parameter is never passed to a logger (T-09-02 — InfoDisc mitigation).
*/
@Controller('modules/cert-manager')
@UseModule('cert-manager')
export class CertManagerController {
constructor(private readonly certManagerService: CertManagerService) {}
/**
* POST /modules/cert-manager/parse
* Inspect a single certificate: subject, issuer, validity, SANs, fingerprints.
* Accepts multipart file upload OR JSON body with pemText.
*/
@Post('parse')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 },
}),
)
async parseCert(
@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 });
}
/**
* POST /modules/cert-manager/split
* Split a fullchain.pem or P7B bundle into individual certificates.
*/
@Post('split')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 },
}),
)
async splitCerts(
@UploadedFile() file: any,
@Body('password') password?: string,
) {
if (!file) {
throw new BadRequestException('No file provided');
}
return this.certManagerService.splitCerts({ file, password });
}
/**
* POST /modules/cert-manager/merge
* Merge multiple certificates into a PEM chain or PFX bundle.
* Uses FilesInterceptor (plural) to accept multiple files with field name "files".
*/
@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 });
}
/**
* POST /modules/cert-manager/convert
* Convert a certificate between PEM, DER, PFX/P12, P7B, CRT/CER formats.
*/
@Post('convert')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 },
}),
)
async convertCert(
@UploadedFile() file: any,
@Body('targetFormat') targetFormat: string,
@Body('password') password?: string,
) {
if (!file) {
throw new BadRequestException('No file provided');
}
return this.certManagerService.convertCert({ file, targetFormat, password });
}
}
@@ -0,0 +1,41 @@
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';
/**
* NestJS module for the Cert Manager feature.
*
* Provides server-side certificate inspection, splitting, merging,
* and format conversion using node-forge (pure JS, no native bindings).
*
* Seeds itself into the module registry on application startup via
* OnModuleInit lifecycle hook (CERT-06).
*
* After seeding: an admin must activate the module per-tenant via the
* Marketplace UI before endpoints become accessible (ModuleGuard checks
* TenantModuleActivation, not isSystem flag — RESEARCH.md Pitfall 2).
*/
@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<void> {
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);
}
}
}
@@ -0,0 +1,26 @@
import { ModuleRegistryService } from '../module-registry/module-registry.service';
/**
* Seeds the cert-manager module into the module registry.
*
* Called during CertManagerModule initialization to ensure the
* "cert-manager" module record exists in the database (CERT-06).
*
* isSystem: true registers the module in the registry but does NOT
* auto-activate it per tenant. Admin must activate via Marketplace UI.
*/
export async function seedCertManagerModule(
moduleRegistryService: ModuleRegistryService,
): Promise<void> {
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,
});
}
@@ -0,0 +1,130 @@
import * as forge from 'node-forge';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { seedCertManagerModule } from './cert-manager.seed';
import { CertManagerService } from './cert-manager.service';
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
/** Generate a self-signed X.509 cert via node-forge (RSA 1024 — fast for tests) */
function generateSelfSignedCert(): forge.pki.Certificate {
const keys = forge.pki.rsa.generateKeyPair(1024);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [{ name: 'commonName', value: 'test.example.com' }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey, forge.md.sha256.create());
return cert;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('seedCertManagerModule', () => {
it('calls moduleRegistryService.seedModule once with cert-manager slug', async () => {
const mockSeedModule = vi.fn().mockResolvedValue(undefined);
const mockModuleRegistryService = { seedModule: mockSeedModule } as any;
await seedCertManagerModule(mockModuleRegistryService);
expect(mockSeedModule).toHaveBeenCalledTimes(1);
const arg = mockSeedModule.mock.calls[0][0];
expect(arg.slug).toBe('cert-manager');
expect(arg.category).toBe('security-tools');
expect(arg.isSystem).toBe(true);
});
});
describe('CertManagerService helpers', () => {
let service: CertManagerService;
let testCert: forge.pki.Certificate;
beforeAll(() => {
service = new CertManagerService();
testCert = generateSelfSignedCert();
});
// -------------------------------------------------------------------------
// detectFormat
// -------------------------------------------------------------------------
describe('detectFormat', () => {
it('returns "pfx" for .pfx extension', () => {
const buf = Buffer.from([0x30, 0x82]); // arbitrary binary
expect(service.detectFormat('cert.pfx', buf)).toBe('pfx');
});
it('returns "p7b" for .p7b extension', () => {
const buf = Buffer.from([0x30, 0x82]);
expect(service.detectFormat('cert.p7b', buf)).toBe('p7b');
});
it('returns "der" for .der extension', () => {
const buf = Buffer.from([0x30, 0x82]);
expect(service.detectFormat('cert.der', buf)).toBe('der');
});
it('returns "pem" for .pem extension', () => {
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
expect(service.detectFormat('cert.pem', buf)).toBe('pem');
});
it('returns "pem" for .cer extension with PEM content', () => {
const buf = Buffer.from('-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----');
expect(service.detectFormat('cert.cer', buf)).toBe('pem');
});
it('returns "der" for .cer extension with binary (non-PEM) content', () => {
const buf = Buffer.from([0x30, 0x82, 0x01, 0x00]);
expect(service.detectFormat('cert.cer', buf)).toBe('der');
});
});
// -------------------------------------------------------------------------
// getFingerprint
// -------------------------------------------------------------------------
describe('getFingerprint', () => {
it('returns uppercase colon-separated hex for sha256', () => {
const fp = service.getFingerprint(testCert, 'sha256');
// e.g. "AA:BB:CC:..."
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
});
it('returns uppercase colon-separated hex for sha1', () => {
const fp = service.getFingerprint(testCert, 'sha1');
expect(fp).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2})+$/);
});
});
// -------------------------------------------------------------------------
// parsePemChain
// -------------------------------------------------------------------------
describe('parsePemChain', () => {
it('returns array of length 2 for two concatenated cert PEMs', () => {
const cert1 = generateSelfSignedCert();
const cert2 = generateSelfSignedCert();
const pem1 = forge.pki.certificateToPem(cert1);
const pem2 = forge.pki.certificateToPem(cert2);
const chain = pem1 + '\n' + pem2;
const parsed = service.parsePemChain(chain);
expect(parsed).toHaveLength(2);
});
it('returns array of length 1 for a single PEM', () => {
const pem = forge.pki.certificateToPem(testCert);
const parsed = service.parsePemChain(pem);
expect(parsed).toHaveLength(1);
});
});
});
@@ -0,0 +1,122 @@
import { BadRequestException, Injectable, Logger, NotImplementedException } from '@nestjs/common';
import * as forge from 'node-forge';
/**
* CertManagerService — server-side certificate operations.
*
* All cryptographic processing is ephemeral (upload → process → return).
* No data is persisted to disk or database.
*
* SECURITY NOTES:
* - Binary buffers MUST use toString('binary') for forge (never 'utf-8' — Pitfall 1)
* - Password parameters are never passed to the logger
* - All forge operations wrapped in try/catch → BadRequestException
*/
@Injectable()
export class CertManagerService {
private readonly logger = new Logger(CertManagerService.name);
// ---------------------------------------------------------------------------
// Shared helpers (used by all operation methods)
// ---------------------------------------------------------------------------
/**
* Detect the format of a certificate file from extension + content sniff.
* .cer is ambiguous — resolved by inspecting the first bytes of the buffer.
*/
detectFormat(
filename: string,
buffer: Buffer,
): 'pem' | 'der' | 'pfx' | 'p7b' {
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
const isPemContent = buffer.slice(0, 27).toString('ascii').includes('-----BEGIN');
if (ext === 'pfx' || ext === 'p12') return 'pfx';
if (ext === 'p7b' || ext === 'p7c') return 'p7b';
if (ext === 'der') return 'der';
if (ext === 'pem' || ext === 'crt') return 'pem';
if (ext === 'cer') return isPemContent ? 'pem' : 'der'; // .cer is ambiguous
// Fallback: sniff content
return isPemContent ? 'pem' : 'der';
}
/**
* Convert a Node.js Buffer to a forge ByteStringBuffer using 'binary' encoding.
*
* CRITICAL: Always use 'binary' encoding — UTF-8 corrupts DER/PFX/P7B binary data.
* See RESEARCH.md Pitfall 1.
*/
toForgeBuffer(buffer: Buffer): forge.util.ByteStringBuffer {
return forge.util.createBuffer(buffer.toString('binary'));
}
/**
* Compute SHA-1 or SHA-256 fingerprint of a certificate.
* Hash is computed over the DER-encoded bytes, returned as uppercase colon-joined hex.
*/
getFingerprint(cert: forge.pki.Certificate, algorithm: 'sha1' | 'sha256'): string {
const md = algorithm === 'sha1' ? forge.md.sha1.create() : forge.md.sha256.create();
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
md.update(der);
return md.digest().toHex().match(/.{2}/g)!.join(':').toUpperCase();
}
/**
* Split a PEM string containing one or more concatenated certificates.
* Returns an array of parsed forge Certificate objects.
*/
parsePemChain(pem: string): forge.pki.Certificate[] {
const blocks =
pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? [];
return blocks.map((b) => forge.pki.certificateFromPem(b));
}
// ---------------------------------------------------------------------------
// Operation method stubs (implemented in later plan slices)
// ---------------------------------------------------------------------------
async parseCert(_input: {
file?: any;
pemText?: string;
password?: string;
}): Promise<never> {
throw new NotImplementedException('parseCert is not yet implemented');
}
async splitCerts(_input: {
file?: any;
password?: string;
}): Promise<never> {
throw new NotImplementedException('splitCerts is not yet implemented');
}
async mergeCerts(_input: {
files?: any[];
outputFormat: string;
password?: string;
}): Promise<never> {
throw new NotImplementedException('mergeCerts is not yet implemented');
}
async convertCert(_input: {
file?: any;
targetFormat: string;
password?: string;
}): Promise<never> {
throw new NotImplementedException('convertCert is not yet implemented');
}
// ---------------------------------------------------------------------------
// Internal helpers for later slices
// ---------------------------------------------------------------------------
/** Wrap a node-forge operation and re-throw as BadRequestException on failure */
protected _parseOrThrow<T>(fn: () => T, errorMsg: string): T {
try {
return fn();
} catch (_err) {
this.logger.warn(`Cert parse failed: ${errorMsg}`);
throw new BadRequestException(errorMsg);
}
}
}
@@ -0,0 +1,8 @@
/**
* DTO for the cert-manager convert endpoint body fields.
* Used alongside FileInterceptor for single-file upload.
*/
export class ConvertCertDto {
targetFormat!: 'pem' | 'der' | 'pfx' | 'p7b';
password?: string;
}
@@ -0,0 +1,8 @@
/**
* DTO for the cert-manager merge endpoint body fields.
* Used alongside FilesInterceptor for multi-file upload.
*/
export class MergeCertsDto {
outputFormat!: 'pem' | 'pfx';
password?: string;
}
@@ -0,0 +1,8 @@
/**
* DTO for the cert-manager parse endpoint (text paste / JSON body path).
* For file uploads the body fields are extracted via @Body() in the controller.
*/
export class ParseCertDto {
pemText!: string;
password?: string;
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
include: ['src/**/*.spec.ts'],
passWithNoTests: true,
},
});
+857 -20
View File
File diff suppressed because it is too large Load Diff