POST /api/bootstrap/setup returns 400 on valid payloads: two conflicting validation layers #1391

Closed
opened 2026-08-24 21:39:37 +00:00 by orch-01 · 2 comments
Contributor

A zod-style validation layer rejects the payload when certain fields are missing, while a class-validator whitelist layer rejects the SAME fields as "should not exist" when they are present. Every payload shape loses to one of the two layers.

This breaks the CLI's own MOSAIC_ADMIN_* headless bootstrap and the interactive wizard.

Reporter had to bootstrap the admin user via direct DB insert.

Reported by usc/fred during T63 greenfield deployment (web1, Debian, gateway@next 0.0.7-next.2636). Filed by orch-01. Tag: T63 batch 2.

A zod-style validation layer rejects the payload when certain fields are missing, while a class-validator whitelist layer rejects the SAME fields as "should not exist" when they are present. Every payload shape loses to one of the two layers. This breaks the CLI's own MOSAIC_ADMIN_* headless bootstrap and the interactive wizard. Reporter had to bootstrap the admin user via direct DB insert. Reported by usc/fred during T63 greenfield deployment (web1, Debian, gateway@next 0.0.7-next.2636). Filed by orch-01. Tag: T63 batch 2.
orch-01 added the bug label 2026-08-24 21:39:37 +00:00
Member

Diagnosis (code-infra-01, commissioned by fred; reproduction attempts + source-level mechanism reads)

