Skip to content

Commit f6a3a6e

Browse files
committed
11.23.0: Performance and security hardening — depth-aware process() pipeline, SafeModel type guard for mainDbModel, getNativeCollection/getNativeConnection escape hatches
1 parent b5a1fc6 commit f6a3a6e

File tree

17 files changed

+1849
-398
lines changed

17 files changed

+1849
-398
lines changed

.claude/agent-memory/lt-dev-npm-package-maintainer/MEMORY.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
- `@getbrevo/brevo` 3.x → 5.x: Complete API redesign (TransactionalEmailsApi, SendSmtpEmail, TransactionalEmailsApiApiKeys removed). Would require rewriting `src/core/common/services/brevo.service.ts`. See `blocking-updates.md` for details.
1111
- `graphql-upload` 15.x → 17.x: Extension changed from `.js` to `.mjs`. Import paths in `src/core.module.ts`, `src/core/modules/file/core-file.resolver.ts`, `src/server/modules/file/file.resolver.ts`, and `src/types/graphql-upload.d.ts` would all need updating.
1212
- `vite` 7.x → 8.x + `vite-plugin-node` 7.x → 8.x: Both must update together. vite-plugin-node@8.0.0 peerDep requires `vite: '^8.0.0'`. Blocked together.
13-
- `better-auth` + `@better-auth/passkey` 1.5.5 → 1.5.6: `@better-auth/core@1.5.6/dist/instrumentation/tracer.mjs` directly imports `@opentelemetry/api` (SpanStatusCode, trace) — causes "Cannot find package '@opentelemetry/api'" errors across 38+ test files. Verified on 2026-04-04. Do NOT update until better-auth resolves this dep or we add @opentelemetry/api as a dev dep.
13+
- `better-auth` + `@better-auth/passkey` 1.5.5 → 1.6.0: `@better-auth/core@1.6.0/dist/instrumentation/tracer.mjs` still directly imports `@opentelemetry/api` — causes "Cannot find package '@opentelemetry/api'" errors across 38+ test files. Verified on 2026-04-07. Do NOT update until better-auth resolves this dep or we add @opentelemetry/api as a dev dep. NOTE: 1.5.6 AND 1.6.0 both have this issue.
1414
- `typescript` 5.x → 6.x: TypeScript 6.0.2 released 2026-03-23 (very new). Ecosystem readiness unknown — skip until NestJS/tools explicitly support it.
1515

16+
### Categorization Fix (Fixed 2026-04-07)
17+
- `supertest` and `@types/supertest` moved from `devDependencies` to `dependencies`.
18+
Reason: `src/test/test.helper.ts` (exported via `src/index.ts`) imports `supertest` at runtime.
19+
Consuming projects would get runtime errors without it in `dependencies`.
20+
1621
### Critical Categorization Issue (Fixed in 2026-03-11)
1722
- `ts-morph` was incorrectly in `devDependencies` but is IMPORTED in `src/core/modules/permissions/permissions-scanner.ts`. Moved to `dependencies`.
1823

@@ -21,17 +26,18 @@
2126
- `mongoose@9.4.1` bundles `~mongodb@7.1.x` (same as 9.3.x), so current mongodb@7.1.1 is still compatible.
2227
- mongodb@7.1.1 is still the latest in the 7.x line — no update needed there.
2328

