From a13a8a763fb7cf1a0473218c2934a5e177324a3c Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 23:17:23 +0200 Subject: [PATCH 1/4] chore(09-01): install node-forge + Vitest runner for @tessera/api - Add node-forge@^1.4.0 runtime dependency (certificate crypto) - Add @types/node-forge@^1.3.14 and vitest@^3 dev dependencies - Create apps/api/vitest.config.ts (environment: node, passWithNoTests) - Add test + test:watch scripts to apps/api/package.json --- apps/api/package.json | 9 +- apps/api/vitest.config.ts | 10 + pnpm-lock.yaml | 877 +++++++++++++++++++++++++++++++++++++- 3 files changed, 874 insertions(+), 22 deletions(-) create mode 100644 apps/api/vitest.config.ts diff --git a/apps/api/package.json b/apps/api/package.json index 0e581da..9ffe6b6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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" } } diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000..3f8a256 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['src/**/*.spec.ts'], + passWithNoTests: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4a1428..a00c66d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: ldapts: specifier: ^8.1.8 version: 8.1.8 + node-forge: + specifier: ^1.4.0 + version: 1.4.0 node-ical: specifier: 0.26.1 version: 0.26.1 @@ -119,7 +122,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.23(@swc/core@1.15.41)(@types/node@22.19.21)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15) + version: 11.0.23(@swc/core@1.15.41)(@types/node@22.19.21)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15) '@types/cookie-parser': specifier: ^1.4.10 version: 1.4.10(@types/express@5.0.6) @@ -129,6 +132,9 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.21 + '@types/node-forge': + specifier: ^1.3.14 + version: 1.3.14 '@types/nodemailer': specifier: ^8.0.1 version: 8.0.1 @@ -141,6 +147,9 @@ importers: typescript: specifier: ^5.5.0 version: 5.9.3 + vitest: + specifier: ^3 + version: 3.2.6(@types/debug@4.1.13)(@types/node@22.19.21)(jiti@2.7.0)(jsdom@29.1.1)(lightningcss@1.32.0)(terser@5.48.0) apps/desktop: dependencies: @@ -217,7 +226,7 @@ importers: version: 2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@vitejs/plugin-react': specifier: ^6.0.2 - version: 6.0.2(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0)) + version: 6.0.2(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) jsdom: specifier: ^29.1.1 version: 29.1.1 @@ -232,7 +241,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@22.19.21)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0)) + version: 4.1.9(@types/node@22.19.21)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) packages/module-sdk: devDependencies: @@ -522,6 +531,162 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@ewsjs/ntlm-client@3.0.1': resolution: {integrity: sha512-eJEyxiR0Eb1kRU0N96ISubBgyhZJ8PXhw+gj5m5My+M5CqDEn8Ae2xPHiSyo0fUWiSj1A2tQ+kk3PZmursR5kQ==} engines: {node: '>=4.0.0'} @@ -1337,6 +1502,131 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@schummar/icu-type-parser@1.21.5': resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} @@ -1743,6 +2033,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@22.19.21': resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} @@ -1834,9 +2127,23 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + '@vitest/expect@4.1.9': resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.1.9': resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: @@ -1848,18 +2155,33 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + '@vitest/pretty-format@4.1.9': resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + '@vitest/runner@4.1.9': resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + '@vitest/snapshot@4.1.9': resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + '@vitest/spy@4.1.9': resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} @@ -2151,6 +2473,10 @@ packages: magicast: optional: true + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2176,6 +2502,10 @@ packages: resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} engines: {node: '>=0.8'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2206,6 +2536,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -2489,6 +2823,10 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2691,6 +3029,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -2702,6 +3043,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3250,6 +3596,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true @@ -3460,6 +3809,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3878,6 +4230,10 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -4059,6 +4415,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} @@ -4563,6 +4923,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4713,6 +5078,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -4761,6 +5129,9 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -4882,6 +5253,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -4890,10 +5264,22 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tlds@1.261.0: resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} hasBin: true @@ -5090,6 +5476,51 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5133,6 +5564,34 @@ packages: yaml: optional: true + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.1.9: resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5584,6 +6043,84 @@ snapshots: '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + '@ewsjs/ntlm-client@3.0.1': dependencies: des.js: 1.1.0 @@ -5981,7 +6518,7 @@ snapshots: - typescript - uncss - '@nestjs/cli@11.0.23(@swc/core@1.15.41)(@types/node@22.19.21)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)': + '@nestjs/cli@11.0.23(@swc/core@1.15.41)(@types/node@22.19.21)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)': dependencies: '@angular-devkit/core': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) @@ -5992,14 +6529,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15) + webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.15.41 @@ -6287,6 +6824,81 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@schummar/icu-type-parser@1.21.5': {} '@selderee/plugin-htmlparser2@0.12.0(selderee@0.12.0)': @@ -6637,6 +7249,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 22.19.21 + '@types/node@22.19.21': dependencies: undici-types: 6.21.0 @@ -6744,10 +7360,18 @@ snapshots: '@ungap/structured-clone@1.3.2': {} - '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0))': + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0) + vite: 8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0) + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 '@vitest/expect@4.1.9': dependencies: @@ -6758,23 +7382,47 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0))': + '@vitest/mocker@3.2.6(vite@7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + + '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0) + vite: 8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0) + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + '@vitest/runner@4.1.9': dependencies: '@vitest/utils': 4.1.9 pathe: 2.0.3 + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/snapshot@4.1.9': dependencies: '@vitest/pretty-format': 4.1.9 @@ -6782,8 +7430,18 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.1.9': {} + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.1.9': dependencies: '@vitest/pretty-format': 4.1.9 @@ -7115,6 +7773,8 @@ snapshots: pkg-types: 2.3.1 rc9: 2.1.2 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7144,6 +7804,14 @@ snapshots: adler-32: 1.3.1 crc-32: 1.2.2 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} chalk@3.0.0: @@ -7172,6 +7840,8 @@ snapshots: chardet@2.1.1: {} + check-error@2.1.3: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -7486,6 +8156,8 @@ snapshots: dependencies: character-entities: 2.0.2 + deep-eql@5.0.2: {} + deep-extend@0.6.0: optional: true @@ -7678,6 +8350,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} es-object-atoms@1.1.2: @@ -7691,6 +8365,35 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-goat@3.0.0: @@ -7860,7 +8563,7 @@ snapshots: signal-exit: 4.1.0 optional: true - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -7875,7 +8578,7 @@ snapshots: semver: 7.8.4 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15) + webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15) form-data@4.0.6: dependencies: @@ -8387,6 +9090,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -8604,6 +9309,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: optional: true @@ -9613,6 +10320,8 @@ snapshots: node-fetch-native@1.6.7: {} + node-forge@1.4.0: {} + node-gyp-build@4.8.4: {} node-ical@0.26.1: @@ -9809,6 +10518,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + pause@0.0.1: {} pdf-parse@2.4.5: @@ -10492,6 +11203,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -10689,6 +11431,8 @@ snapshots: statuses@2.0.2: {} + std-env@3.10.0: {} + std-env@4.1.0: {} streamsearch@1.1.0: {} @@ -10738,6 +11482,10 @@ snapshots: strip-json-comments@2.0.1: optional: true + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -10798,16 +11546,17 @@ snapshots: temporal-spec@0.3.1: {} - terser-webpack-plugin@5.6.1(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.48.0 - webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15) + webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15) optionalDependencies: '@swc/core': 1.15.41 cssnano: 7.1.9(postcss@8.5.15) + lightningcss: 1.32.0 postcss: 8.5.15 terser@5.48.0: @@ -10823,6 +11572,8 @@ snapshots: tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.2.4: {} tinyglobby@0.2.17: @@ -10830,8 +11581,14 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} + tlds@1.261.0: optional: true @@ -11043,7 +11800,43 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0): + vite-node@3.2.4(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.21 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + terser: 5.48.0 + + vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -11052,14 +11845,58 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.21 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 terser: 5.48.0 - vitest@4.1.9(@types/node@22.19.21)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0)): + vitest@3.2.6(@types/debug@4.1.13)(@types/node@22.19.21)(jiti@2.7.0)(jsdom@29.1.1)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite-node: 3.2.4(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 22.19.21 + jsdom: 29.1.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@4.1.9(@types/node@22.19.21)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0)) + '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -11076,7 +11913,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@22.19.21)(jiti@2.7.0)(terser@5.48.0) + vite: 8.0.16(@types/node@22.19.21)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.21 @@ -11116,7 +11953,7 @@ snapshots: webpack-sources@3.5.0: {} - webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15): + webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -11139,7 +11976,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(lightningcss@1.32.0)(postcss@8.5.15)) watchpack: 2.5.2 webpack-sources: 3.5.0 transitivePeerDependencies: From a06694f9152ed8fca1415d1a52a2340a55977ee7 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 23:18:08 +0200 Subject: [PATCH 2/4] test(09-01): add failing spec for cert-manager seed + helpers (RED) - Test: seedCertManagerModule calls seedModule with slug='cert-manager', category='security-tools', isSystem=true - Test: detectFormat returns pem/der/pfx/p7b based on extension + content sniff - Test: getFingerprint returns uppercase colon-separated hex (sha1 + sha256) - Test: parsePemChain returns array of length 2 for two concatenated PEMs --- .../cert-manager/cert-manager.service.spec.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 apps/api/src/cert-manager/cert-manager.service.spec.ts diff --git a/apps/api/src/cert-manager/cert-manager.service.spec.ts b/apps/api/src/cert-manager/cert-manager.service.spec.ts new file mode 100644 index 0000000..eef1a67 --- /dev/null +++ b/apps/api/src/cert-manager/cert-manager.service.spec.ts @@ -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); + }); + }); +}); From 8bb5cf208db0a1ae57cd7f760082b659a9ac039f Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 23:21:36 +0200 Subject: [PATCH 3/4] feat(09-01): scaffold cert-manager module + shared node-forge helpers (GREEN) - cert-manager.module.ts: OnModuleInit + seedCertManagerModule (CERT-06) - cert-manager.seed.ts: slug='cert-manager', category='security-tools', isSystem=true - cert-manager.service.ts: detectFormat, toForgeBuffer, getFingerprint, parsePemChain; operation stubs parseCert/splitCerts/mergeCerts/convertCert throw NotImplementedException - cert-manager.controller.ts: 4 POST routes with FileInterceptor/FilesInterceptor (5 MB limit each), @UseModule('cert-manager') guard, BadRequestException on missing input - dto/: ParseCertDto, MergeCertsDto, ConvertCertDto - app.module.ts: CertManagerModule added to imports array - All 11 Vitest tests pass; type-check clean --- apps/api/src/app.module.ts | 2 + .../cert-manager/cert-manager.controller.ts | 114 ++++++++++++++++ .../src/cert-manager/cert-manager.module.ts | 41 ++++++ .../api/src/cert-manager/cert-manager.seed.ts | 26 ++++ .../src/cert-manager/cert-manager.service.ts | 122 ++++++++++++++++++ .../src/cert-manager/dto/convert-cert.dto.ts | 8 ++ .../src/cert-manager/dto/merge-certs.dto.ts | 8 ++ .../src/cert-manager/dto/parse-cert.dto.ts | 8 ++ 8 files changed, 329 insertions(+) create mode 100644 apps/api/src/cert-manager/cert-manager.controller.ts create mode 100644 apps/api/src/cert-manager/cert-manager.module.ts create mode 100644 apps/api/src/cert-manager/cert-manager.seed.ts create mode 100644 apps/api/src/cert-manager/cert-manager.service.ts create mode 100644 apps/api/src/cert-manager/dto/convert-cert.dto.ts create mode 100644 apps/api/src/cert-manager/dto/merge-certs.dto.ts create mode 100644 apps/api/src/cert-manager/dto/parse-cert.dto.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 5aecc53..172b310 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -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 { DomaincheckModule } from './domaincheck/domaincheck.module'; import { ModuleRegistryModule } from './module-registry/module-registry.module'; import { PrismaModule } from './prisma/prisma.module'; @@ -33,6 +34,7 @@ import { UserModule } from './user/user.module'; LdapModule, ModuleRegistryModule, DomaincheckModule, + CertManagerModule, DashboardModule, CalendarModule, SettingsModule, diff --git a/apps/api/src/cert-manager/cert-manager.controller.ts b/apps/api/src/cert-manager/cert-manager.controller.ts new file mode 100644 index 0000000..edfd7eb --- /dev/null +++ b/apps/api/src/cert-manager/cert-manager.controller.ts @@ -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 }); + } +} diff --git a/apps/api/src/cert-manager/cert-manager.module.ts b/apps/api/src/cert-manager/cert-manager.module.ts new file mode 100644 index 0000000..f793e4a --- /dev/null +++ b/apps/api/src/cert-manager/cert-manager.module.ts @@ -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 { + 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); + } + } +} diff --git a/apps/api/src/cert-manager/cert-manager.seed.ts b/apps/api/src/cert-manager/cert-manager.seed.ts new file mode 100644 index 0000000..dbbf03e --- /dev/null +++ b/apps/api/src/cert-manager/cert-manager.seed.ts @@ -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 { + 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, + }); +} diff --git a/apps/api/src/cert-manager/cert-manager.service.ts b/apps/api/src/cert-manager/cert-manager.service.ts new file mode 100644 index 0000000..3ce7225 --- /dev/null +++ b/apps/api/src/cert-manager/cert-manager.service.ts @@ -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 { + throw new NotImplementedException('parseCert is not yet implemented'); + } + + async splitCerts(_input: { + file?: any; + password?: string; + }): Promise { + throw new NotImplementedException('splitCerts is not yet implemented'); + } + + async mergeCerts(_input: { + files?: any[]; + outputFormat: string; + password?: string; + }): Promise { + throw new NotImplementedException('mergeCerts is not yet implemented'); + } + + async convertCert(_input: { + file?: any; + targetFormat: string; + password?: string; + }): Promise { + 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(fn: () => T, errorMsg: string): T { + try { + return fn(); + } catch (_err) { + this.logger.warn(`Cert parse failed: ${errorMsg}`); + throw new BadRequestException(errorMsg); + } + } +} diff --git a/apps/api/src/cert-manager/dto/convert-cert.dto.ts b/apps/api/src/cert-manager/dto/convert-cert.dto.ts new file mode 100644 index 0000000..c157f36 --- /dev/null +++ b/apps/api/src/cert-manager/dto/convert-cert.dto.ts @@ -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; +} diff --git a/apps/api/src/cert-manager/dto/merge-certs.dto.ts b/apps/api/src/cert-manager/dto/merge-certs.dto.ts new file mode 100644 index 0000000..776ff41 --- /dev/null +++ b/apps/api/src/cert-manager/dto/merge-certs.dto.ts @@ -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; +} diff --git a/apps/api/src/cert-manager/dto/parse-cert.dto.ts b/apps/api/src/cert-manager/dto/parse-cert.dto.ts new file mode 100644 index 0000000..342511f --- /dev/null +++ b/apps/api/src/cert-manager/dto/parse-cert.dto.ts @@ -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; +} From 3506d60dbe68dc42b440ff5edaef50080536bbd0 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 23:22:51 +0200 Subject: [PATCH 4/4] docs(09-01): complete cert-manager API foundation plan summary - SUMMARY.md with task results, deviations, stub tracking, threat scan - Self-check: all 9 files found, 3 commits verified, 11 tests green --- .../09-cert-manager-module/09-01-SUMMARY.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .planning/phases/09-cert-manager-module/09-01-SUMMARY.md diff --git a/.planning/phases/09-cert-manager-module/09-01-SUMMARY.md b/.planning/phases/09-cert-manager-module/09-01-SUMMARY.md new file mode 100644 index 0000000..4f51f71 --- /dev/null +++ b/.planning/phases/09-cert-manager-module/09-01-SUMMARY.md @@ -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 ``: +- 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 |