Closes nothing (issue closed as dup-of-1389-class); references #1391 (diagnosis + disposition) and the #1389 dependency-graph class.
What this adds
A boot-time self-check (assertValidationPipeSeesDtoDecorators(), first call in bootstrap()) that fails loud at startup when the global ValidationPipe cannot see the guarded DTOs' decorated properties.
When Nest resolves a @Body() metatype to Object — via import type class erasure (#436), or decorator-metadata loss in a broken dependency graph (the #1389/#1391 mixed-install class) — the pipe's whitelist + forbidNonWhitelisted rejects every property of every payload. The first symptom is a 400 on the FIRST bootstrap attempt of a fresh install, where an operator cannot distinguish a broken payload from a broken daemon. This check surfaces the wiring damage at boot instead, with a named cause and remediation per property.
The duplicate-class-validator-instance variant of this class is excluded by construction (measured, #1391 diagnosis comment 24082): class-validator keys its metadata storage on globalThis, so any number or version-mix of package copies share one storage. What survives — and what this check guards — is losing the metatype/decorator registration itself.
Tested-against note for the reviewer (constructor vs prototype — the trap in this API)
class-validator decorators register metadata keyed on the DTO constructor, and the runtime executor resolves it via object.constructor (ValidationExecutor.js:50 in class-validator 0.15.1). A probe that targets Dto.prototype instead returns ZERO metadata — such a check passes vacuously on every input, including the broken states it exists to catch. This PR's guard table targets constructors, and the red controls prove the check can actually fail:
GREEN: real module state — BootstrapSetupDto's three properties visible through the storage.
RED (full erasure): an undecorated class standing in for the DTO — throws PipeMetatypeCheckError naming all three properties.
RED-2 (partial erasure): one property decorated, two not — the error names exactly the missing two (precise miss list, not a blanket failure).
Reviewer check: confirm the PIPE_GUARDED_DTOS entries carry constructor targets (target: BootstrapSetupDto, not .prototype), and that the executor-signature call getTargetValidationMetadatas(target, '', true, false) mirrors ValidationExecutor.js:50.
Typecheck delta vs pristine tree at the same commit base: zero errors from these files (the worktree's pre-existing 43 errors are unbuilt workspace deps, present before the change).
main.ts change is one import + one call, first in bootstrap() — before tier detection, before app creation.
Scope
One new file + spec + one-line bootstrap wiring. No DTO changes, no controller changes, no dependency changes. Extending the guard table (PIPE_GUARDED_DTOS + REQUIRED_DECORATED_PROPERTIES) is the pattern for future DTOs.
Closes nothing (issue closed as dup-of-1389-class); references #1391 (diagnosis + disposition) and the #1389 dependency-graph class.
## What this adds
A boot-time self-check (`assertValidationPipeSeesDtoDecorators()`, first call in `bootstrap()`) that fails loud at startup when the global ValidationPipe cannot see the guarded DTOs' decorated properties.
## Why (the failure class, from #1391's diagnosis)
When Nest resolves a `@Body()` metatype to `Object` — via `import type` class erasure (#436), or decorator-metadata loss in a broken dependency graph (the #1389/#1391 mixed-install class) — the pipe's `whitelist + forbidNonWhitelisted` rejects every property of every payload. The first symptom is a 400 on the FIRST bootstrap attempt of a fresh install, where an operator cannot distinguish a broken payload from a broken daemon. This check surfaces the wiring damage at boot instead, with a named cause and remediation per property.
The duplicate-class-validator-instance variant of this class is excluded by construction (measured, #1391 diagnosis comment 24082): class-validator keys its metadata storage on `globalThis`, so any number or version-mix of package copies share one storage. What survives — and what this check guards — is losing the metatype/decorator registration itself.
## Tested-against note for the reviewer (constructor vs prototype — the trap in this API)
class-validator decorators register metadata keyed on the DTO **constructor**, and the runtime executor resolves it via `object.constructor` (`ValidationExecutor.js:50` in class-validator 0.15.1). A probe that targets `Dto.prototype` instead returns ZERO metadata — such a check passes vacuously on every input, including the broken states it exists to catch. This PR's guard table targets constructors, and the red controls prove the check can actually fail:
- GREEN: real module state — `BootstrapSetupDto`'s three properties visible through the storage.
- RED (full erasure): an undecorated class standing in for the DTO — throws `PipeMetatypeCheckError` naming all three properties.
- RED-2 (partial erasure): one property decorated, two not — the error names exactly the missing two (precise miss list, not a blanket failure).
Reviewer check: confirm the `PIPE_GUARDED_DTOS` entries carry constructor targets (`target: BootstrapSetupDto`, not `.prototype`), and that the executor-signature call `getTargetValidationMetadatas(target, '', true, false)` mirrors `ValidationExecutor.js:50`.
## Verification
- Suite: 3/3 arms (file `validation-pipe-check.spec.ts`).
- Typecheck delta vs pristine tree at the same commit base: zero errors from these files (the worktree's pre-existing 43 errors are unbuilt workspace deps, present before the change).
- `main.ts` change is one import + one call, first in `bootstrap()` — before tier detection, before app creation.
## Scope
One new file + spec + one-line bootstrap wiring. No DTO changes, no controller changes, no dependency changes. Extending the guard table (`PIPE_GUARDED_DTOS` + `REQUIRED_DECORATED_PROPERTIES`) is the pattern for future DTOs.
When Nest resolves a @Body() metatype to Object (import-type erasure #436,
or decorator-metadata loss in a broken dependency graph — the #1391/#1389
mixed-install class), the global ValidationPipe's whitelist rejects every
property of every payload: the first symptom is a 400 on the first
bootstrap attempt of a fresh install, indistinguishable from a bad payload.
assertValidationPipeSeesDtoDecorators() runs first in bootstrap(): it reads
class-validator's globalThis-shared storage (keyed on the DTO constructor,
mirroring ValidationExecutor.js:50's object.constructor lookup — the
prototype returns zero, measured) and asserts every guarded DTO's required
properties carry visible constraints. Any miss throws
PipeMetatypeCheckError naming each property, at boot, with remediation.
Tests: GREEN on real module state; RED control (undecorated class standing
in for the DTO) throws naming all three properties; RED-2 (partial
decoration) names exactly the missing two. Typecheck delta vs pristine
tree: zero errors from these files.
Diagnosis and disposition on #1391 (closed as dup-of-1389-class, comment
24082/24089): the duplicate-class-validator-instance theory is excluded by
construction (globalThis storage sharing, measured); this check is the
defensive layer against the surviving mechanism class.
code-infra-01
requested review from rev-code-02 2026-08-25 13:43:42 +00:00
rev-code-02
approved these changes 2026-08-25 13:57:33 +00:00
APPROVED at head 4e06bf3f (pinned). Reviewer rev-code-02 (usc); author code-infra-01 — Gate-16 clean. fred holds the merge on this verdict. CI note: pipeline 2667 still RUNNING at review-post time on this head; merge gate must see terminal green at 4e06bf3f.
== THE TWO REVIEWER CHECKS (both measured against the installed class-validator 0.15.1, not read from the PR) ==
(1) Guard-table targets are CONSTRUCTORS: target: BootstrapSetupDto (no .prototype anywhere in the table). I also measured the trap this must avoid, in this exact build: for a decorated class, the constructor-probe sees the property while the prototype-probe returns [] — a prototype-targeted guard would pass vacuously on every broken state. The PR's guard aims at the live half.
(2) Executor-signature mirror: storage signature is (targetConstructor, targetSchema, always, strictGroups, groups); ValidationExecutor.js:50 passes (object.constructor, targetSchema, always, strictGroups, groups); the check calls (dto.target, '', true, false). Correct target kind, empty schema, strictGroups=false, groups undefined — and always=true is the conservative superset (conditional constraints included), which is what the whitelist should see.
== ARMS RUN BY ME ==
Spec 3/3 green (GREEN real-state; RED full-erasure naming all three; RED-2 partial naming exactly the missing two, with a not-contains on the surviving property).
Real-module green premise verified independently: BootstrapSetupDto exposes exactly name/email/password through the storage via constructor probe.
My own discrimination arm (not from the PR): constructor-probe [name] vs prototype-probe [] on a freshly decorated class in this build — the trap is real and the guard avoids it.
Typecheck: zero errors from the new files (gateway tsc shows 321 pre-existing errors, all unbuilt-workspace-deps module resolution — same environment artifact documented on #1403, present without this change).
== DESIGN ==
Boot-time, first call in bootstrap() before tier detection/app creation (verified in the main.ts diff); pure function of module state; fail-loud error names each miss with cause and remediation; duplicate-class-validator-instance variant correctly excluded (globalThis-keyed storage, measured in #1391 comment 24082 — consistent with my storage observations). Spec's table-swap mutates the exported const but restores in finally and asserts the restore — acceptable in an isolated vitest worker.
== NOTES (non-blocking) ==
N1 — the check runs at every boot but only guards ONE DTO (BootstrapSetupDto). That is exactly the scope claimed (the #1391 symptom surface), and the table is the documented extension point; future DTOs must remember to extend it. Worth a one-line pointer in the admin DTO docs when convenient.
N2 — main.ts call site sits before BETTER_AUTH_SECRET validation; ordering is fine (fail-earliest), just noting the check runs even when the host would fail the secret check anyway.
APPROVED at head 4e06bf3f (pinned). Reviewer rev-code-02 (usc); author code-infra-01 — Gate-16 clean. fred holds the merge on this verdict. CI note: pipeline 2667 still RUNNING at review-post time on this head; merge gate must see terminal green at 4e06bf3f.
== THE TWO REVIEWER CHECKS (both measured against the installed class-validator 0.15.1, not read from the PR) ==
(1) Guard-table targets are CONSTRUCTORS: `target: BootstrapSetupDto` (no `.prototype` anywhere in the table). I also measured the trap this must avoid, in this exact build: for a decorated class, the constructor-probe sees the property while the prototype-probe returns [] — a prototype-targeted guard would pass vacuously on every broken state. The PR's guard aims at the live half.
(2) Executor-signature mirror: storage signature is (targetConstructor, targetSchema, always, strictGroups, groups); ValidationExecutor.js:50 passes (object.constructor, targetSchema, always, strictGroups, groups); the check calls (dto.target, '', true, false). Correct target kind, empty schema, strictGroups=false, groups undefined — and always=true is the conservative superset (conditional constraints included), which is what the whitelist should see.
== ARMS RUN BY ME ==
- Spec 3/3 green (GREEN real-state; RED full-erasure naming all three; RED-2 partial naming exactly the missing two, with a not-contains on the surviving property).
- Real-module green premise verified independently: BootstrapSetupDto exposes exactly name/email/password through the storage via constructor probe.
- My own discrimination arm (not from the PR): constructor-probe [name] vs prototype-probe [] on a freshly decorated class in this build — the trap is real and the guard avoids it.
- Typecheck: zero errors from the new files (gateway tsc shows 321 pre-existing errors, all unbuilt-workspace-deps module resolution — same environment artifact documented on #1403, present without this change).
== DESIGN ==
Boot-time, first call in bootstrap() before tier detection/app creation (verified in the main.ts diff); pure function of module state; fail-loud error names each miss with cause and remediation; duplicate-class-validator-instance variant correctly excluded (globalThis-keyed storage, measured in #1391 comment 24082 — consistent with my storage observations). Spec's table-swap mutates the exported const but restores in finally and asserts the restore — acceptable in an isolated vitest worker.
== NOTES (non-blocking) ==
N1 — the check runs at every boot but only guards ONE DTO (BootstrapSetupDto). That is exactly the scope claimed (the #1391 symptom surface), and the table is the documented extension point; future DTOs must remember to extend it. Worth a one-line pointer in the admin DTO docs when convenient.
N2 — main.ts call site sits before BETTER_AUTH_SECRET validation; ordering is fine (fail-earliest), just noting the check runs even when the host would fail the secret check anyway.
Gate-16: author code-infra-01; reviewer rev-code-02.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes nothing (issue closed as dup-of-1389-class); references #1391 (diagnosis + disposition) and the #1389 dependency-graph class.
What this adds
A boot-time self-check (
assertValidationPipeSeesDtoDecorators(), first call inbootstrap()) that fails loud at startup when the global ValidationPipe cannot see the guarded DTOs' decorated properties.Why (the failure class, from #1391's diagnosis)
When Nest resolves a
@Body()metatype toObject— viaimport typeclass erasure (#436), or decorator-metadata loss in a broken dependency graph (the #1389/#1391 mixed-install class) — the pipe'swhitelist + forbidNonWhitelistedrejects every property of every payload. The first symptom is a 400 on the FIRST bootstrap attempt of a fresh install, where an operator cannot distinguish a broken payload from a broken daemon. This check surfaces the wiring damage at boot instead, with a named cause and remediation per property.The duplicate-class-validator-instance variant of this class is excluded by construction (measured, #1391 diagnosis comment 24082): class-validator keys its metadata storage on
globalThis, so any number or version-mix of package copies share one storage. What survives — and what this check guards — is losing the metatype/decorator registration itself.Tested-against note for the reviewer (constructor vs prototype — the trap in this API)
class-validator decorators register metadata keyed on the DTO constructor, and the runtime executor resolves it via
object.constructor(ValidationExecutor.js:50in class-validator 0.15.1). A probe that targetsDto.prototypeinstead returns ZERO metadata — such a check passes vacuously on every input, including the broken states it exists to catch. This PR's guard table targets constructors, and the red controls prove the check can actually fail:BootstrapSetupDto's three properties visible through the storage.PipeMetatypeCheckErrornaming all three properties.Reviewer check: confirm the
PIPE_GUARDED_DTOSentries carry constructor targets (target: BootstrapSetupDto, not.prototype), and that the executor-signature callgetTargetValidationMetadatas(target, '', true, false)mirrorsValidationExecutor.js:50.Verification
validation-pipe-check.spec.ts).main.tschange is one import + one call, first inbootstrap()— before tier detection, before app creation.Scope
One new file + spec + one-line bootstrap wiring. No DTO changes, no controller changes, no dependency changes. Extending the guard table (
PIPE_GUARDED_DTOS+REQUIRED_DECORATED_PROPERTIES) is the pattern for future DTOs.APPROVED at head
4e06bf3f(pinned). Reviewer rev-code-02 (usc); author code-infra-01 — Gate-16 clean. fred holds the merge on this verdict. CI note: pipeline 2667 still RUNNING at review-post time on this head; merge gate must see terminal green at4e06bf3f.== THE TWO REVIEWER CHECKS (both measured against the installed class-validator 0.15.1, not read from the PR) ==
(1) Guard-table targets are CONSTRUCTORS:
target: BootstrapSetupDto(no.prototypeanywhere in the table). I also measured the trap this must avoid, in this exact build: for a decorated class, the constructor-probe sees the property while the prototype-probe returns [] — a prototype-targeted guard would pass vacuously on every broken state. The PR's guard aims at the live half.(2) Executor-signature mirror: storage signature is (targetConstructor, targetSchema, always, strictGroups, groups); ValidationExecutor.js:50 passes (object.constructor, targetSchema, always, strictGroups, groups); the check calls (dto.target, '', true, false). Correct target kind, empty schema, strictGroups=false, groups undefined — and always=true is the conservative superset (conditional constraints included), which is what the whitelist should see.
== ARMS RUN BY ME ==
== DESIGN ==
Boot-time, first call in bootstrap() before tier detection/app creation (verified in the main.ts diff); pure function of module state; fail-loud error names each miss with cause and remediation; duplicate-class-validator-instance variant correctly excluded (globalThis-keyed storage, measured in #1391 comment 24082 — consistent with my storage observations). Spec's table-swap mutates the exported const but restores in finally and asserts the restore — acceptable in an isolated vitest worker.
== NOTES (non-blocking) ==
N1 — the check runs at every boot but only guards ONE DTO (BootstrapSetupDto). That is exactly the scope claimed (the #1391 symptom surface), and the table is the documented extension point; future DTOs must remember to extend it. Worth a one-line pointer in the admin DTO docs when convenient.
N2 — main.ts call site sits before BETTER_AUTH_SECRET validation; ordering is fine (fail-earliest), just noting the check runs even when the host would fail the secret check anyway.
Gate-16: author code-infra-01; reviewer rev-code-02.