24-
### Overrides Status (updated 2026-04-04)
25-
- minimatch overrides: updated to latest versions (3.1.5, 9.0.9, 10.2.5)
26-
- `rollup@>=4.0.0 <4.60.1``4.60.1` override: still needed (4.60.1 is current latest)
29+
### Overrides Status (updated 2026-04-07)
30+
- minimatch overrides: at latest versions (3.1.5, 9.0.9, 10.2.5) — still needed
31+
- `rollup@>=4.0.0 <4.60.1``4.60.1` override: **REMOVED 2026-04-07** — vite@7.3.2 now pulls rollup@4.60.1 directly, override was redundant
2732
- `ajv` overrides still needed
28-
- `undici@>=7.0.0 <7.24.7``7.24.7` override: updated from 7.24.3. @compodoc/compodoc>cheerio requires `^7.12.0` — still needed in 7.x range
29-
- `srvx@<0.11.15``0.11.15` override: updated from 0.11.13. @tus/server 2.3.0 requires `~0.8.2` — still needed
30-
- `handlebars@>=4.0.0 <4.7.9``4.7.9` override: @compodoc/compodoc requires `^4.7.8`still needed for safety
33+
- `undici@>=7.0.0 <7.24.7``7.24.7` override: still needed. @compodoc/compodoc>cheerio requires `^7.12.0` — still needed in 7.x range
34+
- `srvx@<0.11.15``0.11.15` override: still needed. @tus/server 2.3.0 requires `~0.8.2` — still needed
35+
- `handlebars@>=4.0.0 <4.7.9``4.7.9` override: still needed for safety
3136
- `brace-expansion`, `picomatch`, `kysely` overrides: still needed (at latest)
32-
- `path-to-regexp@>=8.0.0 <8.4.2``8.4.2` override: updated from 8.4.1 (new patch)
37+
- `path-to-regexp@>=8.0.0 <8.4.2``8.4.2` override: still needed
3338
- `lodash@>=4.0.0 <4.18.0``4.18.1` override: @nestjs/graphql pins lodash@4.17.23 which has CVE. 4.18.1 is now the latest lodash.
34-
- `defu@<=6.1.4``6.1.6` override: still needed (6.1.6 is current latest)
39+
- `defu@<=6.1.4``6.1.6` override: **UPDATED 2026-04-07** to `defu@<=6.1.6``6.1.7` (6.1.7 is now the latest)
40+
- `vite@>=7.0.0 <=7.3.1` + `vite@>=7.1.0 <=7.3.1` duplicate overrides: **CONSOLIDATED 2026-04-07** to single `vite@>=7.0.0 <7.3.2``7.3.2`. Direct vite dep is now at 7.3.2.
3541
- **REMOVED 2026-04-03**: `file-type@>=13.0.0 <21.3.2` → all nestjs packages now at 11.1.17 with file-type 21.3.2 natively
3642
- **REMOVED 2026-04-03**: `yauzl@<3.2.1`@swc/cli 0.8.1 bundles yauzl 3.2.1 directly
3743
- **REMOVED 2026-04-03**: `flatted@<=3.4.1`@vitest/ui 4.1.2 requires `^3.4.2` already

