chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import 'reflect-metadata';
|
||||
import { getMetadataStorage } from 'class-validator';
|
||||
import { BootstrapSetupDto } from './admin/bootstrap.dto.js';
|
||||
import {
|
||||
ChangeCompanyVisibilityDto,
|
||||
ChangeGrantDto,
|
||||
CreateCompanyDto,
|
||||
CreateEstateDto,
|
||||
CreateGrantDto,
|
||||
CreatePlatformProjectDto,
|
||||
DeleteNodeDto,
|
||||
RenameNodeDto,
|
||||
TransferEstateDto,
|
||||
TransferPlatformProjectDto,
|
||||
} from './hierarchy/hierarchy.dto.js';
|
||||
import {
|
||||
EnrollAgentDto,
|
||||
EnrollCredentialDto,
|
||||
GetEnrollmentQueryDto,
|
||||
} from './enrollment/enrollment.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'],
|
||||
},
|
||||
{
|
||||
name: 'CreateCompanyDto',
|
||||
target: CreateCompanyDto,
|
||||
properties: ['name', 'slug', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'RenameNodeDto',
|
||||
target: RenameNodeDto,
|
||||
properties: ['name', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'ChangeCompanyVisibilityDto',
|
||||
target: ChangeCompanyVisibilityDto,
|
||||
properties: ['visibility', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'DeleteNodeDto',
|
||||
target: DeleteNodeDto,
|
||||
properties: ['idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'CreateEstateDto',
|
||||
target: CreateEstateDto,
|
||||
properties: ['companyId', 'name', 'slug', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'CreatePlatformProjectDto',
|
||||
target: CreatePlatformProjectDto,
|
||||
properties: ['estateId', 'name', 'slug', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'TransferEstateDto',
|
||||
target: TransferEstateDto,
|
||||
properties: ['destinationCompanyId', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'TransferPlatformProjectDto',
|
||||
target: TransferPlatformProjectDto,
|
||||
properties: ['destinationEstateId', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'CreateGrantDto',
|
||||
target: CreateGrantDto,
|
||||
properties: ['userId', 'targetKind', 'targetId', 'role', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'ChangeGrantDto',
|
||||
target: ChangeGrantDto,
|
||||
properties: ['role', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'EnrollAgentDto',
|
||||
target: EnrollAgentDto,
|
||||
properties: [
|
||||
'harness',
|
||||
'name',
|
||||
'persona',
|
||||
'model',
|
||||
'provider',
|
||||
'credential',
|
||||
'idempotencyKey',
|
||||
'correlationId',
|
||||
'replayMode',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'EnrollCredentialDto',
|
||||
target: EnrollCredentialDto,
|
||||
properties: ['mode', 'type', 'value'],
|
||||
},
|
||||
{
|
||||
name: 'GetEnrollmentQueryDto',
|
||||
target: GetEnrollmentQueryDto,
|
||||
properties: ['correlationId'],
|
||||
},
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user