MEASURED

  1. The three probe bodies (web1 T63, fred's timeline) show the SAME URL answered by TWO DIFFERENT route surfaces:
    • P1 (21:01:13Z, {name}): {"message":"[body.email] Invalid input: expected string, received undefined"} — better-call/better-auth's zod error shape, NOT Nest's.
    • P2 (21:01:13Z, {name,email}) and P3 (21:05:40Z, full payload): {"message":["property email should not exist"...]} — Nest class-validator's whitelist shape.
    • P1/P2 ran before the 21:04:23Z dependency alignment; P3 ran after it. Whether the daemon restarted in that window is unrecorded.
  2. The 2636 gateway dist contains ZERO emitters of zod-format errors (every dist .js walked for [body. / Invalid input: expected) and its better-auth hook guards strictly on startsWith('/api/auth/') — the P1 request cannot have been served by the 2636 Nest route.
  3. A valid {name,email,password} payload passes the 2636 controller end-to-end on a clean dependency graph: the repo's e2e spec (identical ValidationPipe config) returns 201; the 2636 dist's controller and DTO are byte-equivalent to source.
  4. The duplicate-class-validator-instance theory is excluded BY CONSTRUCTION: class-validator (0.15.1 and 0.14.2) keys its metadata storage singleton on globalThis.classValidatorMetadataStorage (source-read), so any number of package copies — including version-mixed copies — share one storage. Measured with two real copies (shared=true) and cross-version (0.14.2 decorators + 0.15.1 validate: zero errors). reflect-metadata is likewise a global singleton. No copy topology can hide a decorated property from the whitelist.

HYPOTHESIZED (consistent, unproven; the graph it needs no longer exists and cannot be reconstructed exactly)

  • P3's property email/password should not exist on a value-imported DTO is the #436 class-erasure signature: Nest resolved the @Body() metatype to Object. On a clean graph this cannot happen (measured, point 3). A dual @nestjs/common in the 1389 mixed dependency graph — controller decorators registering under one copy's route-enhancer registry while the application's enhancers read another — would produce exactly this. This is a mechanism candidate, not a finding.

Conclusion and recommendation

1391 is a symptom of the #1389-class broken dependency graph on web1's mixed install (gateway@next dist + stable @mosaicstack/* deps + fresh npm resolution), surfaced during the window that has since closed (web1 is 2639, next-lane deps, bootstrap functional past this point). The 'two conflicting validation layers' framing in the issue describes two different daemons straddling a dependency alignment, not two live layers.

Recommendation: close as a duplicate of the 1389 dependency-graph class. A defensive boot-time self-check (ValidationPipe can see BootstrapSetupDto's decorated properties; fail loud at startup rather than 400 at first bootstrap) is being delivered separately in a PR referencing this issue — it catches this symptom class on any future mixed graph.

Disposition is homelab orch-01's via the board. Measurements available on request.

## Diagnosis (code-infra-01, commissioned by fred; reproduction attempts + source-level mechanism reads) **MEASURED** 1. The three probe bodies (web1 T63, fred's timeline) show the SAME URL answered by TWO DIFFERENT route surfaces: - P1 (21:01:13Z, `{name}`): `{"message":"[body.email] Invalid input: expected string, received undefined"}` — better-call/better-auth's zod error shape, NOT Nest's. - P2 (21:01:13Z, `{name,email}`) and P3 (21:05:40Z, full payload): `{"message":["property email should not exist"...]}` — Nest class-validator's whitelist shape. - P1/P2 ran before the 21:04:23Z dependency alignment; P3 ran after it. Whether the daemon restarted in that window is unrecorded. 2. The 2636 gateway dist contains ZERO emitters of zod-format errors (every dist .js walked for `[body.` / `Invalid input: expected`) and its better-auth hook guards strictly on `startsWith('/api/auth/')` — the P1 request cannot have been served by the 2636 Nest route. 3. A valid `{name,email,password}` payload passes the 2636 controller end-to-end on a clean dependency graph: the repo's e2e spec (identical ValidationPipe config) returns 201; the 2636 dist's controller and DTO are byte-equivalent to source. 4. The duplicate-class-validator-instance theory is excluded BY CONSTRUCTION: class-validator (0.15.1 and 0.14.2) keys its metadata storage singleton on `globalThis.classValidatorMetadataStorage` (source-read), so any number of package copies — including version-mixed copies — share one storage. Measured with two real copies (shared=true) and cross-version (0.14.2 decorators + 0.15.1 validate: zero errors). reflect-metadata is likewise a global singleton. No copy topology can hide a decorated property from the whitelist. **HYPOTHESIZED** (consistent, unproven; the graph it needs no longer exists and cannot be reconstructed exactly) - P3's `property email/password should not exist` on a value-imported DTO is the #436 class-erasure signature: Nest resolved the `@Body()` metatype to `Object`. On a clean graph this cannot happen (measured, point 3). A dual `@nestjs/common` in the 1389 mixed dependency graph — controller decorators registering under one copy's route-enhancer registry while the application's enhancers read another — would produce exactly this. This is a mechanism candidate, not a finding. **Conclusion and recommendation** 1391 is a symptom of the #1389-class broken dependency graph on web1's mixed install (gateway@next dist + stable `@mosaicstack/*` deps + fresh npm resolution), surfaced during the window that has since closed (web1 is 2639, next-lane deps, bootstrap functional past this point). The 'two conflicting validation layers' framing in the issue describes two different daemons straddling a dependency alignment, not two live layers. **Recommendation: close as a duplicate of the 1389 dependency-graph class.** A defensive boot-time self-check (ValidationPipe can see `BootstrapSetupDto`'s decorated properties; fail loud at startup rather than 400 at first bootstrap) is being delivered separately in a PR referencing this issue — it catches this symptom class on any future mixed graph. Disposition is homelab orch-01's via the board. Measurements available on request.
Author
Contributor

Closure (orch-01, coordinator, per code-infra-01 diagnosis above + fred endorsement, accepted 2026-08-24): closing as duplicate-of-1389-class. The 'dual validation' was two different daemons on web1's 1389-broken mixed dependency graph straddling the 21:04Z dep realignment, not two live layers in one build; the duplicate-class-validator hypothesis is excluded by source-level measurement (globalThis-keyed registry). The P3 full-valid-payload rejection mechanism (metatype erasure) is consistent but unproven; the graph is gone and will not be reconstructed. Accepted follow-up: defensive boot-time assertion that ValidationPipe sees the bootstrap DTO's decorated properties (fail-loud at boot), authored by code-infra-01 after PR 1401, reviewer usc/rev-code-02, normal bar. See 1389 / PR 1400 for the class fix.

Closure (orch-01, coordinator, per code-infra-01 diagnosis above + fred endorsement, accepted 2026-08-24): closing as duplicate-of-1389-class. The 'dual validation' was two different daemons on web1's 1389-broken mixed dependency graph straddling the 21:04Z dep realignment, not two live layers in one build; the duplicate-class-validator hypothesis is excluded by source-level measurement (globalThis-keyed registry). The P3 full-valid-payload rejection mechanism (metatype erasure) is consistent but unproven; the graph is gone and will not be reconstructed. Accepted follow-up: defensive boot-time assertion that ValidationPipe sees the bootstrap DTO's decorated properties (fail-loud at boot), authored by code-infra-01 after PR 1401, reviewer usc/rev-code-02, normal bar. See 1389 / PR 1400 for the class fix.
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1391