.claude/rules/configurable-features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ This pattern is currently applied to:
214214
| Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
215215
| 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()` |
216216
| 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+
| Debug Process Input | `debugProcessInput` | Explicit Boolean | `false` (default). When `true`, logs a debug message when `prepareInput()` changes the input type during `process()`. Has performance cost due to `JSON.stringify` on every `process()` call — enable only for debugging |
217218

218219
## Module Override Pattern (via `ICoreModuleOverrides`)
219220

CLAUDE.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,66 @@ Detailed documentation in `.claude/rules/`:
167167
| `package-management.md` | Fixed package versions only - no `^`, `~`, or ranges |
168168
| `framework-compatibility.md` | Maintenance obligations for FRAMEWORK-API.md, shipped docs, cross-repo dependencies |
169169

170+
## Native MongoDB Driver — Security Rules
171+
172+
### model.collection / model.db — BLOCKED
173+
174+
Access to `this.mainDbModel.collection` and `this.mainDbModel.db` is blocked via TypeScript type (`SafeModel = Omit<Model, 'collection' | 'db'>`).
175+
For additionally injected models (`@InjectModel`): **NEVER** use `.collection.*` or `.db.*`.
176+
All Mongoose plugins (Tenant, Audit, RoleGuard, Password) are bypassed.
177+
178+
**Use Mongoose Model methods instead:**
179+
180+
| Forbidden | Allowed |
181+
|-----------|---------|
182+
| `collection.insertOne(doc)` | `Model.insertMany([doc])` |
183+
| `collection.bulkWrite(ops)` | `Model.bulkWrite(ops)` |
184+
| `collection.updateOne(f, u)` | `Model.updateOne(f, u)` |
185+
| `collection.updateMany(f, u)` | `Model.updateMany(f, u)` |
186+
| `collection.deleteOne(f)` | `Model.deleteOne(f)` |
187+
| `collection.deleteMany(f)` | `Model.deleteMany(f)` |
188+
| `collection.find(f)` | `Model.find(f)` or `Model.find(f).lean()` |
189+
| `collection.aggregate(p)` | `Model.aggregate(p)` |
190+
191+
If native access is unavoidable: use `this.getNativeCollection(reason)` or `this.getNativeConnection(reason)` from CrudService.
192+
193+
### connection.db.collection() — Use With Caution
194+
195+
Allowed for:
196+
- Collections without Mongoose schema (OAuth, BetterAuth, MCP)
197+
- Read-only aggregations/counts on other collections
198+
- Admin operations (indexes, drops, backups)
199+
200+
**NOT allowed for:**
201+
- Write operations on tenant-scoped collections → use Mongoose Model instead
202+
- CRUD on collections that have a Mongoose schema → use CrudService instead
203+
204+
Details: [`docs/native-driver-security.md`](docs/native-driver-security.md)
205+
206+
### CrudService process() Pipeline — Memory Considerations
207+
208+
The `process()` pipeline (prepareInput → checkRights → serviceFunc → processFieldSelection → prepareOutput → checkRights) adds memory overhead per call through object cloning, Mongoose hydration, and populate operations. For typical API usage this is negligible, but it can become significant in:
209+
210+
- **High-frequency operations** (e.g. monitor checks running every 10-60 seconds)
211+
- **Service cascades** (Service A → Service B → Service C, each going through process())
212+
- **Populate chains** (3-5 levels of nested population)
213+
214+
**If a project experiences memory issues under high traffic**, check whether `process()` wrapping is the cause. Alternatives that preserve security:
215+
216+
| Instead of | Use | Security |
217+
|-----------|-----|----------|
218+
| `CrudService.create(input)` | `Model.insertMany([input])` — triggers all Mongoose plugins | Tenant, Audit, RoleGuard, Password all active |
219+
| `CrudService.update(id, input)` | `Model.findByIdAndUpdate(id, input)` — triggers all Mongoose plugins | Tenant filter, Audit, RoleGuard all active |
220+
| `CrudService.updateForce(id, input)` | `Model.findByIdAndUpdate(id, { $set: input }).lean()` — for system-internal updates | Plugins active, no process() overhead |
221+
222+
**NEVER** bypass Mongoose entirely via `collection.*` — see section above. The CheckSecurityInterceptor acts as a safety net on HTTP responses regardless of how data was written.
223+
224+
Details: [`docs/process-performance-optimization.md`](docs/process-performance-optimization.md)
225+
170226
## In-Depth Documentation
171227

172228
| File | Content |
173229
|------|---------|
174230
| [`docs/REQUEST-LIFECYCLE.md`](docs/REQUEST-LIFECYCLE.md) | Complete request lifecycle, security architecture, interceptor chain, decorator reference, CrudService pipeline, Safety Net, diagrams |
231+
| [`docs/native-driver-security.md`](docs/native-driver-security.md) | Native MongoDB Driver restrictions, secure alternatives, review checklist |
232+
| [`docs/process-performance-optimization.md`](docs/process-performance-optimization.md) | process() pipeline performance optimizations |

FRAMEWORK-API.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @lenne.tech/nest-server — Framework API Reference
22

3-
> Auto-generated from source code on 2026-04-04 (v11.22.1)
3+
> Auto-generated from source code on 2026-04-07 (v11.23.0)
44
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
55
66
## CoreModule.forRoot()
@@ -21,6 +21,7 @@
2121
- `compression?`: `boolean | compression.CompressionOptions` — Whether to use the compression middleware package to enable gzip compression.
2222
- `cookies?`: `boolean` — Whether to use cookies for authentication handling
2323
- `cronJobs?`: `Record<string, string | false | 0 | CronJobConfigWithTimeZone<null, null> | C...` — Cron jobs configuration object with the name of the cron job function as key
24+
- `debugProcessInput?`: `boolean` (default: `false`) — When true, logs a debug message when prepareInput() changes the input type during process().
2425
- `email?`: `{ defaultSender?: { email?: string; name?: string; }; mailjet?: MailjetOption...` — SMTP and template configuration for sending emails
2526
- `env?`: `string` — Environment
2627
- `errorCode?`: `IErrorCode` — Configuration for the error code module
@@ -183,7 +184,10 @@ Generic: `CrudService<Model, CreateInput, UpdateInput>`
183184
- `async findOne(filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; }, serviceOptions?: ServiceOptions)`: `Promise<Model>` — Find one item via filter
184185
- `async findOneForce(filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; s..., serviceOptions?: ServiceOptions)`: `Promise<Model>` — Find one item via filter without checks or restrictions
185186
- `async findOneRaw(filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; s..., serviceOptions?: ServiceOptions)`: `Promise<Model>` — Find one item via filter without checks, restrictions or preparations
186-
- `getModel()`: `MongooseModel<Document<Types.ObjectId, any, any, Record<string, any>, {}> & M...` — Get service model to process queries directly
187+
- `getModel()`: `MongooseModel<Document<Types.ObjectId, any, any, Record<string, any>, {}> & M...` — Get service model to process queries directly.
188+
- `getNativeCollection(reason: string)`: `Collection<Document>` — Get the native MongoDB Collection, bypassing all Mongoose plugins.
189+
- `getNativeConnection(reason: string)`: `Connection` — Get the Mongoose Connection (which provides access to the native MongoDB Db and MongoClient).
190+
- `validateNativeAccessReason(reason: string, method: string)`: `void`
187191
- `async read(input: string | FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryO..., serviceOptions?: ServiceOptions)`: `Promise<Model | Model[]>` — CRUD alias for get or find
188192
- `async readForce(input: string | FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryO..., serviceOptions?: ServiceOptions)`: `Promise<Model | Model[]>` — CRUD alias for getForce or findForce
189193
- `async readRaw(input: string | FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryO..., serviceOptions?: ServiceOptions)`: `Promise<Model | Model[]>` — CRUD alias for getRaw or findRaw

docs/REQUEST-LIFECYCLE.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,15 +863,38 @@ The `process()` method in `ModuleService` is the **primary** way to handle CRUD
863863
+---------------------------------------------------------------+
864864
```
865865

