@lenne.tech/nest-server 11.21.3 → 11.22.1

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.
Files changed (57) hide show
  1. package/.claude/rules/architecture.md +79 -0
  2. package/.claude/rules/better-auth.md +262 -0
  3. package/.claude/rules/configurable-features.md +308 -0
  4. package/.claude/rules/core-modules.md +205 -0
  5. package/.claude/rules/framework-compatibility.md +79 -0
  6. package/.claude/rules/migration-guides.md +149 -0
  7. package/.claude/rules/module-deprecation.md +214 -0
  8. package/.claude/rules/module-inheritance.md +97 -0
  9. package/.claude/rules/package-management.md +112 -0
  10. package/.claude/rules/role-system.md +146 -0
  11. package/.claude/rules/testing.md +120 -0
  12. package/.claude/rules/versioning.md +53 -0
  13. package/CLAUDE.md +174 -0
  14. package/FRAMEWORK-API.md +231 -0
  15. package/dist/core/common/interfaces/server-options.interface.d.ts +10 -0
  16. package/dist/core/modules/error-code/error-code.module.js.map +1 -1
  17. package/dist/core.module.d.ts +3 -3
  18. package/dist/core.module.js +17 -4
  19. package/dist/core.module.js.map +1 -1
  20. package/dist/server/modules/file/file-info.model.d.ts +1 -5
  21. package/dist/server/modules/user/user.model.d.ts +1 -5
  22. package/dist/server/server.module.js +6 -6
  23. package/dist/server/server.module.js.map +1 -1
  24. package/dist/tsconfig.build.tsbuildinfo +1 -1
  25. package/docs/REQUEST-LIFECYCLE.md +1256 -0
  26. package/docs/error-codes.md +446 -0
  27. package/migration-guides/11.10.x-to-11.11.x.md +266 -0
  28. package/migration-guides/11.11.x-to-11.12.x.md +323 -0
  29. package/migration-guides/11.12.x-to-11.13.0.md +612 -0
  30. package/migration-guides/11.13.x-to-11.14.0.md +348 -0
  31. package/migration-guides/11.14.x-to-11.15.0.md +262 -0
  32. package/migration-guides/11.15.0-to-11.15.3.md +118 -0
  33. package/migration-guides/11.15.x-to-11.16.0.md +497 -0
  34. package/migration-guides/11.16.x-to-11.17.0.md +130 -0
  35. package/migration-guides/11.17.x-to-11.18.0.md +393 -0
  36. package/migration-guides/11.18.x-to-11.19.0.md +151 -0
  37. package/migration-guides/11.19.x-to-11.20.0.md +170 -0
  38. package/migration-guides/11.20.x-to-11.21.0.md +216 -0
  39. package/migration-guides/11.21.0-to-11.21.1.md +194 -0
  40. package/migration-guides/11.21.1-to-11.21.2.md +114 -0
  41. package/migration-guides/11.21.2-to-11.21.3.md +175 -0
  42. package/migration-guides/11.21.x-to-11.22.0.md +224 -0
  43. package/migration-guides/11.22.0-to-11.22.1.md +105 -0
  44. package/migration-guides/11.3.x-to-11.4.x.md +233 -0
  45. package/migration-guides/11.6.x-to-11.7.x.md +394 -0
  46. package/migration-guides/11.7.x-to-11.8.x.md +318 -0
  47. package/migration-guides/11.8.x-to-11.9.x.md +322 -0
  48. package/migration-guides/11.9.x-to-11.10.x.md +571 -0
  49. package/migration-guides/TEMPLATE.md +113 -0
  50. package/package.json +25 -18
  51. package/src/core/common/interfaces/server-options.interface.ts +83 -16
  52. package/src/core/modules/better-auth/CUSTOMIZATION.md +24 -17
  53. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +5 -5
  54. package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +42 -12
  55. package/src/core/modules/error-code/error-code.module.ts +4 -9
  56. package/src/core.module.ts +52 -10
  57. package/src/server/server.module.ts +7 -9
