@lenne.tech/nest-server 11.21.3 → 11.22.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 +79 -0
- package/.claude/rules/better-auth.md +262 -0
- package/.claude/rules/configurable-features.md +308 -0
- package/.claude/rules/core-modules.md +205 -0
- package/.claude/rules/migration-guides.md +149 -0
- package/.claude/rules/module-deprecation.md +214 -0
- package/.claude/rules/module-inheritance.md +97 -0
- package/.claude/rules/package-management.md +112 -0
- package/.claude/rules/role-system.md +146 -0
- package/.claude/rules/testing.md +120 -0
- package/.claude/rules/versioning.md +53 -0
- package/CLAUDE.md +172 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +10 -0
- package/dist/core/modules/error-code/error-code.module.js.map +1 -1
- package/dist/core.module.d.ts +3 -3
- package/dist/core.module.js +17 -4
- package/dist/core.module.js.map +1 -1
- package/dist/server/server.module.js +6 -6
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1256 -0
- package/docs/error-codes.md +446 -0
- package/migration-guides/11.10.x-to-11.11.x.md +266 -0
- package/migration-guides/11.11.x-to-11.12.x.md +323 -0
- package/migration-guides/11.12.x-to-11.13.0.md +612 -0
- package/migration-guides/11.13.x-to-11.14.0.md +348 -0
- package/migration-guides/11.14.x-to-11.15.0.md +262 -0
- package/migration-guides/11.15.0-to-11.15.3.md +118 -0
- package/migration-guides/11.15.x-to-11.16.0.md +497 -0
- package/migration-guides/11.16.x-to-11.17.0.md +130 -0
- package/migration-guides/11.17.x-to-11.18.0.md +393 -0
- package/migration-guides/11.18.x-to-11.19.0.md +151 -0
- package/migration-guides/11.19.x-to-11.20.0.md +170 -0
- package/migration-guides/11.20.x-to-11.21.0.md +216 -0
- package/migration-guides/11.21.0-to-11.21.1.md +194 -0
- package/migration-guides/11.21.1-to-11.21.2.md +114 -0
- package/migration-guides/11.21.2-to-11.21.3.md +175 -0
- package/migration-guides/11.21.x-to-11.22.0.md +224 -0
- package/migration-guides/11.3.x-to-11.4.x.md +233 -0
- package/migration-guides/11.6.x-to-11.7.x.md +394 -0
- package/migration-guides/11.7.x-to-11.8.x.md +318 -0
- package/migration-guides/11.8.x-to-11.9.x.md +322 -0
- package/migration-guides/11.9.x-to-11.10.x.md +571 -0
- package/migration-guides/TEMPLATE.md +113 -0
- package/package.json +8 -3
- package/src/core/common/interfaces/server-options.interface.ts +83 -16
- package/src/core/modules/better-auth/CUSTOMIZATION.md +24 -17
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +5 -5
- package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +42 -12
- package/src/core/modules/error-code/error-code.module.ts +4 -9
- package/src/core.module.ts +52 -10
- package/src/server/server.module.ts +7 -9
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Migration Guide: 11.15.0 → 11.15.3
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | Cross-subdomain cookie Boolean Shorthand, middleware domain fix |
|
|
9
|
+
| **Bugfixes** | Fixed middleware not setting cookie domain, fixed `auto-install-peers` in `.npmrc` |
|
|
10
|
+
| **Migration Effort** | None required — update package only. New feature is opt-in. |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Update package
|
|
18
|
+
pnpm install @lenne.tech/nest-server@11.15.3
|
|
19
|
+
|
|
20
|
+
# Verify build
|
|
21
|
+
pnpm run build
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
pnpm test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No code changes required. All changes are backward compatible.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What's New in 11.15.3
|
|
32
|
+
|
|
33
|
+
### 1. Cross-Subdomain Cookie Boolean Shorthand
|
|
34
|
+
|
|
35
|
+
When your API and other services run on different subdomains (e.g., `api.example.com` and `ws.example.com`), authentication cookies are not shared by default. This feature provides a simple Boolean Shorthand to enable cross-subdomain cookie sharing.
|
|
36
|
+
|
|
37
|
+
**New recommended configuration:**
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// config.env.ts
|
|
41
|
+
const config = {
|
|
42
|
+
baseUrl: 'https://api.dev.example.com',
|
|
43
|
+
appUrl: 'https://dev.example.com',
|
|
44
|
+
betterAuth: {
|
|
45
|
+
crossSubDomainCookies: true, // Domain auto-derived → 'dev.example.com'
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Domain resolution order:** `appUrl` hostname → `baseUrl` hostname (with `api.` prefix stripped) → `baseUrl` hostname as-is.
|
|
51
|
+
|
|
52
|
+
**Replaces the verbose `options.advanced` passthrough:**
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// OLD (still works, but not recommended)
|
|
56
|
+
betterAuth: {
|
|
57
|
+
options: {
|
|
58
|
+
advanced: {
|
|
59
|
+
crossSubDomainCookies: { domain: 'example.com' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// NEW (recommended)
|
|
65
|
+
betterAuth: {
|
|
66
|
+
crossSubDomainCookies: true, // or { domain: 'example.com' }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Boolean Shorthand options:**
|
|
71
|
+
|
|
72
|
+
| Value | Behavior |
|
|
73
|
+
|-------|----------|
|
|
74
|
+
| `undefined` | Disabled (default, backward compatible) |
|
|
75
|
+
| `true` or `{}` | Enabled, domain auto-derived from `baseUrl` hostname |
|
|
76
|
+
| `{ domain: 'example.com' }` | Enabled with explicit domain |
|
|
77
|
+
| `false` or `{ enabled: false }` | Disabled |
|
|
78
|
+
|
|
79
|
+
**What this does:**
|
|
80
|
+
- Sets the `domain` attribute on all authentication cookies (`iam.session_token`, and `token` if Legacy Auth is active)
|
|
81
|
+
- Enables cookie sharing between subdomains (e.g., `api.example.com` ↔ `ws.example.com`)
|
|
82
|
+
- Configures both Better-Auth's native handler AND nest-server's own cookie helper (controller + middleware)
|
|
83
|
+
- Automatically disabled for localhost (not meaningful for cross-subdomain)
|
|
84
|
+
|
|
85
|
+
**Important:** `crossSubDomainCookies` handles only the **cookie domain** (browser sends cookies to subdomains). Better-Auth also validates request origins via `trustedOrigins` (server accepts requests from subdomains). Both are required. `trustedOrigins` is auto-derived from `baseUrl`/`appUrl`, but additional subdomains (e.g., `https://ws.example.com`) must be added explicitly via `trustedOrigins`.
|
|
86
|
+
|
|
87
|
+
**Documentation:** [Better-Auth README — Cross-Subdomain Cookies](../src/core/modules/better-auth/README.md#cross-subdomain-cookies-v11151)
|
|
88
|
+
|
|
89
|
+
### 2. Middleware Cookie Domain Fix
|
|
90
|
+
|
|
91
|
+
**Bug:** The API middleware (`CoreBetterAuthApiMiddleware`) did not read the cookie domain from the config, causing cookies set by the middleware (e.g., after Passkey authentication) to lack the `domain` attribute even when cross-subdomain cookies were configured.
|
|
92
|
+
|
|
93
|
+
**Fix:** The middleware now reads the domain via `CoreBetterAuthService.getCookieDomain()`, consistent with the controller.
|
|
94
|
+
|
|
95
|
+
### 3. Fixed `auto-install-peers` in `.npmrc`
|
|
96
|
+
|
|
97
|
+
Added `auto-install-peers=false` to `.npmrc` to prevent pnpm from automatically installing peer dependencies. This ensures consistent lockfile generation and aligns with the project's fixed-version package management strategy.
|
|
98
|
+
|
|
99
|
+
This fix is internal to the package development workflow and does not affect consuming projects.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Compatibility Notes
|
|
104
|
+
|
|
105
|
+
- All existing configurations continue to work without changes
|
|
106
|
+
- The `options.advanced` passthrough still works as before — the new `crossSubDomainCookies` top-level property is an alternative, not a replacement
|
|
107
|
+
- When both `crossSubDomainCookies` (top-level) and `options.advanced.crossSubDomainCookies` are set, the top-level value is applied first, then `options.advanced` is deep-merged on top (so the passthrough wins)
|
|
108
|
+
- No changes to authentication behavior unless `crossSubDomainCookies` is explicitly configured
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Module Documentation
|
|
113
|
+
|
|
114
|
+
### Better-Auth Module
|
|
115
|
+
|
|
116
|
+
- **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
|
|
117
|
+
- **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
|
|
118
|
+
- **Reference Implementation:** `src/server/modules/auth/`
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# Migration Guide: 11.15.x → 11.16.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | `IServerOptions.security.mapAndValidatePipe` type changed from `boolean` to `boolean \| { nonWhitelistedFields?: 'strip' \| 'error' \| false }` (TypeScript only — runtime is backward compatible). Input properties without `@UnifiedField` are now **silently stripped** by default. |
|
|
8
|
+
| **New Features** | `@UnifiedField({ exclude })` option, automatic input property whitelisting, new error code `LTNS_0303` |
|
|
9
|
+
| **Bugfixes** | None |
|
|
10
|
+
| **Migration Effort** | Low (~5-15 minutes) — update package, migrate any `@Field`-decorated input properties to `@UnifiedField`, verify build |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Update package
|
|
18
|
+
npm install @lenne.tech/nest-server@11.16.1
|
|
19
|
+
|
|
20
|
+
# Verify build
|
|
21
|
+
npm run build
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
npm test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **Important:** If your input classes mix `@Field`/`@IsOptional` with `@UnifiedField` (from parent classes), those `@Field`-only properties will now be silently stripped from REST requests. See [Migration Steps](#detailed-migration-steps) below.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What's New in 11.16.0
|
|
32
|
+
|
|
33
|
+
### 1. Automatic Input Property Whitelisting
|
|
34
|
+
|
|
35
|
+
The `MapAndValidatePipe` now automatically handles input properties that are **not** decorated with `@UnifiedField`. This closes a security gap where properties without `@UnifiedField` (e.g. `type`, `id`, `roles`) were silently accepted in REST request bodies.
|
|
36
|
+
|
|
37
|
+
**How it works:**
|
|
38
|
+
|
|
39
|
+
1. When a request arrives, the pipe checks which properties are decorated with `@UnifiedField` on the input class (including inherited fields).
|
|
40
|
+
2. Any property **not** in the whitelist is handled according to the configured mode.
|
|
41
|
+
3. The check applies recursively to nested objects and arrays.
|
|
42
|
+
|
|
43
|
+
**Three modes available:**
|
|
44
|
+
|
|
45
|
+
| Mode | Behavior | Use Case |
|
|
46
|
+
|------|----------|----------|
|
|
47
|
+
| `'strip'` (default) | Silently removes non-whitelisted properties | Production — tolerant to extra fields |
|
|
48
|
+
| `'error'` | Throws `BadRequestException` (`LTNS_0303`) | Development — strict validation, helps find issues |
|
|
49
|
+
| `false` | Disabled — all properties pass through | Legacy behavior, backward compatible |
|
|
50
|
+
|
|
51
|
+
**Configuration:**
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// config.env.ts
|
|
55
|
+
|
|
56
|
+
// Default behavior (strip) — no config change needed
|
|
57
|
+
security: {
|
|
58
|
+
mapAndValidatePipe: true,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Explicit strip (same as default)
|
|
62
|
+
security: {
|
|
63
|
+
mapAndValidatePipe: {
|
|
64
|
+
nonWhitelistedFields: 'strip',
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Error mode — throws BadRequestException for non-whitelisted properties
|
|
69
|
+
security: {
|
|
70
|
+
mapAndValidatePipe: {
|
|
71
|
+
nonWhitelistedFields: 'error',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Disable whitelist check entirely (legacy behavior)
|
|
76
|
+
security: {
|
|
77
|
+
mapAndValidatePipe: {
|
|
78
|
+
nonWhitelistedFields: false,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Scope of the check:**
|
|
84
|
+
- Only applies to classes with **at least one** `@UnifiedField` decorator (directly or inherited)
|
|
85
|
+
- Classes without any `@UnifiedField` are skipped entirely (e.g. pure `class-validator` classes)
|
|
86
|
+
- Recursion into nested objects uses `nestedTypeRegistry` (populated by `@UnifiedField({ type: () => NestedType })`)
|
|
87
|
+
- Nested objects without a registry entry are not recursed into (only top-level keys are checked)
|
|
88
|
+
|
|
89
|
+
**What is excluded from the check:**
|
|
90
|
+
- **Custom decorator parameters** (`@CurrentUser()`, `@RESTServiceOptions()`, `@GraphQLServiceOptions()`, etc.) are always passed through unchanged — they inject framework values, not user input, and must never be mutated by the pipe
|
|
91
|
+
- **Basic types** (`String`, `Number`, `Boolean`, `Array`, `Object`, `Buffer`, `ArrayBuffer`) cause an early return — no validation and no whitelist check (e.g. `@Param('id') id: string`)
|
|
92
|
+
|
|
93
|
+
### 2. `@UnifiedField({ exclude })` Option
|
|
94
|
+
|
|
95
|
+
New option to explicitly control whether a property is included in the input whitelist. This enables inheritance-safe field exclusion.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Exclude a property — rejected/stripped from input, hidden from GraphQL & Swagger
|
|
99
|
+
@UnifiedField({ exclude: true })
|
|
100
|
+
override type?: TypeEnum;
|
|
101
|
+
|
|
102
|
+
// Explicitly re-enable a property excluded by a parent class
|
|
103
|
+
@UnifiedField({ exclude: false, description: 'Type override', isOptional: true })
|
|
104
|
+
override type?: TypeEnum;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**When `exclude: true` is set:**
|
|
108
|
+
- Property is registered in `EXCLUDED_FIELD_KEYS` metadata
|
|
109
|
+
- `@HideField()` (GraphQL) and `@ApiHideProperty()` (Swagger) are applied automatically
|
|
110
|
+
- No other decorators (`@Field`, `@ApiProperty`, validators) are applied
|
|
111
|
+
- Property is removed from the whitelist → stripped/rejected at runtime
|
|
112
|
+
|
|
113
|
+
**When `exclude: false` is set:**
|
|
114
|
+
- Property is registered in `FORCE_INCLUDED_FIELD_KEYS` metadata
|
|
115
|
+
- Can override a parent's `exclude: true` (explicit re-enable)
|
|
116
|
+
- Normal `@UnifiedField` decorators are applied (`@Field`, `@ApiProperty`, validators)
|
|
117
|
+
|
|
118
|
+
### 3. Inheritance Priority Model
|
|
119
|
+
|
|
120
|
+
The whitelist system uses a 3-level priority model when resolving fields across class hierarchies:
|
|
121
|
+
|
|
122
|
+
| Priority | Setting | Behavior |
|
|
123
|
+
|----------|---------|----------|
|
|
124
|
+
| 1 (highest) | `exclude: true` / `exclude: false` | Explicit — resolved by nearest class (child wins) |
|
|
125
|
+
| 2 | `@UnifiedField()` (no `exclude`) | Implicit inclusion — **cannot** override explicit `exclude: true` |
|
|
126
|
+
| 3 (lowest) | No decorator | Not in whitelist |
|
|
127
|
+
|
|
128
|
+
**Key rule:** An implicit `@UnifiedField()` in a child class **cannot** override a parent's `exclude: true`. Only `@UnifiedField({ exclude: false })` can re-enable an excluded field.
|
|
129
|
+
|
|
130
|
+
#### Full Inheritance Table
|
|
131
|
+
|
|
132
|
+
| Parent Decorator | Child Decorator | Field in Parent Input | Field in Child Input |
|
|
133
|
+
|-----------------|-----------------|----------------------|---------------------|
|
|
134
|
+
| `@UnifiedField()` | _(inherited, no override)_ | ACCEPTED | ACCEPTED |
|
|
135
|
+
| `@UnifiedField()` | `@UnifiedField()` | ACCEPTED | ACCEPTED |
|
|
136
|
+
| `@UnifiedField()` | `@UnifiedField({ exclude: true })` | ACCEPTED | **REJECTED** |
|
|
137
|
+
| `@UnifiedField()` | `@UnifiedField({ exclude: false })` | ACCEPTED | ACCEPTED |
|
|
138
|
+
| `@UnifiedField({ exclude: true })` | _(inherited, no override)_ | REJECTED | REJECTED |
|
|
139
|
+
| `@UnifiedField({ exclude: true })` | `@UnifiedField()` _(implicit)_ | REJECTED | **REJECTED** |
|
|
140
|
+
| `@UnifiedField({ exclude: true })` | `@UnifiedField({ exclude: true })` | REJECTED | REJECTED |
|
|
141
|
+
| `@UnifiedField({ exclude: true })` | `@UnifiedField({ exclude: false })` | REJECTED | **ACCEPTED** |
|
|
142
|
+
| `@UnifiedField({ exclude: false })` | _(inherited, no override)_ | ACCEPTED | ACCEPTED |
|
|
143
|
+
| `@UnifiedField({ exclude: false })` | `@UnifiedField({ exclude: true })` | ACCEPTED | **REJECTED** |
|
|
144
|
+
| _(no decorator)_ | `@UnifiedField()` | — | ACCEPTED |
|
|
145
|
+
| _(no decorator)_ | `@UnifiedField({ exclude: true })` | — | REJECTED |
|
|
146
|
+
| _(no decorator)_ | `@UnifiedField({ exclude: false })` | — | ACCEPTED |
|
|
147
|
+
|
|
148
|
+
> **Reading the table:** "Parent Input" = behavior when the parent class is used as input type. "Child Input" = behavior when the child class is used as input type.
|
|
149
|
+
|
|
150
|
+
#### Practical Example
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// CoreUserInput (from nest-server)
|
|
154
|
+
class CoreUserInput extends CoreInput {
|
|
155
|
+
@UnifiedField({ description: 'Email', isOptional: true })
|
|
156
|
+
email?: string; // ← ACCEPTED everywhere
|
|
157
|
+
|
|
158
|
+
@UnifiedField({ description: 'First name', isOptional: true })
|
|
159
|
+
firstName?: string; // ← ACCEPTED everywhere
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// UserInput (your project) — exclude 'type' from REST input
|
|
163
|
+
class UserInput extends CoreUserInput {
|
|
164
|
+
@UnifiedField({ exclude: true })
|
|
165
|
+
override type?: TypeEnum; // ← REJECTED: stripped/error in REST
|
|
166
|
+
|
|
167
|
+
@UnifiedField({ description: 'Job Title', isOptional: true })
|
|
168
|
+
jobTitle?: string; // ← ACCEPTED: new field in child
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// AdminUserInput (special override) — re-enable 'type' for admins
|
|
172
|
+
class AdminUserInput extends UserInput {
|
|
173
|
+
@UnifiedField({ exclude: false, description: 'Type', isOptional: true })
|
|
174
|
+
override type?: TypeEnum; // ← ACCEPTED: explicit re-enable
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 4. New Error Code: LTNS_0303
|
|
179
|
+
|
|
180
|
+
Available when using `nonWhitelistedFields: 'error'` mode:
|
|
181
|
+
|
|
182
|
+
| Code | Message | Translation (DE) | Translation (EN) |
|
|
183
|
+
|------|---------|-----------------|-----------------|
|
|
184
|
+
| `LTNS_0303` | Non-whitelisted properties found | Die folgenden Eigenschaften sind nicht erlaubt: {{properties}}. Nur mit @UnifiedField dekorierte Eigenschaften werden akzeptiert. | The following properties are not allowed: {{properties}}. Only properties decorated with @UnifiedField are accepted. |
|
|
185
|
+
|
|
186
|
+
Error response format:
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"statusCode": 400,
|
|
190
|
+
"message": "#LTNS_0303: Non-whitelisted properties found [evilProp, nested.badField]"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 5. Nested Object & Array Support
|
|
195
|
+
|
|
196
|
+
The whitelist check recurses into nested objects and arrays:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
class AddressInput {
|
|
200
|
+
@UnifiedField({ description: 'Street', isOptional: true })
|
|
201
|
+
street?: string;
|
|
202
|
+
|
|
203
|
+
@UnifiedField({ description: 'City', isOptional: true })
|
|
204
|
+
city?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
class UserInput extends CoreUserInput {
|
|
208
|
+
@UnifiedField({ description: 'Address', isOptional: true, type: () => AddressInput })
|
|
209
|
+
address?: AddressInput;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Request body:
|
|
213
|
+
{
|
|
214
|
+
"firstName": "John",
|
|
215
|
+
"address": {
|
|
216
|
+
"street": "Main St",
|
|
217
|
+
"city": "Berlin",
|
|
218
|
+
"malicious": "injected" // ← stripped (or error: "address.malicious")
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
For arrays, each element is checked individually:
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"items": [
|
|
227
|
+
{ "name": "ok" },
|
|
228
|
+
{ "name": "ok", "evil": "hack" } // ← items[1].evil stripped/rejected
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Breaking Changes
|
|
236
|
+
|
|
237
|
+
### Properties Without `@UnifiedField` Are Now Stripped
|
|
238
|
+
|
|
239
|
+
**This is the most important change.** If your input classes have properties decorated with `@Field` + `@IsOptional` (the old pattern) but inherit from a parent that uses `@UnifiedField`, those properties will be silently stripped from REST requests.
|
|
240
|
+
|
|
241
|
+
**Before (11.15.x):**
|
|
242
|
+
```typescript
|
|
243
|
+
// This worked — jobTitle was accepted via REST
|
|
244
|
+
@InputType()
|
|
245
|
+
export class UserInput extends CoreUserInput {
|
|
246
|
+
@Field(() => String, { nullable: true })
|
|
247
|
+
@IsOptional()
|
|
248
|
+
jobTitle?: string;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**After (11.16.0):**
|
|
253
|
+
```typescript
|
|
254
|
+
// jobTitle is now stripped because CoreUserInput uses @UnifiedField
|
|
255
|
+
// and jobTitle is not decorated with @UnifiedField
|
|
256
|
+
|
|
257
|
+
// Fix: migrate to @UnifiedField
|
|
258
|
+
@InputType()
|
|
259
|
+
export class UserInput extends CoreUserInput {
|
|
260
|
+
@UnifiedField({ description: 'Job Title', isOptional: true })
|
|
261
|
+
jobTitle?: string;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### TypeScript Interface Change: `mapAndValidatePipe`
|
|
266
|
+
|
|
267
|
+
The `mapAndValidatePipe` property type in `IServerOptions.security` changed from:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Before (11.15.x)
|
|
271
|
+
mapAndValidatePipe?: boolean;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
To:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// After (11.16.0)
|
|
278
|
+
mapAndValidatePipe?:
|
|
279
|
+
| boolean
|
|
280
|
+
| {
|
|
281
|
+
nonWhitelistedFields?: 'strip' | 'error' | false;
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Impact:** This is a **TypeScript-only** change. All existing `boolean` configurations remain valid. Code that directly accesses `mapAndValidatePipe` as a boolean may need type narrowing.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Detailed Migration Steps
|
|
290
|
+
|
|
291
|
+
### Step 1: Update Package
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npm install @lenne.tech/nest-server@11.16.1
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Step 2: Find Input Properties Using Old `@Field` Pattern
|
|
298
|
+
|
|
299
|
+
Search for input classes that extend Core* classes but use `@Field` instead of `@UnifiedField`:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Find input files using @Field directly
|
|
303
|
+
grep -rn "@Field(" src/server/modules/*/inputs/ --include="*.ts"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Step 3: Migrate `@Field` Properties to `@UnifiedField`
|
|
307
|
+
|
|
308
|
+
For each property found:
|
|
309
|
+
|
|
310
|
+
**Before:**
|
|
311
|
+
```typescript
|
|
312
|
+
import { Field, InputType } from '@nestjs/graphql';
|
|
313
|
+
import { IsOptional } from 'class-validator';
|
|
314
|
+
import { Restricted } from '@lenne.tech/nest-server';
|
|
315
|
+
|
|
316
|
+
@Field(() => String, { description: 'Job Title', nullable: true })
|
|
317
|
+
@IsOptional()
|
|
318
|
+
@Restricted(RoleEnum.ADMIN)
|
|
319
|
+
jobTitle?: string;
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**After:**
|
|
323
|
+
```typescript
|
|
324
|
+
import { UnifiedField } from '@lenne.tech/nest-server';
|
|
325
|
+
|
|
326
|
+
@UnifiedField({
|
|
327
|
+
description: 'Job Title',
|
|
328
|
+
isOptional: true,
|
|
329
|
+
roles: RoleEnum.ADMIN,
|
|
330
|
+
})
|
|
331
|
+
jobTitle?: string;
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Mapping table:**
|
|
335
|
+
|
|
336
|
+
| Old Pattern | `@UnifiedField` Equivalent |
|
|
337
|
+
|------------|---------------------------|
|
|
338
|
+
| `@Field(() => String, { nullable: true })` | `isOptional: true` |
|
|
339
|
+
| `@Field(() => [String])` | `isArray: true, type: () => String` |
|
|
340
|
+
| `@IsOptional()` | `isOptional: true` |
|
|
341
|
+
| `@IsEnum(MyEnum)` | `enum: { enum: MyEnum }` |
|
|
342
|
+
| `@Restricted(RoleEnum.ADMIN)` | `roles: RoleEnum.ADMIN` |
|
|
343
|
+
| `@ValidateNested()` + `@Type(() => X)` | `type: () => X` (automatic) |
|
|
344
|
+
|
|
345
|
+
### Step 4: Exclude Properties That Should Not Be Set via Input
|
|
346
|
+
|
|
347
|
+
If you have properties that should never be set via REST (e.g. `type`, `roles`, `id`):
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
@UnifiedField({ exclude: true })
|
|
351
|
+
override type?: TypeEnum;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Step 5: Verify Build and Tests
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm run build
|
|
358
|
+
npm test
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Step 6: Choose Whitelist Mode (Optional)
|
|
362
|
+
|
|
363
|
+
For development/staging, consider using `'error'` mode to catch issues early:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// config.env.ts (development only)
|
|
367
|
+
security: {
|
|
368
|
+
mapAndValidatePipe: {
|
|
369
|
+
nonWhitelistedFields: 'error',
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Compatibility Notes
|
|
377
|
+
|
|
378
|
+
### Existing Projects Using `mapAndValidatePipe: true`
|
|
379
|
+
|
|
380
|
+
No configuration change needed. The default `'strip'` mode activates automatically.
|
|
381
|
+
|
|
382
|
+
### Existing Projects Using `mapAndValidatePipe: false`
|
|
383
|
+
|
|
384
|
+
The pipe is not registered at all, so no whitelist check runs. No change needed.
|
|
385
|
+
|
|
386
|
+
### Projects Without `@UnifiedField`
|
|
387
|
+
|
|
388
|
+
If your input classes do not use `@UnifiedField` at all (neither directly nor inherited), the whitelist check is skipped entirely (`whitelistedKeys.size === 0` → no check). This ensures backward compatibility for pure `class-validator` setups.
|
|
389
|
+
|
|
390
|
+
### GraphQL vs REST
|
|
391
|
+
|
|
392
|
+
The whitelist check runs in `MapAndValidatePipe`, which processes both REST and GraphQL inputs. However, GraphQL already enforces its schema strictly — unknown fields cause GraphQL validation errors before the pipe runs. The primary benefit is for **REST endpoints** where JSON bodies accept arbitrary properties.
|
|
393
|
+
|
|
394
|
+
### `@HideField` and `@ApiHideProperty` with `exclude: true`
|
|
395
|
+
|
|
396
|
+
When using `@UnifiedField({ exclude: true })`, the decorator automatically applies `@HideField()` (GraphQL) and `@ApiHideProperty()` (Swagger). This means the field is hidden from both the GraphQL schema and the Swagger documentation, in addition to being stripped/rejected at runtime.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Troubleshooting
|
|
401
|
+
|
|
402
|
+
### Property Suddenly Returning `null` After Update
|
|
403
|
+
|
|
404
|
+
**Cause:** The property uses `@Field` instead of `@UnifiedField`, and its parent class uses `@UnifiedField`. The property is now being stripped.
|
|
405
|
+
|
|
406
|
+
**Solution:** Migrate the property to `@UnifiedField`:
|
|
407
|
+
```typescript
|
|
408
|
+
// Before
|
|
409
|
+
@Field(() => String, { nullable: true })
|
|
410
|
+
@IsOptional()
|
|
411
|
+
jobTitle?: string;
|
|
412
|
+
|
|
413
|
+
// After
|
|
414
|
+
@UnifiedField({ description: 'Job Title', isOptional: true })
|
|
415
|
+
jobTitle?: string;
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### `BadRequestException` with `LTNS_0303` in Production
|
|
419
|
+
|
|
420
|
+
**Cause:** `nonWhitelistedFields` is set to `'error'` and the client sends extra properties.
|
|
421
|
+
|
|
422
|
+
**Solution:** Either:
|
|
423
|
+
1. Fix the client to only send whitelisted properties
|
|
424
|
+
2. Switch to `'strip'` mode (default): `nonWhitelistedFields: 'strip'`
|
|
425
|
+
|
|
426
|
+
### Child Class Cannot Override Parent's `exclude: true`
|
|
427
|
+
|
|
428
|
+
**Cause:** Using implicit `@UnifiedField()` (without `exclude: false`) cannot override a parent's `exclude: true`.
|
|
429
|
+
|
|
430
|
+
**Solution:** Use explicit `@UnifiedField({ exclude: false })`:
|
|
431
|
+
```typescript
|
|
432
|
+
// This does NOT work — implicit cannot override
|
|
433
|
+
@UnifiedField({ description: 'Type', isOptional: true })
|
|
434
|
+
override type?: TypeEnum; // Still rejected!
|
|
435
|
+
|
|
436
|
+
// This works — explicit re-enable
|
|
437
|
+
@UnifiedField({ exclude: false, description: 'Type', isOptional: true })
|
|
438
|
+
override type?: TypeEnum; // Accepted
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Nested Object Properties Not Being Checked
|
|
442
|
+
|
|
443
|
+
**Cause:** The nested type is not registered in `nestedTypeRegistry`. This happens when using `Record<string, any>` or `object` type without specifying `type: () => NestedClass`.
|
|
444
|
+
|
|
445
|
+
**Solution:** Use explicit type in `@UnifiedField`:
|
|
446
|
+
```typescript
|
|
447
|
+
// No recursive check (metadata is Record<string, any>)
|
|
448
|
+
@UnifiedField({ description: 'Metadata', isOptional: true })
|
|
449
|
+
metadata?: Record<string, any>;
|
|
450
|
+
|
|
451
|
+
// Recursive check enabled
|
|
452
|
+
@UnifiedField({ description: 'Address', isOptional: true, type: () => AddressInput })
|
|
453
|
+
address?: AddressInput;
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Module Documentation
|
|
459
|
+
|
|
460
|
+
### MapAndValidatePipe (Updated)
|
|
461
|
+
|
|
462
|
+
- **Key File:** [src/core/common/pipes/map-and-validate.pipe.ts](../src/core/common/pipes/map-and-validate.pipe.ts)
|
|
463
|
+
- **Changes:**
|
|
464
|
+
- `processNonWhitelistedFields()` — recursive whitelist enforcement
|
|
465
|
+
- `nonWhitelistedFieldsMode` — configurable via `ConfigService`
|
|
466
|
+
- New import of `ErrorCode` for `LTNS_0303` error messages
|
|
467
|
+
|
|
468
|
+
### UnifiedField Decorator (Updated)
|
|
469
|
+
|
|
470
|
+
- **Key File:** [src/core/common/decorators/unified-field.decorator.ts](../src/core/common/decorators/unified-field.decorator.ts)
|
|
471
|
+
- **Changes:**
|
|
472
|
+
- New symbols: `UNIFIED_FIELD_KEYS`, `EXCLUDED_FIELD_KEYS`, `FORCE_INCLUDED_FIELD_KEYS`
|
|
473
|
+
- New option: `exclude?: boolean`
|
|
474
|
+
- New function: `getUnifiedFieldKeys(metatype)` — resolves whitelist with inheritance
|
|
475
|
+
- `exclude: true` applies `@HideField()` + `@ApiHideProperty()` automatically
|
|
476
|
+
|
|
477
|
+
### Error Codes (Updated)
|
|
478
|
+
|
|
479
|
+
- **Key File:** [src/core/modules/error-code/error-codes.ts](../src/core/modules/error-code/error-codes.ts)
|
|
480
|
+
- **Documentation:** [docs/error-codes.md](../docs/error-codes.md)
|
|
481
|
+
- **Changes:** New error code `LTNS_0303: NON_WHITELISTED_PROPERTIES`
|
|
482
|
+
|
|
483
|
+
### Unit Tests
|
|
484
|
+
|
|
485
|
+
- **Key File:** [tests/unit/unified-field-whitelist.spec.ts](../tests/unit/unified-field-whitelist.spec.ts)
|
|
486
|
+
- **Coverage:** 49 tests covering all modes, inheritance, nesting, config variants, custom decorator parameters, edge cases
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## References
|
|
491
|
+
|
|
492
|
+
- [IServerOptions Interface](../src/core/common/interfaces/server-options.interface.ts) — Updated `mapAndValidatePipe` type
|
|
493
|
+
- [UnifiedField Decorator](../src/core/common/decorators/unified-field.decorator.ts) — `exclude` option and metadata tracking
|
|
494
|
+
- [MapAndValidatePipe](../src/core/common/pipes/map-and-validate.pipe.ts) — Whitelist enforcement implementation
|
|
495
|
+
- [Error Codes](../src/core/modules/error-code/error-codes.ts) — `LTNS_0303` definition
|
|
496
|
+
- [Unit Tests](../tests/unit/unified-field-whitelist.spec.ts) — Comprehensive test coverage
|
|
497
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) — Reference implementation
|