866+
### Depth-Based Optimization (v11.23.0+)
867+
868+
When `process()` is called from within another `process()` call (service cascades like A.create → B.create → C.create), steps 4–6 are **conditionally skipped** on inner calls to avoid redundant work:
869+
870+
| Step | Depth 0 (outermost) | Depth > 0 (nested) |
871+
|------|---------------------|---------------------|
872+
| 1. prepareInput | Runs | Runs |
873+
| 2. checkRights (INPUT) | Runs | Runs |
874+
| 3. serviceFunc | Runs | Runs |
875+
| 4. processFieldSelection | Runs | **Skipped** (unless `populate` explicitly set) |
876+
| 5. prepareOutput (model mapping) | Runs | **Skipped** (secret removal still active) |
877+
| 6. checkRights (OUTPUT) | Runs | **Skipped** |
878+
879+
**Security is maintained** because:
880+
1. Input authorization (step 2) always runs at every depth
881+
2. Output authorization (step 6) runs at the outermost call
882+
3. `CheckSecurityInterceptor` (Safety Net) runs on the final HTTP response
883+
884+
**Important:** Code running at depth > 0 (cron jobs, queue consumers, event handlers outside the HTTP cycle) must NOT return data directly to external consumers without either an outer depth-0 `process()` call or manual `checkRights` — the output rights check is skipped at depth > 0.
885+
886+
See [process() Performance Optimization](process-performance-optimization.md) for details.
887+
866888
### Key Options
867889

868890
| Option | Type | Default | Effect |
869891
|--------|------|---------|--------|
870892
| `force` | boolean | `false` | Disables checkRights, checkRoles, removeSecrets, bypasses role guard plugin |
871893
| `raw` | boolean | `false` | Disables prepareInput and prepareOutput entirely |
872894
| `checkRights` | boolean | `true` | Enable/disable authorization checks |
873-
| `populate` | object | - | Field selection for population |
895+
| `populate` | object | - | Field selection for population (overrides nested skip) |
874896
| `currentUser` | object | from request | Override the current user |
897+
| `debugProcessInput` | boolean | `false` | Config flag: log when prepareInput changes the input type (performance cost) |
875898

876899
### Alternative: processResult()
877900

@@ -883,7 +906,7 @@ const doc = await this.mainDbModel.findById(id).exec();
883906
return this.processResult(doc, serviceOptions);
884907
```
885908

886-
`processResult()` handles population and `prepareOutput()` only. Security is handled by the Safety Net (Mongoose plugins for input, interceptors for output).
909+
`processResult()` handles population and `prepareOutput()` only. It does **not** perform authorization checks (`checkRights`). Security is handled by the Safety Net (Mongoose plugins for input, interceptors for output). If called outside an HTTP request cycle (cron, queue), call `checkRights` manually before returning data to external consumers.
887910

888911
---
889912

0 commit comments

Comments
 (0)