@lenne.tech/nest-server 11.21.2 → 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/better-auth/core-better-auth-user.mapper.js +25 -25
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +8 -4
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-code.module.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +59 -4
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.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/test/test.helper.d.ts +6 -2
- package/dist/test/test.helper.js +28 -6
- package/dist/test/test.helper.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/better-auth/core-better-auth-user.mapper.ts +29 -25
- package/src/core/modules/better-auth/core-better-auth.service.ts +13 -9
- 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/modules/tenant/INTEGRATION-CHECKLIST.md +13 -2
- package/src/core/modules/tenant/README.md +26 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +142 -11
- package/src/core/modules/tenant/core-tenant.helpers.ts +6 -2
- package/src/core.module.ts +52 -10
- package/src/server/server.module.ts +7 -9
- package/src/test/README.md +47 -0
- package/src/test/test.helper.ts +55 -6
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Code Architecture
|
|
2
|
+
|
|
3
|
+
## Framework Stack
|
|
4
|
+
|
|
5
|
+
- **NestJS** - Server framework
|
|
6
|
+
- **GraphQL** - API layer (Apollo Server)
|
|
7
|
+
- **MongoDB** - Database (Mongoose ODM)
|
|
8
|
+
|
|
9
|
+
## Two-Layer Structure
|
|
10
|
+
|
|
11
|
+
1. **Core Layer** (`src/core/`) - Reusable framework components (exported to consumers)
|
|
12
|
+
2. **Server Layer** (`src/server/`) - Internal test/demo implementation (not exported)
|
|
13
|
+
|
|
14
|
+
## Core Module (`src/core.module.ts`)
|
|
15
|
+
|
|
16
|
+
- Dynamic module providing base functionality
|
|
17
|
+
- Configures GraphQL with Apollo Server (can be disabled via `graphQl: false`), MongoDB with Mongoose
|
|
18
|
+
- Provides global services: ConfigService, EmailService, TemplateService
|
|
19
|
+
- Sets up security interceptors, validation pipes, complexity plugins (when GraphQL enabled)
|
|
20
|
+
- Handles GraphQL subscriptions with authentication
|
|
21
|
+
|
|
22
|
+
## Configuration System (`src/config.env.ts`)
|
|
23
|
+
|
|
24
|
+
Environment-based configuration (development, local, production) with multiple sources:
|
|
25
|
+
|
|
26
|
+
- Direct environment variables in config file
|
|
27
|
+
- `NEST_SERVER_CONFIG` JSON environment variable
|
|
28
|
+
- `NSC__*` prefixed single environment variables
|
|
29
|
+
|
|
30
|
+
Key areas: JWT, MongoDB, GraphQL, email, security, static assets
|
|
31
|
+
|
|
32
|
+
## Core Common Components (`src/core/common/`)
|
|
33
|
+
|
|
34
|
+
| Type | Components |
|
|
35
|
+
|------|------------|
|
|
36
|
+
| **Decorators** | `@Restricted()`, `@Roles()`, `@CurrentUser()`, `@UnifiedField()` |
|
|
37
|
+
| **Helpers** | Database, GraphQL, filtering, validation utilities |
|
|
38
|
+
| **Security** | Response/security interceptors, input validation pipes |
|
|
39
|
+
| **Scalars** | Custom GraphQL scalars (Date, JSON, Any) |
|
|
40
|
+
| **Services** | CRUD operations, email (Mailjet/SMTP), template rendering |
|
|
41
|
+
|
|
42
|
+
## Core Modules (`src/core/modules/`)
|
|
43
|
+
|
|
44
|
+
| Module | Purpose |
|
|
45
|
+
|--------|---------|
|
|
46
|
+
| **Auth** | JWT authentication, refresh tokens, role-based access |
|
|
47
|
+
| **BetterAuth** | Modern auth integration (2FA, Passkey, Social) |
|
|
48
|
+
| **ErrorCode** | Centralized error codes with unique identifiers |
|
|
49
|
+
| **File** | File upload/download with GridFS storage |
|
|
50
|
+
| **HealthCheck** | Application health monitoring |
|
|
51
|
+
| **Migrate** | Database migration utilities |
|
|
52
|
+
| **SystemSetup** | Initial admin creation for fresh deployments |
|
|
53
|
+
| **Tus** | Resumable file uploads via tus.io protocol |
|
|
54
|
+
| **User** | Core user management functionality |
|
|
55
|
+
|
|
56
|
+
## Security Implementation
|
|
57
|
+
|
|
58
|
+
- `@Restricted()` - Field-level access control
|
|
59
|
+
- `@Roles()` - Method-level authorization
|
|
60
|
+
- `CheckResponseInterceptor` - Filters restricted fields
|
|
61
|
+
- `CheckSecurityInterceptor` - Processes `securityCheck()` methods
|
|
62
|
+
|
|
63
|
+
## Model Inheritance
|
|
64
|
+
|
|
65
|
+
- `CorePersistenceModel` - Base for database entities
|
|
66
|
+
- `CoreModel` - Base for GraphQL types
|
|
67
|
+
- Automatic ID handling with custom Mongoose plugin
|
|
68
|
+
|
|
69
|
+
## Input Validation
|
|
70
|
+
|
|
71
|
+
- `MapAndValidatePipe` - Automatic validation with inheritance-aware checking
|
|
72
|
+
- `@UnifiedField()` - Single decorator for GraphQL, Swagger, and validation (replaces separate `@Field`, `@ApiProperty`, `@IsOptional`, etc.)
|
|
73
|
+
- Automatic input property whitelisting — properties without `@UnifiedField` are stripped (default) or rejected
|
|
74
|
+
- `@UnifiedField({ exclude: true })` — explicitly exclude a property from input (hidden from schema, rejected at runtime)
|
|
75
|
+
- `@UnifiedField({ exclude: false })` — explicitly re-enable a property excluded by a parent class
|
|
76
|
+
- Configurable via `security.mapAndValidatePipe.nonWhitelistedFields`: `'strip'` (default), `'error'`, or `false`
|
|
77
|
+
- Custom decorator parameters (`@CurrentUser()`, `@RESTServiceOptions()`, etc.) and basic types (`String`, `Number`, etc.) are skipped — no validation or whitelist check
|
|
78
|
+
- Recursive nested object/array checking via `nestedTypeRegistry`
|
|
79
|
+
- Core args classes for filtering/pagination
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: src/core/modules/better-auth/**
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Better-Auth Module Development Rules
|
|
6
|
+
|
|
7
|
+
These rules apply when working in `src/core/modules/better-auth/`.
|
|
8
|
+
|
|
9
|
+
## 1. Maximize Better-Auth Standard Compliance
|
|
10
|
+
|
|
11
|
+
When making changes to the Better-Auth module:
|
|
12
|
+
|
|
13
|
+
- **Stay as close as possible to Better-Auth's standard behavior**
|
|
14
|
+
- **Minimize custom implementations** - use Better-Auth's built-in functionality wherever possible
|
|
15
|
+
- **Never bypass or disable security mechanisms** provided by Better-Auth
|
|
16
|
+
- **Maintain update compatibility** - changes must not break when Better-Auth releases updates
|
|
17
|
+
|
|
18
|
+
### Rationale
|
|
19
|
+
|
|
20
|
+
Better-Auth is a security-critical library. Custom implementations:
|
|
21
|
+
- May introduce security vulnerabilities
|
|
22
|
+
- Can break with Better-Auth updates
|
|
23
|
+
- Add maintenance burden
|
|
24
|
+
- May not benefit from Better-Auth's security audits
|
|
25
|
+
|
|
26
|
+
### Example: Adapter Pattern
|
|
27
|
+
|
|
28
|
+
When extending functionality (e.g., JWT mode for Passkey), prefer adapter patterns:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// GOOD: Adapter that bridges to Better-Auth's mechanisms
|
|
32
|
+
// - Uses Better-Auth's verificationToken
|
|
33
|
+
// - Lets Better-Auth handle all WebAuthn logic
|
|
34
|
+
// - Only bridges the cookie gap for JWT mode
|
|
35
|
+
|
|
36
|
+
// BAD: Custom implementation that replaces Better-Auth logic
|
|
37
|
+
// - Stores challenges separately
|
|
38
|
+
// - Implements own verification
|
|
39
|
+
// - Bypasses Better-Auth's security checks
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 2. Security-First Implementation
|
|
43
|
+
|
|
44
|
+
All Better-Auth code must be implemented with maximum security:
|
|
45
|
+
|
|
46
|
+
### Mandatory Security Measures
|
|
47
|
+
|
|
48
|
+
1. **Cryptographically secure IDs** - Use `crypto.randomBytes(32)` for tokens/IDs
|
|
49
|
+
2. **TTL-based expiration** - All temporary data must have automatic cleanup
|
|
50
|
+
3. **One-time use** - Tokens/challenges must be deleted after use
|
|
51
|
+
4. **Secrets protection** - Never expose internal tokens to clients
|
|
52
|
+
5. **Cookie signing** - Use proper HMAC signatures for cookies
|
|
53
|
+
|
|
54
|
+
### Security Review Checklist
|
|
55
|
+
|
|
56
|
+
Before completing any Better-Auth changes:
|
|
57
|
+
|
|
58
|
+
- [ ] No secrets/tokens exposed to client (only opaque IDs)
|
|
59
|
+
- [ ] All temporary data has TTL-based expiration
|
|
60
|
+
- [ ] One-time tokens are deleted after use
|
|
61
|
+
- [ ] Cryptographically secure random generation used
|
|
62
|
+
- [ ] No OWASP Top 10 vulnerabilities introduced
|
|
63
|
+
- [ ] Cookie signing uses proper HMAC with application secret
|
|
64
|
+
- [ ] Rate limiting considered for authentication endpoints
|
|
65
|
+
- [ ] Input validation on all user-supplied data
|
|
66
|
+
|
|
67
|
+
### Example: Challenge Storage
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Security measures applied:
|
|
71
|
+
// 1. challengeId: 256-bit entropy (crypto.randomBytes(32))
|
|
72
|
+
// 2. verificationToken: never sent to client
|
|
73
|
+
// 3. TTL index: automatic MongoDB cleanup
|
|
74
|
+
// 4. One-time use: deleted after verification
|
|
75
|
+
// 5. User binding: challenges tied to specific user
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 3. Comprehensive Testing Requirements
|
|
79
|
+
|
|
80
|
+
All Better-Auth changes require comprehensive tests:
|
|
81
|
+
|
|
82
|
+
### Test Requirements
|
|
83
|
+
|
|
84
|
+
1. **New functionality tests** - Cover all new features completely
|
|
85
|
+
2. **Security tests** - Verify authentication/authorization works correctly
|
|
86
|
+
3. **Edge case tests** - Token expiration, invalid input, race conditions
|
|
87
|
+
4. **Regression tests** - Existing tests must pass (adapt if needed)
|
|
88
|
+
|
|
89
|
+
### Pre-Commit Checklist
|
|
90
|
+
|
|
91
|
+
Before completing any Better-Auth changes:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# All tests must pass
|
|
95
|
+
pnpm test
|
|
96
|
+
|
|
97
|
+
# Specific test file for targeted changes
|
|
98
|
+
pnpm test -- tests/stories/better-auth-*.ts
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Test Categories for Better-Auth
|
|
102
|
+
|
|
103
|
+
| Category | Focus | Example Tests |
|
|
104
|
+
|----------|-------|---------------|
|
|
105
|
+
| Authentication | Login/logout flows | `better-auth-api.story.test.ts` |
|
|
106
|
+
| Authorization | Role-based access | `better-auth-rest-security.e2e-spec.ts` |
|
|
107
|
+
| Security | Token validation, rate limiting | `better-auth-rate-limit.story.test.ts` |
|
|
108
|
+
| Integration | Module initialization | `better-auth-integration.story.test.ts` |
|
|
109
|
+
| Plugins | 2FA, Passkey, Social Login | `better-auth-plugins.story.test.ts` |
|
|
110
|
+
|
|
111
|
+
### Adapting Existing Tests
|
|
112
|
+
|
|
113
|
+
When changes affect existing test expectations:
|
|
114
|
+
|
|
115
|
+
1. **Understand why** the test was written that way
|
|
116
|
+
2. **Verify the change is correct** - not breaking intended behavior
|
|
117
|
+
3. **Update test** to match new correct behavior
|
|
118
|
+
4. **Document** why the test was changed in commit message
|
|
119
|
+
|
|
120
|
+
## 4. Customization Patterns
|
|
121
|
+
|
|
122
|
+
When a project needs custom BetterAuth behavior, follow these patterns:
|
|
123
|
+
|
|
124
|
+
### Module Registration Patterns
|
|
125
|
+
|
|
126
|
+
| Pattern | Use When | Configuration |
|
|
127
|
+
|---------|----------|---------------|
|
|
128
|
+
| **Zero-Config** | No customization needed | `CoreModule.forRoot(envConfig)` |
|
|
129
|
+
| **Overrides Parameter** (recommended) | Custom Controller/Resolver | `CoreModule.forRoot(envConfig, { betterAuth: { controller, resolver } })` |
|
|
130
|
+
| **Separate Module** | Full control, additional providers | `betterAuth: { autoRegister: false }` |
|
|
131
|
+
|
|
132
|
+
### Pattern Selection Decision Tree
|
|
133
|
+
|
|
134
|
+
1. Does the project need custom Controller or Resolver?
|
|
135
|
+
- No → Use Zero-Config (Pattern 1)
|
|
136
|
+
- Yes → Continue to 2
|
|
137
|
+
|
|
138
|
+
2. Does the project need additional providers or complex module structure?
|
|
139
|
+
- No → Use Overrides Parameter (Pattern 2) - pass `{ betterAuth: { controller, resolver } }` as second arg to `CoreModule.forRoot()`
|
|
140
|
+
- Yes → Use Separate Module (Pattern 3) - set `autoRegister: false`
|
|
141
|
+
|
|
142
|
+
### Critical: Resolver Decorator Re-declaration
|
|
143
|
+
|
|
144
|
+
When customizing the Resolver, **ALL decorators MUST be re-declared**:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// WRONG - method won't appear in GraphQL schema!
|
|
148
|
+
override async betterAuthSignUp(...) {
|
|
149
|
+
return super.betterAuthSignUp(...);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// CORRECT - all decorators re-declared
|
|
153
|
+
@Mutation(() => BetterAuthAuthModel)
|
|
154
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
155
|
+
override async betterAuthSignUp(...) {
|
|
156
|
+
return super.betterAuthSignUp(...);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Why:** GraphQL schema is built from decorators at compile time. Parent class is `isAbstract: true`.
|
|
161
|
+
|
|
162
|
+
### Email Template Customization
|
|
163
|
+
|
|
164
|
+
Templates are resolved in order:
|
|
165
|
+
1. `<template>-<locale>.ejs` in project templates
|
|
166
|
+
2. `<template>.ejs` in project templates
|
|
167
|
+
3. `<template>-<locale>.ejs` in nest-server (fallback)
|
|
168
|
+
4. `<template>.ejs` in nest-server (fallback)
|
|
169
|
+
|
|
170
|
+
To override: Create `src/templates/email-verification-de.ejs` in the project.
|
|
171
|
+
|
|
172
|
+
### Avoiding "forRoot() called twice" Warning
|
|
173
|
+
|
|
174
|
+
If you see this warning, the project has duplicate registration:
|
|
175
|
+
|
|
176
|
+
**Solutions:**
|
|
177
|
+
1. Use the `overrides` parameter on `CoreModule.forRoot()` (Pattern 2): `CoreModule.forRoot(envConfig, { betterAuth: { controller, resolver } })`
|
|
178
|
+
2. Set `betterAuth.autoRegister: false` (Pattern 3)
|
|
179
|
+
|
|
180
|
+
**See:** `src/core/modules/better-auth/CUSTOMIZATION.md` for complete documentation.
|
|
181
|
+
|
|
182
|
+
## 5. RolesGuard Architecture
|
|
183
|
+
|
|
184
|
+
### Two Guard Implementations
|
|
185
|
+
|
|
186
|
+
The BetterAuth module provides two RolesGuard implementations:
|
|
187
|
+
|
|
188
|
+
| Guard | Used In | Key Characteristics |
|
|
189
|
+
|-------|---------|---------------------|
|
|
190
|
+
| `RolesGuard` | Legacy + Hybrid Mode | Extends `AuthGuard(JWT)`, supports Passport |
|
|
191
|
+
| `BetterAuthRolesGuard` | IAM-Only Mode | No Passport, no constructor dependencies |
|
|
192
|
+
|
|
193
|
+
### Why BetterAuthRolesGuard Exists
|
|
194
|
+
|
|
195
|
+
**Problem:** `AuthGuard()` from `@nestjs/passport` is a **mixin** that generates `design:paramtypes` metadata. When `RolesGuard extends AuthGuard(JWT)` is registered as `APP_GUARD` in a dynamic module (Pattern 3: `autoRegister: false`), NestJS DI fails to inject `Reflector` and `ModuleRef`.
|
|
196
|
+
|
|
197
|
+
**Error:** `Reflector not available - RolesGuard cannot function without it`
|
|
198
|
+
|
|
199
|
+
**Solution:** `BetterAuthRolesGuard` with NO constructor dependencies:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
@Injectable()
|
|
203
|
+
export class BetterAuthRolesGuard implements CanActivate {
|
|
204
|
+
// NO constructor dependencies - avoids mixin DI conflict
|
|
205
|
+
|
|
206
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
207
|
+
// Use Reflect.getMetadata directly (not NestJS Reflector)
|
|
208
|
+
const roles = Reflect.getMetadata('roles', context.getHandler());
|
|
209
|
+
|
|
210
|
+
// Access services via static module reference
|
|
211
|
+
const tokenService = CoreBetterAuthModule.getTokenServiceInstance();
|
|
212
|
+
|
|
213
|
+
// ... role checking logic identical to RolesGuard
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Guard Selection Logic
|
|
219
|
+
|
|
220
|
+
In `CoreBetterAuthModule.createDeferredModule()`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// IAM-Only Mode: Use BetterAuthRolesGuard (no Passport dependency)
|
|
224
|
+
providers: [
|
|
225
|
+
BetterAuthRolesGuard,
|
|
226
|
+
{ provide: APP_GUARD, useExisting: BetterAuthRolesGuard },
|
|
227
|
+
]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
In `CoreAuthModule` (Legacy Mode):
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Legacy Mode: Use RolesGuard (extends AuthGuard for Passport support)
|
|
234
|
+
providers: [
|
|
235
|
+
{ provide: APP_GUARD, useClass: RolesGuard },
|
|
236
|
+
]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Security Equivalence
|
|
240
|
+
|
|
241
|
+
Both guards implement identical security logic:
|
|
242
|
+
- Same `@Roles()` decorator processing
|
|
243
|
+
- Same role checks (S_USER, S_EVERYONE, S_VERIFIED, S_SELF, S_CREATOR, S_NO_ONE)
|
|
244
|
+
- Same token verification (via BetterAuthTokenService)
|
|
245
|
+
- Same error responses (401 Unauthorized, 403 Forbidden)
|
|
246
|
+
|
|
247
|
+
### When Working on Guards
|
|
248
|
+
|
|
249
|
+
1. **Changes to role logic** → Update BOTH guards
|
|
250
|
+
2. **New system roles** → Add to BOTH guards
|
|
251
|
+
3. **Token verification changes** → Update `BetterAuthTokenService` (shared by both)
|
|
252
|
+
4. **Testing** → Test both Legacy Mode and IAM-Only Mode
|
|
253
|
+
|
|
254
|
+
## Summary
|
|
255
|
+
|
|
256
|
+
| Principle | Requirement |
|
|
257
|
+
|-----------|-------------|
|
|
258
|
+
| Standard Compliance | Stay close to Better-Auth, minimize custom code |
|
|
259
|
+
| Security | Maximum security, thorough review before completion |
|
|
260
|
+
| Testing | Full coverage, all tests pass, security tests included |
|
|
261
|
+
| Customization | Use correct registration pattern, re-declare Resolver decorators |
|
|
262
|
+
| Guards | Maintain both RolesGuard and BetterAuthRolesGuard in sync |
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Configurable Features Pattern
|
|
2
|
+
|
|
3
|
+
This document describes the standard pattern for implementing optional, configurable features in @lenne.tech/nest-server.
|
|
4
|
+
|
|
5
|
+
## "Presence Implies Enabled" Pattern
|
|
6
|
+
|
|
7
|
+
When implementing configurable features, follow this pattern for activation logic:
|
|
8
|
+
|
|
9
|
+
### Rules
|
|
10
|
+
|
|
11
|
+
1. **No configuration** (`undefined` or `null`): Feature is **disabled** (backward compatible)
|
|
12
|
+
2. **Empty object** (`{}`): Feature is **enabled** with all default values
|
|
13
|
+
3. **Partial configuration** (`{ max: 5 }`): Feature is **enabled**, missing values use defaults
|
|
14
|
+
4. **Explicit disable** (`{ enabled: false, ... }`): Feature is **disabled**, allows pre-configuration
|
|
15
|
+
|
|
16
|
+
### Benefits
|
|
17
|
+
|
|
18
|
+
- **Backward Compatible**: Existing projects without config continue to work unchanged
|
|
19
|
+
- **Efficient**: No need to set `enabled: true` redundantly when already providing config
|
|
20
|
+
- **Flexible**: Can pre-configure without activating via `enabled: false`
|
|
21
|
+
- **Intuitive**: Providing a config object signals intent to use the feature
|
|
22
|
+
|
|
23
|
+
### Implementation Example
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
interface IFeatureConfig {
|
|
27
|
+
enabled?: boolean; // Optional - presence of config implies true
|
|
28
|
+
max?: number;
|
|
29
|
+
windowSeconds?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_CONFIG: Required<IFeatureConfig> = {
|
|
33
|
+
enabled: false, // Default is false, but overridden by presence
|
|
34
|
+
max: 10,
|
|
35
|
+
windowSeconds: 60,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class FeatureService {
|
|
39
|
+
private config: Required<IFeatureConfig> = DEFAULT_CONFIG;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configure the feature
|
|
43
|
+
*
|
|
44
|
+
* Follows the "presence implies enabled" pattern:
|
|
45
|
+
* - If config is undefined/null: feature stays disabled (backward compatible)
|
|
46
|
+
* - If config is an object (even empty {}): feature is enabled by default
|
|
47
|
+
* - Unless `enabled: false` is explicitly set
|
|
48
|
+
*/
|
|
49
|
+
configure(config: IFeatureConfig | undefined | null): void {
|
|
50
|
+
// No config = stay disabled (backward compatible)
|
|
51
|
+
if (config === undefined || config === null) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Presence of config implies enabled, unless explicitly disabled
|
|
56
|
+
const enabled = config.enabled !== false;
|
|
57
|
+
|
|
58
|
+
this.config = {
|
|
59
|
+
...DEFAULT_CONFIG,
|
|
60
|
+
...config,
|
|
61
|
+
enabled,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Usage Examples
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// config.env.ts
|
|
71
|
+
|
|
72
|
+
// Feature disabled (no config)
|
|
73
|
+
// rateLimit: undefined // or just don't define it
|
|
74
|
+
|
|
75
|
+
// Feature enabled with all defaults
|
|
76
|
+
auth: {
|
|
77
|
+
rateLimit: {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Feature enabled with custom max
|
|
81
|
+
auth: {
|
|
82
|
+
rateLimit: { max: 20 }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Feature enabled with full configuration
|
|
86
|
+
auth: {
|
|
87
|
+
rateLimit: {
|
|
88
|
+
max: 10,
|
|
89
|
+
windowSeconds: 60,
|
|
90
|
+
message: 'Too many requests'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pre-configured but disabled (for testing or gradual rollout)
|
|
95
|
+
auth: {
|
|
96
|
+
rateLimit: {
|
|
97
|
+
enabled: false,
|
|
98
|
+
max: 10,
|
|
99
|
+
windowSeconds: 60
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Boolean Shorthand Pattern
|
|
105
|
+
|
|
106
|
+
For simple enable/disable scenarios, support `boolean | object` configuration:
|
|
107
|
+
|
|
108
|
+
### Rules
|
|
109
|
+
|
|
110
|
+
1. **`true`**: Feature is **enabled** with all default values
|
|
111
|
+
2. **`false`**: Feature is **disabled**
|
|
112
|
+
3. **`{}`**: Feature is **enabled** with all default values (same as `true`)
|
|
113
|
+
4. **`{ option: value }`**: Feature is **enabled** with custom settings
|
|
114
|
+
5. **`{ enabled: false }`**: Feature is **disabled** (allows pre-configuration)
|
|
115
|
+
6. **`undefined`**: Feature is **disabled** (default)
|
|
116
|
+
|
|
117
|
+
### Benefits
|
|
118
|
+
|
|
119
|
+
- **Concise**: `jwt: true` instead of `jwt: {}`
|
|
120
|
+
- **Readable**: Clear intent at a glance
|
|
121
|
+
- **Flexible**: Can still use objects for customization
|
|
122
|
+
|
|
123
|
+
### Implementation Example
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Interface definition
|
|
127
|
+
interface IBetterAuth {
|
|
128
|
+
jwt?: boolean | IBetterAuthJwtConfig;
|
|
129
|
+
twoFactor?: boolean | IBetterAuthTwoFactorConfig;
|
|
130
|
+
passkey?: boolean | IBetterAuthPasskeyConfig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface IBetterAuthJwtConfig {
|
|
134
|
+
enabled?: boolean;
|
|
135
|
+
expiresIn?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Helper functions
|
|
139
|
+
function isPluginEnabled<T extends { enabled?: boolean }>(
|
|
140
|
+
config: boolean | T | undefined
|
|
141
|
+
): boolean {
|
|
142
|
+
if (config === undefined) return false;
|
|
143
|
+
if (typeof config === 'boolean') return config;
|
|
144
|
+
return config.enabled !== false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getPluginConfig<T extends { enabled?: boolean }>(
|
|
148
|
+
config: boolean | T | undefined
|
|
149
|
+
): T | undefined {
|
|
150
|
+
if (!isPluginEnabled(config)) return undefined;
|
|
151
|
+
if (typeof config === 'boolean') return {} as T;
|
|
152
|
+
return config;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Usage in build logic
|
|
156
|
+
const jwtConfig = getPluginConfig(config.jwt);
|
|
157
|
+
if (jwtConfig) {
|
|
158
|
+
plugins.push(jwt({ expirationTime: jwtConfig.expiresIn || '15m' }));
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Usage Examples
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// config.env.ts
|
|
166
|
+
|
|
167
|
+
betterAuth: {
|
|
168
|
+
// Boolean shorthand - enable with defaults
|
|
169
|
+
jwt: true,
|
|
170
|
+
twoFactor: true,
|
|
171
|
+
passkey: true,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Equivalent to:
|
|
175
|
+
betterAuth: {
|
|
176
|
+
jwt: {},
|
|
177
|
+
twoFactor: {},
|
|
178
|
+
passkey: {},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mixed - some with defaults, some customized
|
|
182
|
+
betterAuth: {
|
|
183
|
+
jwt: true, // Enable with defaults
|
|
184
|
+
twoFactor: { appName: 'My App' }, // Enable with custom settings
|
|
185
|
+
passkey: false, // Explicitly disabled
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Pre-configured but disabled
|
|
189
|
+
betterAuth: {
|
|
190
|
+
jwt: { enabled: false, expiresIn: '1h' }, // Ready to enable later
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Applied Features
|
|
195
|
+
|
|
196
|
+
This pattern is currently applied to:
|
|
197
|
+
|
|
198
|
+
| Feature | Config Path | Pattern | Default Values |
|
|
199
|
+
|---------|-------------|---------|----------------|
|
|
200
|
+
| Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
|
|
201
|
+
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
|
|
202
|
+
| BetterAuth JWT Plugin | `betterAuth.jwt` | Boolean Shorthand | `expiresIn: '15m'` |
|
|
203
|
+
| BetterAuth 2FA Plugin | `betterAuth.twoFactor` | Boolean Shorthand | `appName: 'Nest Server'` |
|
|
204
|
+
| BetterAuth Passkey Plugin | `betterAuth.passkey` | Boolean Shorthand | `rpName: 'Nest Server'` |
|
|
205
|
+
| BetterAuth Cross-Subdomain Cookies | `betterAuth.crossSubDomainCookies` | Boolean Shorthand | `domain: auto (appUrl → baseUrl without api. prefix)` |
|
|
206
|
+
| BetterAuth Disable Sign-Up | `betterAuth.emailAndPassword.disableSignUp` | Explicit Boolean | `false` (sign-up enabled) |
|
|
207
|
+
| System Setup | `systemSetup` | Enabled by Default (when BetterAuth active) | `initialAdmin: undefined` |
|
|
208
|
+
| GraphQL | `graphQl` | Explicit Disable (`false`) | Enabled (full GraphQL stack) |
|
|
209
|
+
| Mongoose Password Plugin | `security.mongoosePasswordPlugin` | Boolean Shorthand | `true` (enabled), `skipPatterns: []` |
|
|
210
|
+
| Mongoose Role Guard Plugin | `security.mongooseRoleGuardPlugin` | Boolean Shorthand | `true` (enabled), `allowedRoles: []`. Bypass: `RequestContext.runWithBypassRoleGuard()` or `force: true` |
|
|
211
|
+
| Mongoose Audit Fields Plugin | `security.mongooseAuditFieldsPlugin` | Boolean Shorthand | `true` (enabled) |
|
|
212
|
+
| Response Model Interceptor | `security.responseModelInterceptor` | Boolean Shorthand | `true` (enabled), `debug: false` |
|
|
213
|
+
| Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
|
|
214
|
+
| Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
|
|
215
|
+
| Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `CoreTenantGuard.invalidateUser(userId)` / `invalidateAll()` |
|
|
216
|
+
| BetterAuth Tenant Skip | `betterAuth.skipTenantCheck` | Explicit Boolean | `true` (default). When `true` and no `X-Tenant-Id` header is sent, IAM endpoints (controller + resolver) skip `CoreTenantGuard` tenant validation. When header IS present, normal membership validation runs regardless. Set `false` for tenant-aware auth scenarios (subdomain-based, invite links, SSO per tenant) |
|
|
217
|
+
|
|
218
|
+
## Module Override Pattern (via `ICoreModuleOverrides`)
|
|
219
|
+
|
|
220
|
+
For replacing default controllers, resolvers, or services of auto-registered core modules.
|
|
221
|
+
|
|
222
|
+
### Why a separate `overrides` parameter?
|
|
223
|
+
|
|
224
|
+
NestJS registers controllers at module scan time — there is no mechanism to replace them after registration.
|
|
225
|
+
When `CoreModule.forRoot()` auto-registers a module (e.g., ErrorCodeModule), the only way to use a custom controller
|
|
226
|
+
is to pass it **before** registration happens. A separate `overrides` parameter on `CoreModule.forRoot()` keeps
|
|
227
|
+
class references (code) cleanly separated from environment configuration (strings/numbers).
|
|
228
|
+
|
|
229
|
+
### Usage
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// IAM-only mode
|
|
233
|
+
CoreModule.forRoot(envConfig, {
|
|
234
|
+
errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
|
|
235
|
+
betterAuth: { resolver: BetterAuthResolver },
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Legacy mode
|
|
239
|
+
CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig, {
|
|
240
|
+
errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Available Override Fields
|
|
245
|
+
|
|
246
|
+
| Module | Fields | Description |
|
|
247
|
+
|--------|--------|-------------|
|
|
248
|
+
| `errorCode` | `controller`, `service` | Custom error code endpoint and/or service |
|
|
249
|
+
| `betterAuth` | `controller`, `resolver` | Custom IAM REST controller and/or GraphQL resolver |
|
|
250
|
+
|
|
251
|
+
### Rules
|
|
252
|
+
|
|
253
|
+
1. Overrides take precedence over `betterAuth.controller`/`resolver` in config (backward compatible)
|
|
254
|
+
2. Only auto-registered modules are affected — `autoRegister: false` modules are imported separately
|
|
255
|
+
3. The `ICoreModuleOverrides` interface enforces type safety per module
|
|
256
|
+
|
|
257
|
+
### Alternative: `autoRegister: false`
|
|
258
|
+
|
|
259
|
+
For complex setups requiring additional providers or a custom module structure, disable auto-registration
|
|
260
|
+
and import the module separately:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// config.env.ts
|
|
264
|
+
errorCode: { autoRegister: false }
|
|
265
|
+
betterAuth: { autoRegister: false }
|
|
266
|
+
|
|
267
|
+
// server.module.ts
|
|
268
|
+
@Module({
|
|
269
|
+
imports: [
|
|
270
|
+
CoreModule.forRoot(envConfig),
|
|
271
|
+
ErrorCodeModule.forRoot({ controller: MyController, service: MyService }),
|
|
272
|
+
BetterAuthModule.forRoot({ controller: MyController, resolver: MyResolver }),
|
|
273
|
+
],
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Checklist for New Configurable Features
|
|
278
|
+
|
|
279
|
+
When adding a new configurable feature:
|
|
280
|
+
|
|
281
|
+
### For "Presence Implies Enabled" Pattern:
|
|
282
|
+
|
|
283
|
+
- [ ] Define interface with `enabled?: boolean` as optional property
|
|
284
|
+
- [ ] Set `enabled: false` in DEFAULT_CONFIG
|
|
285
|
+
- [ ] Implement "presence implies enabled" logic in configure method
|
|
286
|
+
- [ ] Document all default values in interface JSDoc
|
|
287
|
+
- [ ] Add tests for: undefined config, empty object, partial config, explicit disable
|
|
288
|
+
|
|
289
|
+
### For "Boolean Shorthand" Pattern:
|
|
290
|
+
|
|
291
|
+
- [ ] Define separate interface for config options (e.g., `IBetterAuthJwtConfig`)
|
|
292
|
+
- [ ] Use union type: `property?: boolean | IPropertyConfig`
|
|
293
|
+
- [ ] Implement `isPluginEnabled()` helper for boolean/object handling
|
|
294
|
+
- [ ] Implement `getPluginConfig()` helper to normalize to object
|
|
295
|
+
- [ ] Add tests for: `true`, `false`, `{}`, `{ option: value }`, `{ enabled: false }`, `undefined`
|
|
296
|
+
|
|
297
|
+
### For "Module Override" Pattern:
|
|
298
|
+
|
|
299
|
+
- [ ] Add override fields to `ICoreModuleOverrides` interface
|
|
300
|
+
- [ ] Pass overrides through in `CoreModule.forRoot()` to the module's `forRoot()`
|
|
301
|
+
- [ ] Ensure the module's `forRoot()` accepts controller/resolver/service parameters
|
|
302
|
+
- [ ] Update this document with the new override fields
|
|
303
|
+
- [ ] Update module's INTEGRATION-CHECKLIST.md
|
|
304
|
+
|
|
305
|
+
### For Both Patterns:
|
|
306
|
+
|
|
307
|
+
- [ ] Update this document with the new feature
|
|
308
|
+
- [ ] Export new interfaces in `src/index.ts` (if needed)
|