import 'reflect-metadata'; import { getMetadataStorage } from 'class-validator'; import { BootstrapSetupDto } from './admin/bootstrap.dto.js'; /** * Boot-time self-check: the global ValidationPipe must be able to SEE the * decorated properties of the DTOs it guards (#1391, #436 class). * * WHY THIS EXISTS. When Nest resolves a @Body() metatype to Object — via * `import type` class erasure (#436), or a dependency graph where the * controller's decorators and the application's route enhancers disagree * (#1391's hypothesized dual-@nestjs/common on a mixed install) — the * ValidationPipe's whitelist treats every property as forbidden. The first * symptom is a 400 on the FIRST bootstrap attempt of a fresh install, the * worst place to discover wiring damage: the operator cannot tell a broken * payload from a broken daemon. * * This check fails LOUD at boot instead: if the pipe cannot see the DTO's * decorated properties, the gateway refuses to start with a named cause. * It catches the whole class — erasure, decorator metadata loss — on every * host, at the moment the damage exists rather than at first use. * * Storage sharing note: class-validator keys its metadata storage on * globalThis, so duplicate package copies do NOT hide metadata (measured, * #1391 diagnosis). What hides it is losing the metatype itself, which is * what this asserts against. */ /** * DTOs the global pipe guards, mapped to the properties the whitelist must * admit. Target is the CONSTRUCTOR (the object class itself): class-validator * decorators register metadata keyed on the constructor, and its executor * looks up `object.constructor` (ValidationExecutor.js:50) — the probe * through `prototype` returns zero. Extend when adding DTOs to the app. */ export const PIPE_GUARDED_DTOS: Array<{ name: string; target: abstract new (...args: never[]) => unknown; properties: string[]; }> = [ { name: 'BootstrapSetupDto', target: BootstrapSetupDto, properties: ['name', 'email', 'password'], }, ]; export class PipeMetatypeCheckError extends Error { constructor(missing: string[]) { super( 'ValidationPipe metatype check failed: ' + missing.join('; ') + '. The global ValidationPipe cannot see decorated DTO properties — ' + 'every request body would be rejected as non-whitelisted. ' + 'Check for import-type erasure or decorator metadata loss in the ' + 'dependency graph (see issues #436, #1391).', ); this.name = 'PipeMetatypeCheckError'; } } /** * Assert the pipe's whitelist can see every guarded DTO's decorated * properties. Throws PipeMetatypeCheckError (fail-loud at boot) listing * each miss. Pure function of module state: no I/O, safe to call twice. */ export function assertValidationPipeSeesDtoDecorators(): void { const storage = getMetadataStorage(); const missing: string[] = []; for (const dto of PIPE_GUARDED_DTOS) { // class-validator records constraints keyed on the DTO's constructor // (decorators run on the class), and its executor resolves them via // object.constructor. A property with no recorded metadata is invisible // to the whitelist — whatever the cause — and fails here. // Signature mirrors ValidationExecutor.js:50 — (constructor, schema, always, // strictGroups, groups?). No schema, always=true, no groups: every // constraint regardless of grouping, which is what the whitelist sees. const metadatas = storage.getTargetValidationMetadatas(dto.target, '', true, false); const decorated = new Set(metadatas.map((m) => m.propertyName)); for (const property of dto.properties) { if (!decorated.has(property)) { missing.push( `${dto.name}.${property} has no class-validator constraints visible to the pipe`, ); } } } if (missing.length > 0) { throw new PipeMetatypeCheckError(missing); } }