@@ -0,0 +1,170 @@
1
+ # Migration Guide: 11.19.x → 11.20.0
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | None |
8
+ | **New Features** | Multi-Tenancy Plugin (opt-in) |
9
+ | **Bugfixes** | None |
10
+ | **Migration Effort** | 0 min (feature is opt-in, no changes required for existing projects) |
11
+
12
+ ---
13
+
14
+ ## Quick Migration (No Breaking Changes)
15
+
16
+ ```bash
17
+ # Update package
18
+ pnpm add @lenne.tech/nest-server@11.20.0
19
+
20
+ # Verify build
21
+ pnpm run build
22
+
23
+ # Run tests
24
+ pnpm test
25
+ ```
26
+
27
+ No code changes required. Multi-tenancy is disabled by default and has zero overhead when not configured.
28
+
29
+ ---
30
+
31
+ ## What's New in 11.20.0
32
+
33
+ ### Multi-Tenancy: Tenant-Based Data Isolation
34
+
35
+ A global Mongoose plugin that automatically filters all database queries by `tenantId`, providing tenant isolation in a shared MongoDB database.
36
+
37
+ **Key features:**
38
+ - Auto-filters queries (find, update, delete, aggregate, count, distinct)
39
+ - Auto-sets `tenantId` on new documents (save, insertMany)
40
+ - Zero overhead when disabled (plugin not registered)
41
+ - Works with both Legacy Auth (JWT) and BetterAuth (IAM)
42
+ - Follows the "Presence Implies Enabled" configuration pattern
43
+
44
+ #### Step 1: Enable in config
45
+
46
+ ```typescript
47
+ // config.env.ts
48
+ {
49
+ // Enable with defaults (userField: 'tenantId')
50
+ multiTenancy: {},
51
+
52
+ // Or with options
53
+ multiTenancy: {
54
+ excludeSchemas: ['User', 'Session'], // Model names to exclude from filtering
55
+ userField: 'tenantId', // Field on req.user (default: 'tenantId')
56
+ },
57
+
58
+ // Pre-configured but disabled
59
+ multiTenancy: {
60
+ enabled: false,
61
+ excludeSchemas: ['User'],
62
+ },
63
+ }
64
+ ```
65
+
66
+ #### Step 2: Add `tenantId` to your User model
67
+
68
+ ```typescript
69
+ // In your project's User model (NOT in nest-server!)
70
+ export class User extends CoreUserModel {
71
+ @UnifiedField({
72
+ mongoose: { type: String, index: true },
73
+ roles: RoleEnum.S_NO_ONE, // Users cannot modify their own tenantId
74
+ })
75
+ tenantId: string = undefined;
76
+ }
77
+ ```
78
+
79
+ #### Step 3: Add `tenantId` to domain models
80
+
81
+ > **Security Warning:** You **MUST** use `@UnifiedField` with `roles: RoleEnum.S_NO_ONE` on the `tenantId` field of every domain model. Without this, API consumers can inject arbitrary tenant IDs via request body, bypassing tenant isolation on writes. The plugin auto-sets `tenantId` from the user context — manual input should be blocked.
82
+ >
83
+ > **This is a developer responsibility that the plugin cannot enforce.** The tenant plugin operates at the Mongoose middleware layer and has no knowledge of your GraphQL/REST input schema. If you omit the `roles: RoleEnum.S_NO_ONE` declaration, the field will be writable via API input, silently undermining tenant isolation on write paths. There is no runtime warning — the plugin will simply preserve whatever `tenantId` value was provided in the request body instead of auto-assigning from the user context.
84
+
85
+ ```typescript
86
+ @MongooseSchema({ timestamps: true })
87
+ export class Order extends CorePersistenceModel {
88
+ @UnifiedField({
89
+ mongoose: { type: String, index: true },
90
+ roles: RoleEnum.S_NO_ONE, // Prevent user from setting tenantId via API input
91
+ })
92
+ tenantId: string = undefined;
93
+
94
+ // ... other fields
95
+ }
96
+ ```
97
+
98
+ The plugin automatically activates on any schema that has a `tenantId` field. Schemas without `tenantId` are not affected.
99
+
100
+ #### Cross-Tenant Admin Operations
101
+
102
+ **Important:** ADMIN users are also filtered by their own `tenantId`. For admin dashboards or cross-tenant queries, use the explicit bypass:
103
+
104
+ ```typescript
105
+ import { RequestContext } from '@lenne.tech/nest-server';
106
+
107
+ // Admin dashboard: view all tenants
108
+ const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
109
+ return this.orderService.find();
110
+ });
111
+ ```
112
+
113
+ #### Behavior Reference
114
+
115
+ | Situation | Behavior |
116
+ |-----------|----------|
117
+ | No `multiTenancy` config | Plugin not registered, zero overhead |
118
+ | No RequestContext (cron, migration) | No filter applied |
119
+ | `bypassTenantGuard: true` | No filter applied |
120
+ | Schema in `excludeSchemas` | No filter applied |
121
+ | User with `tenantId: "abc"` | Filter: `{ tenantId: "abc" }` |
122
+ | User without `tenantId` | Filter: `{ tenantId: null }` (sees only unassigned data) |
123
+ | User with `tenantId: ""` (empty string) | Same as without — empty string is falsy, filter: `{ tenantId: null }` |
124
+ | No user (public endpoint) | No filter applied |
125
+
126
+ #### Known Limitations
127
+
128
+ 1. **`populate()`**: Referenced models with `tenantId` are also filtered. Shared models (User, Config) should be in `excludeSchemas`.
129
+ 2. **`estimatedDocumentCount`**: Does not support query filters (MongoDB limitation).
130
+ 3. **`$lookup` in aggregates**: Joined collections are not automatically filtered. Add `$match` in `$lookup.pipeline` manually.
131
+ 4. **Legacy data**: Existing documents without `tenantId` are not auto-assigned. Backfill tenantIds via migration script.
132
+ 5. **Write-path protection**: The plugin auto-sets `tenantId` on new documents but does not enforce it on explicit values. Use `@UnifiedField({ roles: RoleEnum.S_NO_ONE })` on domain model `tenantId` fields to prevent cross-tenant write injection via API input.
133
+ 6. **Empty string tenantId**: If `req.user.tenantId` is an empty string (`''`), it is treated as falsy — the user sees only documents with `tenantId: null`. Ensure tenant IDs are never stored as empty strings.
134
+
135
+ ---
136
+
137
+ ## Compatibility Notes
138
+
139
+ ### Pattern: Existing projects without multi-tenancy
140
+
141
+ **Status: Fully compatible, no action required**
142
+
143
+ If your project does not configure `multiTenancy`, the plugin is never registered. There is zero performance impact.
144
+
145
+ ### Pattern: Projects with custom Mongoose plugins
146
+
147
+ **Status: Compatible**
148
+
149
+ The tenant plugin is registered after the audit fields plugin in `connectionFactory`. It does not interfere with other plugins.
150
+
151
+ ---
152
+
153
+ ## Module Documentation
154
+
155
+ | Module | Documentation |
156
+ |--------|--------------|
157
+ | Multi-Tenancy Plugin | `src/core/common/plugins/mongoose-tenant.plugin.ts` |
158
+ | RequestContext | `src/core/common/services/request-context.service.ts` |
159
+ | Configuration | `IMultiTenancy` in `src/core/common/interfaces/server-options.interface.ts` |
160
+
161
+ ---
162
+
163
+ ## Troubleshooting
164
+
165
+ | Issue | Solution |
166
+ |-------|----------|
167
+ | Admin can't see all tenants | Use `RequestContext.runWithBypassTenantGuard()` — ADMIN users are also filtered |
168
+ | `populate()` returns null for cross-tenant refs | Add referenced model to `excludeSchemas` |
169
+ | Legacy data invisible after enabling MT | Backfill `tenantId` on existing documents |
170
+ | Cron job sees all data | Expected — no RequestContext = no filter |
@@ -0,0 +1,216 @@
1
+ # Migration Guide: 11.20.x → 11.21.0
2
+
3
+ ## Overview
4
+
5
+ | Category | Changes | Effort |
6
+ |----------|---------|--------|
7
+ | **Breaking** | `userField` removed from `IMultiTenancy`, tenant filtering behavior changed | Medium |
8
+ | **Breaking** | Safety Net: tenantId-schemas throw `ForbiddenException` without valid tenant context | Low |
9
+ | **New Feature** | Header-based multi-tenant membership with configurable hierarchy roles | Low-Medium |
10
+
11
+ ## Not Using Multi-Tenancy?
12
+
13
+ If your project does **not** configure `multiTenancy` in `config.env.ts`, this update requires **no action**. All changes are fully backward compatible — just update the package version.
14
+
15
+ ## Breaking Changes
16
+
17
+ ### 1. `userField` Removed from IMultiTenancy
18
+
19
+ The `userField` property has been removed. Tenant ID is now read from the `X-Tenant-Id` request header instead of from `req.user`.
20
+
21
+ **Before (11.20.x):**
22
+ ```typescript
23
+ multiTenancy: {
24
+ userField: 'organizationId',
25
+ excludeSchemas: ['User'],
26
+ }
27
+ ```
28
+
29
+ **After (11.21.0):**
30
+ ```typescript
31
+ multiTenancy: {
32
+ headerName: 'x-tenant-id', // Optional, this is the default
33
+ excludeSchemas: ['User'],
34
+ }
35
+ ```
36
+
37
+ **Action Required:** Remove any `userField` references from your `config.env.ts`.
38
+
39
+ ### 2. Tenant Filtering Behavior Change
40
+
41
+ **Before (11.20.x):**
42
+ - Logged-in user without `tenantId` → queries filtered by `{ tenantId: null }` (sees only unassigned data)
43
+
44
+ **After (11.21.0):**
45
+ - Unauthenticated request with `X-Tenant-Id` header → **403 "Authentication required for tenant access"**
46
+ - Authenticated request without `X-Tenant-Id` header + hierarchy role → checks `user.roles`, filters `tenantIds` by level
47
+ - Authenticated user with no memberships → **Safety Net: `ForbiddenException`** on tenantId-schemas
48
+ - Use `@Roles(DefaultHR.MEMBER)` to require tenant membership on specific endpoints
49
+
50
+ **Action Required:** If you relied on the null-filter behavior, use `@Roles(DefaultHR.MEMBER)` on endpoints that should require tenant context.
51
+
52
+ ### 3. Safety Net (Defense-in-Depth)
53
+
54
+ The Mongoose tenant plugin now throws `ForbiddenException` instead of returning unfiltered data when a tenantId-scoped schema is accessed without valid tenant context.
55
+
56
+ **Affected scenarios:**
57
+ - Public endpoints (`@Roles(S_EVERYONE)`) accessing tenantId-scoped models → now throw 403
58
+ - System operations without `RequestContext` → unaffected (no context = no filter, as before)
59
+ - Operations with `bypassTenantGuard` → unaffected (bypass skips all filtering)
60
+
61
+ **Action Required:** Public routes that access tenantId-scoped models must use one of:
62
+ - `@SkipTenantCheck()` decorator + `RequestContext.runWithBypassTenantGuard()` in the service
63
+ - Or ensure a valid tenant context is provided
64
+
65
+ ### 4. New IMultiTenancy Properties
66
+
67
+ ```typescript
68
+ export interface IMultiTenancy {
69
+ enabled?: boolean;
70
+ excludeSchemas?: string[];
71
+ // NEW
72
+ headerName?: string; // Default: 'x-tenant-id'
73
+ membershipModel?: string; // Default: 'TenantMember'
74
+ adminBypass?: boolean; // Default: true
75
+ roleHierarchy?: Record<string, number>; // Default: { member: 1, manager: 2, owner: 3 }
76
+ }
77
+ ```
78
+
79
+ ## New Feature: Multi-Tenant Membership
80
+
81
+ ### What's New
82
+
83
+ - **CoreTenantModule** — Auto-registered when `multiTenancy` is configured
84
+ - **CoreTenantMemberModel** — Join table for user-tenant membership with roles
85
+ - **CoreTenantGuard** — APP_GUARD validating tenant header and membership
86
+ - **CoreTenantService** — Membership CRUD operations
87
+ - **Hierarchy Roles** — Configurable role levels with level comparison (`@Roles(DefaultHR.MEMBER)`)
88
+ - **Normal Roles** — Non-hierarchy roles with exact match (`@Roles('auditor')`)
89
+ - **`createHierarchyRoles()`** — Generate type-safe constants from hierarchy config
90
+ - **`DefaultHR`** — Pre-built constants for default hierarchy (MEMBER, MANAGER, OWNER)
91
+ - **`@SkipTenantCheck()`** — Skip tenant validation for specific endpoints
92
+ - **`@CurrentTenant()`** — Parameter decorator for tenant ID extraction
93
+ - **`TenantMemberStatus`** enum — ACTIVE, INVITED, SUSPENDED
94
+
95
+ ### Quick Setup
96
+
97
+ ```typescript
98
+ // config.env.ts — enable tenant system
99
+ multiTenancy: {},
100
+ ```
101
+
102
+ ### Protecting Endpoints
103
+
104
+ ```typescript
105
+ import { Roles, CurrentTenant, DefaultHR } from '@lenne.tech/nest-server';
106
+
107
+ @Controller('projects')
108
+ export class ProjectController {
109
+ @Get()
110
+ @Roles(DefaultHR.MEMBER) // any active member
111
+ async list(@CurrentTenant() tenantId: string) {
112
+ return this.projectService.find({ tenantId });
113
+ }
114
+
115
+ @Delete(':id')
116
+ @Roles(DefaultHR.OWNER) // highest level only
117
+ async delete(@Param('id') id: string) {
118
+ return this.projectService.delete(id);
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Custom Hierarchy (5+ Levels)
124
+
125
+ ```typescript
126
+ // config.env.ts
127
+ multiTenancy: {
128
+ roleHierarchy: { viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 },
129
+ }
130
+
131
+ // roles.ts
132
+ import { createHierarchyRoles } from '@lenne.tech/nest-server';
133
+ export const HR = createHierarchyRoles({ viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 });
134
+
135
+ // resolver.ts
136
+ @Roles(HR.EDITOR) // requires level >= 2 (editor, manager, admin, owner)
137
+ ```
138
+
139
+ ### Tenant Context Rule
140
+
141
+ When `X-Tenant-Id` header is present: only `membership.role` is checked (user.roles ignored, except ADMIN bypass).
142
+ When no header: `user.roles` is checked instead (same hierarchy/normal semantics).
143
+
144
+ ### Frontend: Send Tenant Header
145
+
146
+ All tenant-scoped API calls must include:
147
+ ```
148
+ X-Tenant-Id: <tenant-id>
149
+ ```
150
+
151
+ ### How It Works
152
+
153
+ 1. `RequestContextMiddleware` provides lazy getters for tenant context
154
+ 2. `CoreTenantGuard` validates membership → sets `req.tenantId` (only after successful check)
155
+ 3. Mongoose tenant plugin reads `tenantId` from `RequestContext` → filters queries
156
+ 4. **Safety Net:** Plugin throws `ForbiddenException` if tenantId-schema accessed without valid context
157
+
158
+ ### Admin Bypass
159
+
160
+ System admins (`RoleEnum.ADMIN`) skip membership validation by default.
161
+ Disable: `multiTenancy: { adminBypass: false }`.
162
+
163
+ ## Compatibility Notes
164
+
165
+ | Pattern | Status | Notes |
166
+ |---------|--------|-------|
167
+ | `multiTenancy: {}` | Compatible | Enables tenant system with defaults |
168
+ | `multiTenancy: { enabled: false }` | Compatible | Pre-configured but disabled |
169
+ | No `multiTenancy` config | Compatible | Zero overhead, no changes needed |
170
+ | `excludeSchemas` | Compatible | Works as before; `TenantMember` auto-added |
171
+ | `RequestContext.runWithBypassTenantGuard()` | Compatible | Unchanged |
172
+ | Public endpoints on non-tenant schemas | Compatible | Unaffected by Safety Net |
173
+ | Public endpoints on tenant schemas | **Breaking** | Safety Net throws 403 — use `@SkipTenantCheck()` + bypass |
174
+
175
+ ## Troubleshooting
176
+
177
+ | Issue | Cause | Fix |
178
+ |-------|-------|-----|
179
+ | 403 "Authentication required for tenant access" | Header present but no user | Authenticate before accessing tenant endpoints |
180
+ | 403 "Insufficient tenant role" | Membership role too low for hierarchy requirement | Upgrade membership role or use a lower-level decorator |
181
+ | 403 "Insufficient role" | No header, user.roles don't meet requirement | Add appropriate role to user.roles or send tenant header |
182
+ | 403 "Not a member of this tenant" | User has no active membership | Add membership via `CoreTenantService.addMember()` |
183
+ | 403 "Tenant context required" | Safety Net: tenantId-schema accessed without valid context | Add `@SkipTenantCheck()` + `runWithBypassTenantGuard()`, or provide tenant header |
184
+ | Data not filtered by tenant | Model missing `tenantId` field | Add `@Prop({ type: String }) tenantId: string` |
185
+ | TypeScript error: `userField` not found | Breaking change in IMultiTenancy | Remove `userField` from config |
186
+
187
+ ## Rollback / Downgrade
188
+
189
+ If you need to revert to 11.20.x after upgrading:
190
+
191
+ 1. **Downgrade the package:**
192
+ ```bash
193
+ pnpm add @lenne.tech/nest-server@11.20.1
194
+ ```
195
+
196
+ 2. **Restore `userField` in config:**
197
+ ```typescript
198
+ multiTenancy: {
199
+ userField: 'tenantId', // Re-add the removed property
200
+ excludeSchemas: ['User'],
201
+ }
202
+ ```
203
+
204
+ 3. **Remove tenant decorators** from any endpoints where you added `@Roles(DefaultHR.*)`, `@SkipTenantCheck()`, or `@CurrentTenant()`.
205
+
206
+ 4. **Remove `X-Tenant-Id` header** from frontend API calls if already added.
207
+
208
+ 5. **Note:** The `TenantMember` collection created by 11.21.0 will remain in MongoDB but is unused by 11.20.x. You can drop it manually if desired:
209
+ ```
210
+ db.tenantmembers.drop()
211
+ ```
212
+
213
+ ## Module Documentation
214
+
215
+ - [Tenant Module README](../src/core/modules/tenant/README.md)
216
+ - [Integration Checklist](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
@@ -0,0 +1,194 @@
1
+ # Migration Guide: 11.21.0 → 11.21.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Changes | Effort |
6
+ |----------|---------|--------|
7
+ | **Breaking Changes** | None | None |
8
+ | **New Features** | Tenant membership cache with configurable TTL, cache invalidation APIs, DB indices for BetterAuth | Low (opt-in) |
9
+ | **Performance Improvements** | CheckSecurityInterceptor, processDeep, EmailService, RequestContext bypass | None (automatic) |
10
+ | **Migration Effort** | Update package version | ~1 minute |
11
+
12
+ ---
13
+
14
+ ## Quick Migration
15
+
16
+ This is a PATCH release with no breaking changes. All improvements are backward compatible and most activate automatically.
17
+
18
+ ```bash
19
+ # Update package
20
+ pnpm add @lenne.tech/nest-server@11.21.1
21
+
22
+ # Verify build
23
+ pnpm run build
24
+
25
+ # Run tests
26
+ pnpm test
27
+ ```
28
+
29
+ No code changes required.
30
+
31
+ ---
32
+
33
+ ## What's New in 11.21.1
34
+
35
+ ### 1. Tenant Membership Cache (`multiTenancy.cacheTtlMs`)
36
+
37
+ The `CoreTenantGuard` now caches membership lookups in-process to avoid repeated database queries when the same user accesses the same tenant. This is especially beneficial for high-traffic multi-tenant applications.
38
+
39
+ **Default behavior:** Cache is enabled with a 30-second TTL. In test environments (`VITEST=true`, `NODE_ENV=test`, or `NODE_ENV=e2e`), the cache is automatically disabled to avoid stale data between test cases.
40
+
41
+ ```typescript
42
+ // config.env.ts — default (no changes needed, cache is on)
43
+ multiTenancy: {}
44
+
45
+ // config.env.ts — custom TTL (e.g., 60 seconds)
46
+ multiTenancy: {
47
+ cacheTtlMs: 60_000,
48
+ }
49
+
50
+ // config.env.ts — disable cache entirely
51
+ multiTenancy: {
52
+ cacheTtlMs: 0,
53
+ }
54
+ ```
55
+
56
+ **Important for horizontally scaled deployments:** The cache is process-local. When a membership is added or removed on one server instance, other instances will not see the change until the TTL expires. For security-critical deployments where immediate revocation is required, reduce the TTL or set it to `0`.
57
+
58
+ ### 2. Cache Invalidation APIs
59
+
60
+ Two new public methods on `CoreTenantGuard` allow programmatic cache invalidation:
61
+
62
+ | Method | Description |
63
+ |--------|-------------|
64
+ | `invalidateUser(userId)` | Clears all cached entries for a specific user |
65
+ | `invalidateAll()` | Clears the entire membership cache |
66
+
67
+ These are called automatically by `CoreTenantService` whenever memberships are added, removed, or updated. **No action required** unless your project extends `CoreTenantService` with custom membership mutation methods.
68
+
69
+ **Action required only if:** You have a custom service that modifies tenant memberships directly (bypassing `CoreTenantService`). In that case, inject `CoreTenantGuard` and call `invalidateUser()` after changes:
70
+
71
+ ```typescript
72
+ import { CoreTenantGuard } from '@lenne.tech/nest-server';
73
+
74
+ @Injectable()
75
+ export class CustomTenantService extends CoreTenantService {
76
+ constructor(
77
+ // ... other deps
78
+ private tenantGuard: CoreTenantGuard,
79
+ ) {
80
+ super(/* ... */);
81
+ }
82
+
83
+ async customMembershipChange(userId: string, tenantId: string): Promise<void> {
84
+ // ... your custom logic ...
85
+ this.tenantGuard.invalidateUser(userId);
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### 3. User Role Cache Invalidation (`CoreBetterAuthUserMapper`)
91
+
92
+ A new public method `invalidateUserCache(iamId)` is available on `CoreBetterAuthUserMapper`. It clears the cached user data (15-second TTL) used during session-to-user mapping.
93
+
94
+ This is called automatically from `CoreUserService.setRoles()`. **No action required** unless your project modifies user roles outside of `CoreUserService.setRoles()`.
95
+
96
+ **Action required only if:** You have custom logic that changes user roles directly in the database:
97
+
98
+ ```typescript
99
+ import { CoreBetterAuthUserMapper } from '@lenne.tech/nest-server';
100
+
101
+ @Injectable()
102
+ export class CustomUserService {
103
+ constructor(private userMapper: CoreBetterAuthUserMapper) {}
104
+
105
+ async customRoleChange(userId: string, iamId: string): Promise<void> {
106
+ // ... your custom role update logic ...
107
+ this.userMapper.invalidateUserCache(iamId);
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### 4. Automatic Database Indices
113
+
114
+ The following indices are now auto-created by `CoreBetterAuthService.onModuleInit()` when BetterAuth is enabled:
115
+
116
+ | Collection | Index | Purpose |
117
+ |------------|-------|---------|
118
+ | `session` | `{ token: 1 }` | Fast session token lookups |
119
+ | `session` | `{ userId: 1, expiresAt: 1 }` | Active session queries per user |
120
+ | `users` | `{ iamId: 1 }` (sparse) | IAM ID lookups during session mapping |
121
+
122
+ No action required. Index creation is idempotent — calling `createIndex` on an existing index is a no-op. These indices improve query performance for BetterAuth session validation and user mapping.
123
+
124
+ ---
125
+
126
+ ## Performance Improvements
127
+
128
+ These optimizations apply automatically. No configuration or code changes needed.
129
+
130
+ ### 1. CheckSecurityInterceptor: Conditional JSON.stringify
131
+
132
+ `JSON.stringify` on response data now only runs when `debug: true` is configured. Previously, every response was serialized even when debug output was not used. This reduces CPU overhead on every request that passes through the security interceptor.
133
+
134
+ ### 2. processDeep: Options Object Reuse
135
+
136
+ The `processDeep()` helper (used by `CheckSecurityInterceptor` and input validation) now reuses its options object across recursive calls instead of recreating it for each nested property. This reduces garbage collection pressure for deeply nested response objects.
137
+
138
+ ### 3. EmailService: SMTP Transport Reuse
139
+
140
+ The SMTP transport created by `nodemailer.createTransport()` is now cached as a singleton per configuration. Previously, a new transport was created for each email send operation. The cache key is derived from the SMTP configuration hash, so transport changes at runtime are handled correctly.
141
+
142
+ ### 4. RequestContext.runWithBypass*: Skip Redundant Context
143
+
144
+ `RequestContext.runWithBypassRoleGuard()` and `RequestContext.runWithBypassTenantGuard()` now skip creating a new `AsyncLocalStorage` context when the bypass flag is already set. This avoids unnecessary object spread operations in nested bypass calls.
145
+
146
+ ---
147
+
148
+ ## Compatibility Notes
149
+
150
+ | Pattern | Status | Notes |
151
+ |---------|--------|-------|
152
+ | `multiTenancy: {}` | Compatible | Cache enabled at 30s TTL by default |
153
+ | `multiTenancy: { cacheTtlMs: 0 }` | Compatible | Cache disabled, same behavior as before 11.21.1 |
154
+ | No `multiTenancy` config | Compatible | No changes, no overhead |
155
+ | Custom `CoreTenantService` extensions | Compatible | Automatic invalidation if using `super.addMember()` etc. |
156
+ | Direct membership DB mutations | **Check** | Must call `invalidateUser()` manually (see section 2 above) |
157
+ | Custom role-change logic (bypassing `CoreUserService.setRoles`) | **Check** | Must call `invalidateUserCache()` manually (see section 3 above) |
158
+ | BetterAuth disabled | Compatible | DB indices are not created when BetterAuth is disabled |
159
+ | Test environments | Compatible | Cache auto-disabled in test/e2e environments |
160
+
161
+ ---
162
+
163
+ ## Troubleshooting
164
+
165
+ | Issue | Cause | Fix |
166
+ |-------|-------|-----|
167
+ | Stale membership data after role change | Membership cache TTL not expired | Call `CoreTenantGuard.invalidateUser(userId)` after custom membership changes |
168
+ | Stale user roles after `setRoles()` | Should not happen (auto-invalidated) | Verify you are using `CoreUserService.setRoles()`, not direct DB updates |
169
+ | Membership changes not visible across server instances | Process-local cache | Reduce `cacheTtlMs` or set to `0` for immediate consistency |
170
+ | Tests see stale membership data | Cache enabled in test env | Verify `NODE_ENV=test` or `VITEST=true` is set (auto-disables cache) |
171
+
172
+ ---
173
+
174
+ ## Module Documentation
175
+
176
+ ### Tenant Module
177
+
178
+ - **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md)
179
+ - **Integration Checklist:** [src/core/modules/tenant/INTEGRATION-CHECKLIST.md](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
180
+ - **Key File:** `src/core/modules/tenant/core-tenant.guard.ts` — cache implementation and `invalidateUser()`/`invalidateAll()` methods
181
+
182
+ ### BetterAuth Module
183
+
184
+ - **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
185
+ - **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
186
+ - **Key File:** `src/core/modules/better-auth/core-better-auth.service.ts` — DB index creation in `onModuleInit()`
187
+ - **Key File:** `src/core/modules/better-auth/core-better-auth-user.mapper.ts` — `invalidateUserCache()` method
188
+
189
+ ---
190
+
191
+ ## References
192
+
193
+ - [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
194
+ - [11.20.x → 11.21.0 Migration Guide](./11.20.x-to-11.21.0.md) (previous release with multi-tenancy breaking changes)
@@ -0,0 +1,114 @@
1
+ # Migration Guide: 11.21.1 → 11.21.2
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | None |
8
+ | **New Features** | BetterAuth auto-skip for tenant validation on IAM endpoints |
9
+ | **Bugfixes** | lodash security fix (CVE HIGH + MODERATE) |
10
+ | **Migration Effort** | < 5 minutes |
11
+
12
+ ---
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ # Update package
18
+ pnpm add @lenne.tech/nest-server@11.21.2
19
+
20
+ # Verify build
21
+ pnpm run build
22
+
23
+ # Run tests
24
+ pnpm test
25
+ ```
26
+
27
+ No code changes required. The new feature activates automatically with safe defaults.
28
+
29
+ ---
30
+
31
+ ## What's New in 11.21.2
32
+
33
+ ### 1. BetterAuth Auto-Skip for Tenant Validation
34
+
35
+ **Affects:** Projects with both `multiTenancy` and `betterAuth` active.
36
+
37
+ When both features are enabled, IAM endpoints (sign-in, sign-up, sign-out, session, etc.)
38
+ now automatically skip `CoreTenantGuard` tenant validation when no `X-Tenant-Id` header is
39
+ present. This solves the chicken-and-egg problem where users couldn't authenticate because
40
+ they weren't yet a member of any tenant.
41
+
42
+ **Default behavior (`skipTenantCheck: true`):**
43
+
44
+ | Scenario | Behavior |
45
+ |----------|----------|
46
+ | No `X-Tenant-Id` header | Skip tenant validation (auth before tenant) |
47
+ | `X-Tenant-Id` header present | Validate normally (membership check + tenant context) |
48
+
49
+ The auto-skip applies to both REST controllers (`CoreBetterAuthController` subclasses)
50
+ and GraphQL resolvers (`CoreBetterAuthResolver` subclasses).
51
+
52
+ **If you previously used `@SkipTenantCheck()` on your IAM resolver/controller to work
53
+ around this issue, you can remove it** — the framework now handles this automatically.
54
+
55
+ ### 2. Opt-Out for Tenant-Aware Authentication
56
+
57
+ If your project requires tenant context at login time (e.g., subdomain-based tenancy,
58
+ invite links with embedded tenant, SSO per tenant), disable the auto-skip:
59
+
60
+ ```typescript
61
+ // config.env.ts
62
+ betterAuth: {
63
+ skipTenantCheck: false, // IAM endpoints require valid X-Tenant-Id header
64
+ }
65
+ ```
66
+
67
+ ### 3. Security: lodash Update
68
+
69
+ `lodash` updated from 4.17.23 to 4.18.1, fixing:
70
+ - GHSA-r5fr-rjxr-66jc (HIGH — prototype pollution)
71
+ - GHSA-f23m-r3pf-42rh (MODERATE — prototype pollution)
72
+
73
+ A `pnpm.overrides` entry ensures all transitive dependencies also use the patched version.
74
+
75
+ ### 4. Dependency Updates
76
+
77
+ | Package | From | To | Type |
78
+ |---------|------|----|------|
79
+ | `lodash` | 4.17.23 | 4.18.1 | Security fix |
80
+ | `dotenv` | 17.3.1 | 17.4.0 | Minor feature |
81
+ | `@swc/cli` | 0.8.0 | 0.8.1 | Patch |
82
+
83
+ Three unnecessary pnpm overrides were removed (`file-type`, `yauzl`, `flatted`).
84
+
85
+ ---
86
+
87
+ ## Compatibility Notes
88
+
89
+ - **Projects without `multiTenancy`:** Not affected. No behavior change.
90
+ - **Projects without `betterAuth`:** Not affected. No behavior change.
91
+ - **Projects with both features + default config:** IAM endpoints now work without `X-Tenant-Id` header. Previously, these would fail if the user wasn't a tenant member.
92
+ - **Projects with `@SkipTenantCheck()` on IAM endpoints:** The decorator continues to work but is no longer needed for this use case. Safe to remove.
93
+ - **Subdomain-based / invite-link tenancy:** Set `betterAuth.skipTenantCheck: false` to require tenant context on IAM endpoints.
94
+
95
+ ---
96
+
97
+ ## Module Documentation
98
+
99
+ ### Tenant Module
100
+
101
+ - **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md) — See "BetterAuth (IAM) Integration" section
102
+ - **Integration Checklist:** [src/core/modules/tenant/INTEGRATION-CHECKLIST.md](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md) — See § 9 "BetterAuth (IAM) Coexistence"
103
+
104
+ ### BetterAuth Module
105
+
106
+ - **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md) — See "Multi-Tenancy Integration" section
107
+
108
+ ---
109
+
110
+ ## References
111
+
112
+ - [Configurable Features](../.claude/rules/configurable-features.md) — `betterAuth.skipTenantCheck` entry
113
+ - [Request Lifecycle](../docs/REQUEST-LIFECYCLE.md) — CoreTenantGuard flow with auto-skip
114
+ - [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)