@lenne.tech/nest-server 11.34.0 → 11.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/architecture.md +3 -1
- package/.claude/rules/configurable-features.md +3 -3
- package/.claude/rules/role-system.md +281 -0
- package/CLAUDE.md +11 -1
- package/FRAMEWORK-API.md +7 -3
- package/dist/core/common/decorators/nested-type.registry.d.ts +2 -0
- package/dist/core/common/decorators/nested-type.registry.js +17 -0
- package/dist/core/common/decorators/nested-type.registry.js.map +1 -0
- package/dist/core/common/decorators/restricted.decorator.d.ts +2 -1
- package/dist/core/common/decorators/restricted.decorator.js +52 -16
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/decorators/restrictions-checked.marker.d.ts +4 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js +30 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js.map +1 -0
- package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -1
- package/dist/core/common/decorators/unified-field.decorator.js +6 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/enums/role.enum.d.ts +8 -0
- package/dist/core/common/enums/role.enum.js +28 -1
- package/dist/core/common/enums/role.enum.js.map +1 -1
- package/dist/core/common/helpers/execution-context-request.helper.d.ts +7 -0
- package/dist/core/common/helpers/execution-context-request.helper.js +31 -0
- package/dist/core/common/helpers/execution-context-request.helper.js.map +1 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +15 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js +96 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -0
- package/dist/core/common/helpers/gridfs.helper.js +1 -1
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/helpers/validation-message.helper.d.ts +3 -0
- package/dist/core/common/helpers/validation-message.helper.js +41 -0
- package/dist/core/common/helpers/validation-message.helper.js.map +1 -0
- package/dist/core/common/interceptors/check-security.interceptor.js +2 -1
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interceptors/response-model.interceptor.js +3 -2
- package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
- package/dist/core/common/pipes/map-and-validate.pipe.js +16 -6
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/core/common/plugins/mongoose-system-role.plugin.d.ts +1 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js +106 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-tenant.plugin.js +134 -9
- package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
- package/dist/core/common/services/core-s3.service.js +4 -1
- package/dist/core/common/services/core-s3.service.js.map +1 -1
- package/dist/core/common/services/core-tenant-context.registry.d.ts +10 -0
- package/dist/core/common/services/core-tenant-context.registry.js +12 -0
- package/dist/core/common/services/core-tenant-context.registry.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +2 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -10
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +3 -1
- package/dist/core/modules/file/core-file.service.js +62 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-access.helper.d.ts +18 -0
- package/dist/core/modules/file/file-access.helper.js +46 -0
- package/dist/core/modules/file/file-access.helper.js.map +1 -0
- package/dist/core/modules/file/file-metadata.helper.js +1 -1
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -1
- package/dist/core/modules/file/file-roles.config.d.ts +10 -0
- package/dist/core/modules/file/file-roles.config.js +78 -0
- package/dist/core/modules/file/file-roles.config.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +2 -2
- package/dist/core/modules/file/file-roles.helper.js +8 -20
- package/dist/core/modules/file/file-roles.helper.js.map +1 -1
- package/dist/core/modules/tenant/core-role-scope.registry.d.ts +28 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js +93 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +5 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +37 -11
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +9 -1
- package/dist/core/modules/tenant/core-tenant.helpers.js +65 -4
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +5 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.service.js +25 -0
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.service.js +34 -4
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/index.d.ts +1 -0
- package/dist/core/modules/tus/index.js +1 -0
- package/dist/core/modules/tus/index.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +2 -1
- package/dist/core/modules/tus/tus.constants.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +5 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/inputs/core-user.input.js +8 -0
- package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
- package/dist/core.module.js +10 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/file/file.service.js +4 -1
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +26 -6
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +60 -1
- package/migration-guides/11.34.0-to-11.34.1.md +132 -0
- package/migration-guides/11.34.x-to-11.35.x.md +832 -0
- package/package.json +1 -1
- package/src/core/common/decorators/nested-type.registry.ts +42 -0
- package/src/core/common/decorators/restricted.decorator.ts +149 -21
- package/src/core/common/decorators/restrictions-checked.marker.ts +87 -0
- package/src/core/common/decorators/unified-field.decorator.ts +4 -3
- package/src/core/common/enums/role.enum.ts +116 -0
- package/src/core/common/helpers/execution-context-request.helper.ts +102 -0
- package/src/core/common/helpers/graphql-ws-context.helper.ts +192 -0
- package/src/core/common/helpers/gridfs.helper.ts +18 -2
- package/src/core/common/helpers/validation-message.helper.ts +83 -0
- package/src/core/common/interceptors/check-security.interceptor.ts +4 -1
- package/src/core/common/interceptors/response-model.interceptor.ts +3 -2
- package/src/core/common/interfaces/server-options.interface.ts +139 -7
- package/src/core/common/pipes/map-and-validate.pipe.ts +25 -7
- package/src/core/common/plugins/mongoose-system-role.plugin.ts +193 -0
- package/src/core/common/plugins/mongoose-tenant.plugin.ts +244 -12
- package/src/core/common/services/core-s3.service.ts +9 -1
- package/src/core/common/services/core-tenant-context.registry.ts +50 -0
- package/src/core/modules/auth/guards/roles.guard.ts +7 -5
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +7 -13
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +95 -1
- package/src/core/modules/file/README.md +61 -0
- package/src/core/modules/file/core-file.service.ts +166 -9
- package/src/core/modules/file/file-access.helper.ts +149 -0
- package/src/core/modules/file/file-metadata.helper.ts +13 -5
- package/src/core/modules/file/file-roles.config.ts +202 -0
- package/src/core/modules/file/file-roles.helper.ts +11 -40
- package/src/core/modules/tenant/README.md +20 -1
- package/src/core/modules/tenant/core-role-scope.registry.ts +195 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +92 -12
- package/src/core/modules/tenant/core-tenant.helpers.ts +139 -7
- package/src/core/modules/tenant/core-tenant.module.ts +12 -0
- package/src/core/modules/tenant/core-tenant.service.ts +56 -0
- package/src/core/modules/tus/README.md +46 -7
- package/src/core/modules/tus/core-tus.service.ts +94 -5
- package/src/core/modules/tus/index.ts +1 -0
- package/src/core/modules/tus/tus.constants.ts +17 -1
- package/src/core/modules/user/core-user.service.ts +12 -0
- package/src/core/modules/user/inputs/core-user.input.ts +28 -2
- package/src/core.module.ts +62 -1
- package/src/index.ts +3 -0
- package/src/server/modules/file/file.service.ts +45 -2
- package/src/test/test.helper.ts +54 -11
|
@@ -205,6 +205,7 @@ JWT-based authentication for existing projects:
|
|
|
205
205
|
| **Apollo Server** | Full GraphQL server with schema-first or code-first |
|
|
206
206
|
| **Custom Scalars** | `Date`, `DateTime` (timestamp), `JSON`, `Any` |
|
|
207
207
|
| **Subscriptions** | WebSocket support via `graphql-ws` with auth. The `PUB_SUB` provider is built from a factory: `CoreRedisPubSub` when `redis` is configured (delivery is then cluster-wide), the in-memory `PubSub` otherwise (delivery only to clients connected to the publishing replica). **Constraint once Redis is in play: every published payload must be JSON-serializable** — it crosses the wire as JSON, so `Date`, class instances, `Map`/`Set` and `undefined` do not survive the round trip. An in-process `PubSub` never had this constraint, so a payload that worked on one replica can silently lose fields on a cluster. Publish plain objects and ISO strings |
|
|
208
|
+
| **Subscription request context** | Since 11.35.0 `CoreModule` installs a context-aware `execute` / `subscribe` pair (inside `subscriptions`, where `ApolloDriver` forwards it) on all three GraphQL driver builders, so every WebSocket operation runs inside a `RequestContext`. Before that a WS operation had **none** — no Express middleware runs on an upgrade, and `CoreTenantGuard.getRequest()` finds no `req` on a subscription context — and `mongooseTenantPlugin` reads "no context" as "system operation, no filter", so a tenant-scoped read while delivering a subscription message returned EVERY tenant's rows. The tenant comes from the handshake's tenant header **validated against an active membership**, else from the subscriber's memberships; an unresolvable tenant leaves the safety net to refuse the read. The async iterator is wrapped too, not only the subscribe call: graphql-js runs `resolve` / `filter` / field resolvers inside its `next()`. See `.claude/rules/role-system.md` → "Tenant context on non-HTTP transports" |
|
|
208
209
|
| **Complexity Analysis** | Query cost calculation to prevent DoS attacks |
|
|
209
210
|
| **Enum Registration** | `registerEnum()` helper for GraphQL enum types |
|
|
210
211
|
| **Upload Support** | `graphqlUploadExpress()` for multipart file uploads |
|
|
@@ -1217,6 +1218,64 @@ export class User extends CorePersistenceModel {
|
|
|
1217
1218
|
}
|
|
1218
1219
|
```
|
|
1219
1220
|
|
|
1221
|
+
#### Nested data: only a DECLARED type is enforced (11.35.0+)
|
|
1222
|
+
|
|
1223
|
+
The check recurses, but it can only find metadata for a nested value whose class it knows. An embedded
|
|
1224
|
+
subdocument read out of MongoDB is a **plain object** (`CoreModel.map()` is a shallow `Object.assign`;
|
|
1225
|
+
`prepareOutput()` and `ResponseModelInterceptor` map the top level only), and `Object` carries no
|
|
1226
|
+
`@Restricted` metadata. Before 11.35.0 that meant every nested restriction silently evaluated to "no
|
|
1227
|
+
restrictions at all"; since 11.35.0 the nested type registry `@UnifiedField` fills is consulted, so a
|
|
1228
|
+
plain nested value is matched against the class its parent DECLARED — at every level, and for every
|
|
1229
|
+
item of a declared array.
|
|
1230
|
+
|
|
1231
|
+
```typescript
|
|
1232
|
+
@UnifiedField({ type: () => Insurance }) // enforced
|
|
1233
|
+
insurance?: Insurance;
|
|
1234
|
+
|
|
1235
|
+
@UnifiedField({ isArray: true, type: () => Insurance }) // enforced, every item
|
|
1236
|
+
insurances?: Insurance[];
|
|
1237
|
+
|
|
1238
|
+
@UnifiedField({ isAny: true }) // NOT reached — nothing declares the type
|
|
1239
|
+
extra?: any;
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
An undeclared nested type stays unchecked on purpose: such a value is just as legitimately free-form
|
|
1243
|
+
JSON, a `Map` or a scalar, and failing closed would strip far more than it protects. **If a nested
|
|
1244
|
+
field must be protected, declare its type.** A class-level `@Restricted` on a nested type strips the
|
|
1245
|
+
contents and leaves an empty container (the `checkObjectItself: false` default).
|
|
1246
|
+
|
|
1247
|
+
### File access — two layers, and why neither is optional
|
|
1248
|
+
|
|
1249
|
+
Files are the one resource whose authorization does NOT go through `CrudService.process()`, so the
|
|
1250
|
+
decorator model above does not reach them. GridFS is reached through the native MongoDB driver and S3
|
|
1251
|
+
through its own SDK, which means `mongooseTenantPlugin` never runs on a file store — and that is why
|
|
1252
|
+
`CoreFileController` and `CoreFileResolver` carry `@SkipTenantCheck()`.
|
|
1253
|
+
|
|
1254
|
+
| Layer | What it answers | Where |
|
|
1255
|
+
|-------|-----------------|-------|
|
|
1256
|
+
| `file.downloadRoles` / `uploadRoles` / `deleteRoles` | "may this caller reach the route at all" | applied onto the base-class members at boot by `applyFileRoles()`, read by the role guards |
|
|
1257
|
+
| `CoreFileService.checkRights()` | "…but only THIS file" | the service, once per operation, with `checkInputType` naming the path (`'id'`, `'filename'`, `'filterArgs'`, `'file'`, `'files'`) |
|
|
1258
|
+
|
|
1259
|
+
**The first layer cannot express the second.** The role names resolve against `user.roles` — a global
|
|
1260
|
+
attribute — never against `membership.role`, so no configuration can say "only their own tenant's
|
|
1261
|
+
files". That sentence needs data, and the data lives in the file's `metadata`.
|
|
1262
|
+
|
|
1263
|
+
**File ids are not secrets.** An ObjectId is 4 bytes of timestamp + 5 bytes of randomness generated once
|
|
1264
|
+
PER PROCESS + a 3-byte incrementing counter, so a caller who holds one valid id (their own upload) knows
|
|
1265
|
+
the random part and a counter reference point; neighbouring files sit on neighbouring values. The file
|
|
1266
|
+
routes are also not rate-limited by the framework. So a widened role gate without a per-file rule is
|
|
1267
|
+
practically enumerable, not theoretically.
|
|
1268
|
+
|
|
1269
|
+
Since 11.35.0 the second layer is a declaration rather than code — `file.access`, one value per project
|
|
1270
|
+
class (`'public'`, `'authenticated'`, `'owner'`, `'tenant'`, or `'custom'` to write your own, the
|
|
1271
|
+
default). `'owner'` / `'tenant'` also STAMP the metadata they decide on as the service writes. When the
|
|
1272
|
+
gate is widened past `ADMIN` and neither `file.access` nor an override declares a policy, the service
|
|
1273
|
+
warns at boot — the difference between a decision and an omission is the only thing a boot check can
|
|
1274
|
+
usefully detect.
|
|
1275
|
+
|
|
1276
|
+
Full model, per-driver behaviour and the audit checklist: `src/core/modules/file/README.md` § Access
|
|
1277
|
+
control and `src/core/modules/file/INTEGRATION-CHECKLIST.md`.
|
|
1278
|
+
|
|
1220
1279
|
### @UnifiedField() — Schema & Validation
|
|
1221
1280
|
|
|
1222
1281
|
Single decorator that replaces `@Field()`, `@ApiProperty()`, `@IsOptional()`, and more:
|
|
@@ -1459,7 +1518,7 @@ const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
|
|
|
1459
1518
|
|
|
1460
1519
|
// Exclude specific schemas from tenant filtering
|
|
1461
1520
|
multiTenancy: {
|
|
1462
|
-
excludeSchemas: [
|
|
1521
|
+
excludeSchemas: [], // model names, not collection names — this turns isolation OFF per model
|
|
1463
1522
|
}
|
|
1464
1523
|
```
|
|
1465
1524
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Migration Guide: 11.34.0 → 11.34.1
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None in signatures. One **client-visible text** change: validation error messages are now rendered the way class-validator itself renders them (§1, §2) |
|
|
8
|
+
| **Bugfixes** | A custom `message` in `ValidationOptions` is no longer ignored (§1). Default messages no longer reach API clients with raw placeholders such as `$constraint1` (§2) |
|
|
9
|
+
| **Migration Effort** | Nothing to configure. Read §1 and §2 if any test — or any frontend — asserts on exact validation error strings. Read §3 before you put `$value` in a custom message |
|
|
10
|
+
|
|
11
|
+
`MapAndValidatePipe` re-implements class-validator's validation executor, so it also has to render
|
|
12
|
+
error messages the way that executor does. It did neither: it built every message from the
|
|
13
|
+
constraint's `defaultMessage()` alone, and substituted only `$property`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Migration
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm update @lenne.tech/nest-server@11.34.1
|
|
21
|
+
pnpm run build
|
|
22
|
+
pnpm test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
No configuration change is required, and no source change is required in a typical project.
|
|
26
|
+
|
|
27
|
+
**Vendor-mode projects: this release ADDS a file, so the sync is atomic.**
|
|
28
|
+
`src/core/common/pipes/map-and-validate.pipe.ts` now imports the new
|
|
29
|
+
`src/core/common/helpers/validation-message.helper.ts`. Taking the pipe without the helper does not
|
|
30
|
+
fail a rights check or a test — it fails the build. Sync both files together.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. A custom `message` in `ValidationOptions` is now honored
|
|
35
|
+
|
|
36
|
+
Previously the pipe built the message from the constraint's own `defaultMessage()` and never looked
|
|
37
|
+
at `metadata.message`, so the message you wrote was silently discarded and the client received the
|
|
38
|
+
generic default instead.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
class UpdateUserInput {
|
|
42
|
+
@IsIn(['admin'], { each: true, message: 'roles may only contain assignable roles (admin)' })
|
|
43
|
+
@IsOptional()
|
|
44
|
+
roles?: string[];
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| | Before | After |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Response | `each value in roles must be one of the following values: $constraint1` | `roles may only contain assignable roles (admin)` |
|
|
51
|
+
|
|
52
|
+
Custom messages now take precedence over the constraint's default — the same order class-validator's
|
|
53
|
+
own executor uses. The function form (`message: (args) => string`) works too, and its result is
|
|
54
|
+
interpolated afterwards.
|
|
55
|
+
|
|
56
|
+
**What you may notice:** a custom message that was written but never delivered starts being
|
|
57
|
+
delivered. If a frontend or a test was written against the *default* text your project was
|
|
58
|
+
accidentally serving, it now sees the text the decorator actually asks for.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Default messages no longer leak raw placeholders
|
|
63
|
+
|
|
64
|
+
Only `$property` was substituted, so every other token reached the client verbatim:
|
|
65
|
+
|
|
66
|
+
| | Before | After |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Response | `each value in roles must be one of the following values: $constraint1` | `each value in roles must be one of the following values: alpha, beta` |
|
|
69
|
+
|
|
70
|
+
All four special tokens are now interpolated — `$constraint1..N`, `$value`, `$property` and
|
|
71
|
+
`$target` — rendered exactly as class-validator renders them (arrays joined with `, `, symbols by
|
|
72
|
+
their description).
|
|
73
|
+
|
|
74
|
+
**Scope of the text change:** 44 of class-validator's built-in messages carry a `$constraint` token,
|
|
75
|
+
so those 44 change text. None of the built-ins uses `$value` or `$target`, so no built-in message
|
|
76
|
+
changes *shape* — they stop showing a placeholder and start showing the value it stood for.
|
|
77
|
+
|
|
78
|
+
**What to check:** any test or frontend string comparison that asserts on a full validation message.
|
|
79
|
+
A test that was pinned to the broken output (`…: $constraint1`) fails and should be updated to the
|
|
80
|
+
interpolated text.
|
|
81
|
+
|
|
82
|
+
This also applies to `@IsDefined`, which since class-validator 0.13 is the one built-in that does not
|
|
83
|
+
register through `ValidateBy` and therefore took a separate code path with the same two gaps.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. Security: `$value` echoes the submitted value, and validation errors are not scrubbed
|
|
88
|
+
|
|
89
|
+
`$value` now works — which means a custom message containing it renders the value the client
|
|
90
|
+
submitted, straight back into the error response.
|
|
91
|
+
|
|
92
|
+
**Validation errors bypass the secret-field filtering you may be assuming.**
|
|
93
|
+
`security.secretFields` is applied by `CheckSecurityInterceptor` on the **response** path, whereas
|
|
94
|
+
the `BadRequestException` raised by the pipe goes to the exception filter and is forwarded verbatim.
|
|
95
|
+
The guard inside the renderer is a **type** guard (`boolean | number | string`), not a secrecy guard
|
|
96
|
+
— it will happily echo a password or a token.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// DANGEROUS — the rejected password is echoed back in the error response
|
|
100
|
+
@MinLength(12, { message: 'password "$value" is too short' })
|
|
101
|
+
password: string;
|
|
102
|
+
|
|
103
|
+
// SAFE — describe the rule, never the submitted value
|
|
104
|
+
@MinLength(12, { message: 'password must be at least $constraint1 characters' })
|
|
105
|
+
password: string;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
No built-in message uses `$value`, so reaching this is opt-in: it requires a custom message that
|
|
109
|
+
names the token. Note that class-validator's own `validate()` behaves identically — this is not a
|
|
110
|
+
new exposure introduced by the framework, but it becomes reachable in projects where `$value`
|
|
111
|
+
previously did nothing.
|
|
112
|
+
|
|
113
|
+
**Action:** grep your input classes for `$value` in custom messages and confirm none of them sits on
|
|
114
|
+
a credential, token, or otherwise sensitive field.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Under the hood
|
|
119
|
+
|
|
120
|
+
The token renderer is a port of class-validator internals
|
|
121
|
+
(`ValidationUtils.replaceMessageSpecialTokens`, `constraintToString`), which are not part of its
|
|
122
|
+
public API. Because vendor-mode projects resolve their own class-validator, that port can meet a
|
|
123
|
+
version this framework never installed. It is therefore pinned by an invariant test that compares it
|
|
124
|
+
against the installed implementation over a fixture table, so a divergence surfaces as a failing
|
|
125
|
+
test rather than as two renderers disagreeing about one decorator.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Module Documentation
|
|
130
|
+
|
|
131
|
+
- Request lifecycle and the validation pipe: [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md)
|
|
132
|
+
- Field decorators and validation: `src/core/common/decorators/unified-field.decorator.ts`
|