@lenne.tech/nest-server 11.32.3 → 11.33.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 +16 -0
- package/.claude/rules/configurable-features.md +11 -3
- package/.claude/rules/testing.md +101 -1
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +58 -3
- package/bin/migrate.js +13 -3
- package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
- package/dist/core/common/helpers/content-disposition.helper.js +14 -0
- package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.d.ts +21 -2
- package/dist/core/common/helpers/file.helper.js +69 -13
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
- package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
- package/dist/core/common/helpers/project-name.helper.js +38 -0
- package/dist/core/common/helpers/project-name.helper.js.map +1 -0
- package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
- package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
- package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
- package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
- package/dist/core/common/services/core-cron-jobs.service.js +278 -27
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
- package/dist/core/common/services/core-redis-pubsub.js +112 -0
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
- package/dist/core/common/services/core-redis.service.d.ts +25 -0
- package/dist/core/common/services/core-redis.service.js +154 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -0
- package/dist/core/common/services/core-s3.service.d.ts +51 -0
- package/dist/core/common/services/core-s3.service.js +261 -0
- package/dist/core/common/services/core-s3.service.js.map +1 -0
- package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
- package/dist/core/common/services/rate-limit-store.d.ts +61 -0
- package/dist/core/common/services/rate-limit-store.js +235 -0
- package/dist/core/common/services/rate-limit-store.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
- package/dist/core/modules/ai/services/core-ai.service.js +24 -20
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +4 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +15 -3
- package/dist/core/modules/file/core-file.controller.js +90 -20
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
- package/dist/core/modules/file/core-file.resolver.js +23 -16
- package/dist/core/modules/file/core-file.resolver.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +41 -7
- package/dist/core/modules/file/core-file.service.js +227 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
- package/dist/core/modules/file/file-metadata.helper.js +33 -0
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
- package/dist/core/modules/file/file-roles.helper.js +53 -0
- package/dist/core/modules/file/file-roles.helper.js.map +1 -0
- package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
- package/dist/core/modules/file/file-storage.helper.js +65 -0
- package/dist/core/modules/file/file-storage.helper.js.map +1 -0
- package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
- package/dist/core/modules/file/filesystem-file.helper.js +77 -0
- package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
- package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
- package/dist/core/modules/file/s3-file.helper.js +81 -0
- package/dist/core/modules/file/s3-file.helper.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
- package/dist/core/modules/hub/core-hub.controller.js +24 -22
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
- package/dist/core/modules/hub/hub-buffer.js +101 -0
- package/dist/core/modules/hub/hub-buffer.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
- package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
- package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
- package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +3 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +55 -8
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
- package/dist/core/modules/migrate/mongo-state-store.js +98 -9
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
- package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +3 -4
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.controller.js +29 -3
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
- package/dist/core/modules/tus/core-tus.service.js +182 -28
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
- package/dist/core/modules/tus/tus-redis-locker.js +87 -0
- package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -0
- package/dist/core/modules/tus/tus.module.js +30 -4
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +18 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +1 -12
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +5 -1
- package/dist/server/modules/file/file.service.js +12 -3
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.d.ts +5 -1
- package/dist/server/modules/user/avatar.controller.js +26 -10
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.js +3 -2
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +6 -2
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +6 -13
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +57 -8
- package/docs/security-overrides.md +30 -5
- package/migration-guides/11.32.3-to-11.32.4.md +335 -0
- package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
- package/package.json +74 -42
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +201 -20
- package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
- package/src/core/common/helpers/project-name.helper.ts +71 -0
- package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
- package/src/core/common/interfaces/server-options.interface.ts +439 -2
- package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
- package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
- package/src/core/common/services/core-cron-jobs.service.ts +589 -47
- package/src/core/common/services/core-redis-pubsub.ts +183 -0
- package/src/core/common/services/core-redis.service.ts +249 -0
- package/src/core/common/services/core-s3.service.ts +465 -0
- package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
- package/src/core/common/services/rate-limit-store.ts +492 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
- package/src/core/modules/ai/services/core-ai.service.ts +33 -24
- package/src/core/modules/auth/core-auth.module.ts +7 -1
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
- package/src/core/modules/file/README.md +324 -41
- package/src/core/modules/file/core-file.controller.ts +267 -19
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +482 -16
- package/src/core/modules/file/file-metadata.helper.ts +142 -0
- package/src/core/modules/file/file-roles.helper.ts +110 -0
- package/src/core/modules/file/file-storage.helper.ts +163 -0
- package/src/core/modules/file/filesystem-file.helper.ts +184 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
- package/src/core/modules/file/s3-file.helper.ts +209 -0
- package/src/core/modules/hub/README.md +48 -0
- package/src/core/modules/hub/core-hub.controller.ts +18 -16
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
- package/src/core/modules/hub/hub-buffer.ts +198 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
- package/src/core/modules/migrate/README.md +68 -6
- package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +144 -10
- package/src/core/modules/migrate/migration-runner.ts +10 -1
- package/src/core/modules/migrate/mongo-state-store.ts +179 -11
- package/src/core/modules/system-setup/README.md +7 -2
- package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
- package/src/core/modules/tenant/README.md +21 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
- package/src/core/modules/tenant/core-tenant.module.ts +8 -4
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
- package/src/core/modules/tus/README.md +35 -2
- package/src/core/modules/tus/core-tus.controller.ts +64 -12
- package/src/core/modules/tus/core-tus.service.ts +358 -34
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
- package/src/core/modules/tus/tus-redis-locker.ts +139 -0
- package/src/core/modules/tus/tus.module.ts +78 -6
- package/src/core.module.ts +26 -1
- package/src/index.ts +10 -0
- package/src/main.ts +7 -1
- package/src/server/modules/file/file.controller.ts +7 -3
- package/src/server/modules/file/file.resolver.ts +5 -17
- package/src/server/modules/file/file.service.ts +42 -3
- package/src/server/modules/user/avatar.controller.ts +39 -9
- package/src/server/modules/user/user.model.ts +9 -3
- package/src/server/modules/user/user.module.ts +12 -3
- package/src/server/modules/user/user.service.ts +20 -17
- package/src/test/README.md +7 -2
- package/src/core/common/helpers/logging.helper.spec.ts +0 -61
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
- package/src/core/common/services/brevo.service.spec.ts +0 -266
- package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
- package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
|
@@ -38,6 +38,19 @@ Key areas: JWT, MongoDB, GraphQL, email, security, static assets
|
|
|
38
38
|
| **Security** | Response/security interceptors, input validation pipes |
|
|
39
39
|
| **Scalars** | Custom GraphQL scalars (Date, JSON, Any) |
|
|
40
40
|
| **Services** | CRUD operations, email (Mailjet/SMTP), template rendering |
|
|
41
|
+
| **Central infrastructure (optional)** | `CoreRedisService` + `CoreRedisPubSub` (`redis` config), `CoreS3Service` (`s3` config), `RateLimitStore`, the `installGracefulShutdown()` helper. All inert without their config — see below |
|
|
42
|
+
|
|
43
|
+
### Optional central infrastructure
|
|
44
|
+
|
|
45
|
+
`CoreRedisService` and `CoreS3Service` are always provided and exported by `CoreModule`, but stay
|
|
46
|
+
**inert unless configured** (`redis` / `s3`, "presence implies enabled"). Their client libraries are
|
|
47
|
+
OPTIONAL peer dependencies, lazy-imported at bootstrap, so a project that uses neither installs
|
|
48
|
+
nothing extra. They exist so process-local state can become shared state when a deployment runs more
|
|
49
|
+
than one replica: rate-limit counters, cron deduplication, GraphQL subscriptions, tenant-cache
|
|
50
|
+
invalidation, Hub collectors and file storage all pick the distributed path automatically when the
|
|
51
|
+
config is present, and keep their previous single-process behaviour when it is not. Inject them with
|
|
52
|
+
`@Optional()` and always keep the non-configured fallback path — that is the pattern every core
|
|
53
|
+
consumer follows. See `.claude/rules/configurable-features.md` for the full option reference.
|
|
41
54
|
|
|
42
55
|
## Core Modules (`src/core/modules/`)
|
|
43
56
|
|
|
@@ -98,6 +111,8 @@ Whether such a cycle throws depends on **which module the graph is entered throu
|
|
|
98
111
|
|
|
99
112
|
Repo-wide cycles went from **10 → 5**, and **every DI token in `src/core/` now lives in an import-free leaf**. The five that remain are, per an SWC-emit audit, **not runtime cycles at all** — type-only imports that madge reports but both compilers erase (their emits are empty).
|
|
100
113
|
|
|
114
|
+
Since 11.33.0 madge reports a **sixth**: `server/modules/user/user.module.ts ↔ server/modules/file/file.module.ts`. That one IS a real runtime cycle, and it is deliberate — the avatar upload needs `FileService` to reach the central file storage while `FileModule` already imported `UserModule`. It is a NestJS *module* cycle, not a TDZ hazard: both sides declare it with `forwardRef()`, and nothing on either side dereferences the other at class-definition time. It lives in `src/core/`-external `src/server/`, which vendor consumers do not receive.
|
|
115
|
+
|
|
101
116
|
Both invariants are enforced by `tests/unit/import-cycle-invariants.spec.ts`, which fails if a token reappears in a `*.module.ts` / `*.service.ts` or if a leaf grows an import. That matters, because the guard below catches the *crash*, not the *disarming* of a safety property — those are different things, and only the second one is silent.
|
|
102
117
|
|
|
103
118
|
| Module | Token / type leaf |
|
|
@@ -107,6 +122,7 @@ Both invariants are enforced by `tests/unit/import-cycle-invariants.spec.ts`, wh
|
|
|
107
122
|
| `tus` | `tus.constants.ts` (`TUS_CONFIG`) |
|
|
108
123
|
| `tenant` | `core-tenant.enums.ts` |
|
|
109
124
|
| `auth` | `interfaces/auth-provider.interface.ts` |
|
|
125
|
+
| `common/services` | `core-cron-jobs.registry.ts` (the cron infrastructure refs `CoreCronJobs` reads — `import type` only, so its emit is empty). Unlike the better-auth registry it IS barrel-exported: `setCronJobsInfrastructure()` / `getCronJobsInfrastructure()` are public, because a project may register its own connection or Redis service |
|
|
110
126
|
| `common/helpers` | `id.helper.ts` (ID cluster, out of `db.helper`) + `clone.helper.ts` (`clone`/`deepFreeze`, out of `input.helper`) |
|
|
111
127
|
| `common/inputs` | `FilterInput` + `CombinedFilterInput` merged into `filter.input.ts` — declaration order is load-bearing |
|
|
112
128
|
| `common/decorators` | `restricted.decorator` is on **zero** cycles; its exports are hoisted `function` declarations (TDZ-immune) as defense in depth |
|
|
@@ -248,8 +248,8 @@ This pattern is currently applied to:
|
|
|
248
248
|
|
|
249
249
|
| Feature | Config Path | Pattern | Default Values |
|
|
250
250
|
|---------|-------------|---------|----------------|
|
|
251
|
-
| Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
|
|
252
|
-
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
|
|
251
|
+
| Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Counters live in a `RateLimitStore`: `RedisRateLimitStore` when `redis` is configured (limit enforced EXACTLY across replicas instead of `max × replicas`), else `InMemoryRateLimitStore` (previous behavior). On a Redis outage it degrades to the in-memory counter and logs once per transition — never a 500, never "allowed". `check()` / `reset()` / `clear()` are ASYNC since 11.33.0; `getStats().activeEntries` is `-1` on the Redis store |
|
|
252
|
+
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Same `RateLimitStore` selection, degradation and async signatures as the Legacy Auth row (namespace `better-auth`) |
|
|
253
253
|
| BetterAuth JWT Plugin | `betterAuth.jwt` | Boolean Shorthand | `expiresIn: '15m'` |
|
|
254
254
|
| BetterAuth 2FA Plugin | `betterAuth.twoFactor` | Boolean Shorthand | `appName: 'Nest Server'` |
|
|
255
255
|
| BetterAuth Passkey Plugin | `betterAuth.passkey` | Boolean Shorthand | `rpName: 'Nest Server'` |
|
|
@@ -265,12 +265,20 @@ This pattern is currently applied to:
|
|
|
265
265
|
| Response Model Interceptor | `security.responseModelInterceptor` | Boolean Shorthand | `true` (enabled), `debug: false` |
|
|
266
266
|
| Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
|
|
267
267
|
| Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
|
|
268
|
-
| 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: `
|
|
268
|
+
| 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: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing |
|
|
269
269
|
| 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) |
|
|
270
270
|
| 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 |
|
|
271
271
|
| JSONTransport Production Guard | `email.smtp` with `jsonTransport` | Runtime Guard | Throws `Error` when `email.smtp` has a truthy `jsonTransport` property in `production` or `staging` environments (read from config `env` field). JSONTransport silently discards all outgoing mail — the guard prevents accidental misconfiguration that causes password-reset, 2FA, and verification emails to vanish. Use `{ jsonTransport: true }` only in CI/e2e/local environments |
|
|
272
272
|
| Cookies | `cookies` | Boolean Shorthand (default true) | `true` (enabled), `exposeTokenInBody: false`. When enabled: loads `cookie-parser`, sets CORS `credentials: true`, sets signed httpOnly session cookies. When `exposeTokenInBody: true`: token stays in response body alongside cookies (for hybrid JWT+Cookie auth). JWT via `Authorization: Bearer` always works independently. **BetterAuth cookie name (since v11.27.6):** `createBetterAuthInstance()` pins `advanced.useSecureCookies: false` so BetterAuth's native handlers read the same UNPREFIXED `<cookiePrefix>.session_token` the helper writes (fixes a `401` split-brain on 2FA/passkey/`/token`); the `Secure` attribute is still applied on an `https://` baseURL via `advanced.defaultCookieAttributes`. Opt back into the `__Secure-` prefix with `betterAuth.options.advanced.useSecureCookies: true` only when BetterAuth manages cookies entirely |
|
|
273
273
|
| CORS | `cors` | Boolean Shorthand | `enabled: true`, `allowAll: false`, `deriveAppUrl: true`. Origins come from `appUrl`/`baseUrl`, resolved by the shared `resolveServerUrls()` helper (`cookies.helper.ts`) that ALL three CORS layers use (GraphQL, REST, BetterAuth `trustedOrigins`) — they can no longer drift. `appUrl` resolution: explicit → derived from a **host-split** localhost `baseUrl` (its `api.` label strips to a sibling host: `https://api.crm.localhost` → `https://crm.localhost`, as served by `lt dev up`; the port is preserved) → localhost default (`http://localhost:3001`, only for `env: local`/`ci`/`e2e` with a **port-split** localhost `baseUrl` — one host, API `:3000`, app `:3001`; `https://api.localhost` strips to the bare `localhost` the API already answers on and is therefore a port split, not a host split) → derived from `baseUrl` by stripping a leading `api.` label (`https://api.example.com` → `https://example.com`). **Security:** the derived origin receives credentialed CORS; set `deriveAppUrl: false` when the apex domain is not trusted, then list the frontend origin via `appUrl`/`allowedOrigins` (a host-split localhost `baseUrl` then falls back to the localhost default). The derivation never yields a bare TLD (`https://api.dev` unchanged) and never emits the opaque `null` origin (non-http(s) `baseUrl` passes through verbatim). `allowAll: true` mirrors the request origin for REST/GraphQL, but BetterAuth's `trustedOrigins` still resolve to `[appUrl]` (+ passkey origins) — an origin check has no "allow everything" mode, so a separately hosted frontend must appear in `appUrl`/`allowedOrigins` (or set `betterAuth.trustedOrigins` explicitly). `enabled: false` disables CORS on all layers including BetterAuth (`trustedOrigins: []`, which still trusts BetterAuth's own `baseURL`). Explicit `betterAuth.trustedOrigins` always takes precedence |
|
|
274
|
+
| Central Redis | `redis` | Boolean Shorthand + Presence Implies Enabled | `host: 'localhost'`, `port: 6379`, `db: 0`, `keyPrefix: <package.json name, slugified>` (per APPLICATION, not per framework — a constant default silently collides when two apps share one Redis; set it explicitly when sharing IS intended); `url` (takes precedence over host/port/db/credentials), `username`/`password`, `options` (passed to the ioredis constructor). `true` / `{}` enables with defaults; `{ enabled: false }` pre-configures without enabling; **absent = every consumer keeps its process-local fallback** (that fallback is the whole backward-compatibility story of 11.33.0). Requires the OPTIONAL peer `ioredis` — configured-but-missing **fails the boot** with a named error rather than crashing on first use. `keyPrefix` is applied by the framework per key, NOT as ioredis `keyPrefix` (that would collide with BullMQ's own prefix). One `CoreRedisService` serves all features: shared client (`getClient()`), one cached subscriber (`getSubscriber()` — a subscribing client cannot run commands), dedicated connections (`createClient(label)`); all are tracked and quit on shutdown. Switches on automatically: exact cross-replica rate limits via `RedisRateLimitStore` (see the Legacy Auth / BetterAuth / AI rate-limit rows), cron dedup (see Cron Job Deduplication row), `CoreRedisPubSub` as `PUB_SUB` for cluster-wide GraphQL subscriptions (**payloads must be JSON-serializable — `Date`, class instances, `Map`/`Set`, `undefined` do not survive**), tenant-cache invalidation broadcast, Hub collector mirroring, MCP session registry (turns a wrong-replica request from a misleading `404` into a `409` — `/ai/mcp` still REQUIRES sticky sessions, sessions are not portable) |
|
|
275
|
+
| File Access Roles | `file.downloadRoles`, `file.uploadRoles`, `file.deleteRoles` | Config-Driven Role List | `[RoleEnum.ADMIN]` each. **These are the only breaking change a single-replica project gets in 11.33.0** — six members moved from `@Roles(S_EVERYONE)` to these knobs: `GET /files/id/:id`, `GET /files/:filename` and `getFileInfo` (`downloadRoles`), `uploadFile` / `uploadFiles` (`uploadRoles`), `deleteFile` (`deleteRoles`). Plain role STRINGS, not `RoleEnum` members, so project roles work (`['company-admin', 'editor']`). Applied at boot by `applyFileRoles()` via `Reflect.defineMetadata('roles', …)` on the base-class methods — the same runtime mechanism `CorePermissionsModule` uses, because the value is only known from config. **`[]`, a non-array, or an array holding a non-string is REJECTED with a warning and the default applies** — an all-empty role set reads to the guards as "no roles required" and would OPEN the route, the exact opposite of the intent. **ADMIN is always unioned in**: both `CoreFileController` and `CoreFileResolver` carry a class-level `@Roles(RoleEnum.ADMIN)` and the guards UNION class + handler metadata, so these knobs can grant but never exclude admins. Both classes also carry `@SkipTenantCheck()` — GridFS and the S3 metadata collection are reached outside Mongoose, so `mongooseTenantPlugin` never scopes them and a role name alone cannot express a per-tenant rule; roles resolve against `user.roles`, never `membership.role`. **A subclass that OVERRIDES a member opts out permanently** (decorator metadata lives on the function object, and an override is a different function) — inherit the member instead. Roles are the coarse filter only; per-file rules belong in `CoreFileService.checkRights()`, which now receives `currentUser` and can read raw metadata via `getRawFileInfo()`. Implementation: `src/core/modules/file/file-roles.helper.ts` (`FILE_ROLE_DEFAULTS`, `applyFileRoles`) |
|
|
276
|
+
| TUS Roles | `tus.roles` | Config-Driven Role List | `[RoleEnum.S_USER]` (was `S_EVERYONE` — breaking). A TUS upload writes into the SAME store the download routes guard, and the termination extension (on by default) can delete from it, so anonymous writes into a store only privileged callers may read is the wrong way round. Applied by `TusModule.applyRoles()` onto the registered controller class **and** onto `handleTus` / `handleTusWithId`. Same rejection rule as the file roles: `[]` / non-array / non-string → warning + `DEFAULT_TUS_CONFIG.roles`. **`OPTIONS` is deliberately exempt**: `handleTusOptions` / `handleTusOptionsWithId` keep their own handler-level `@Roles(RoleEnum.S_EVERYONE)`, because that is the CORS preflight — browsers send it WITHOUT credentials, and it returns server capabilities only. Gating it would make every browser upload fail before the first byte. `CoreTusController` also carries `@SkipTenantCheck()`. A custom controller is covered as long as it INHERITS the handlers; one that re-declares `@All()` / `@Roles()` carries its own metadata and thereby opts out — the documented way to hard-code a policy config must not be able to change. **Set `roles: [RoleEnum.S_EVERYONE]` explicitly if you accept attachments on a public form.** Implementation: `src/core/modules/tus/tus.module.ts`, `src/core/modules/tus/interfaces/tus-config.interface.ts` |
|
|
277
|
+
| File Storage | `file.storage` + `file.storageDir` + `s3` | Explicit Enum with DERIVED default | `'filesystem' \| 'gridfs' \| 's3'`. **Unset → derived**, most capable first: `'s3'` when `s3.bucket` is set, else `'gridfs'` when `mongoose.uri` is set, else `'filesystem'`. **Set → enforced**: an unavailable store FAILS THE BOOT (`assertFileStorageAvailable()`), it never falls back — a silent fallback puts files in a store the operator does not believe they are in, unrecoverably. A DERIVED driver is enforced too (`s3.bucket` set but `s3Service` not forwarded to `super()` → boot error). Metadata always lives in MongoDB whichever driver holds the bytes (`fs.files` / `s3-files` / `filesystem-files`) — it has to stay queryable for `findFileInfo()` and `checkRights()`. Reads consult ALL stores so switching drivers is forward-only with no migration; writes go to the active driver only. `'filesystem'` is pod-local: not shared between replicas, lost on restart unless `storageDir` is a mounted volume. `s3`: `bucket` (required — the one thing S3 cannot default, hence the eligibility test), `region: 'us-east-1'`, `forcePathStyle: false`, `stagingBucket: bucket`, `presignedDownloads: false` (`true`/`{}` → `expiresInSeconds: 300`), `endpoint` (MinIO/RustFS), `accessKeyId`/`secretAccessKey` (omit → AWS default credential chain), `enabled: false` to pre-configure. Requires the OPTIONAL peer `@aws-sdk/client-s3`, plus `@aws-sdk/s3-request-presigner` for presigned downloads. `presignedDownloads` makes `GET /files/id/:id` answer `302` to a time-limited S3 URL instead of streaming — the URL is a bearer capability, authorized once at issue time. The resolved driver is logged at boot (`[CoreFileStorage] File storage: …`) |
|
|
278
|
+
| TUS S3 Staging | `tus.s3Staging` | Explicit Boolean (default ON when S3 configured) | `true` when `s3` is configured, otherwise inert. Stages in-progress uploads in `s3.stagingBucket` via `@tus/s3-store` instead of `tus.uploadDir` on local disk, so resumable uploads survive replica restarts and need no sticky sessions. Set `false` to force local disk. Missing OPTIONAL peer `@tus/s3-store` → warning + fall back to local disk (NOT a boot failure). **Give the staging bucket a lifecycle rule expiring incomplete multipart uploads** — the framework's own expiration cleanup is skipped in S3 mode (S3 is the right place for that policy), so aborted uploads otherwise accumulate parts nothing removes |
|
|
279
|
+
| Cron Job Deduplication | per job: `distributed` in `CronJobConfig` | Explicit Boolean | **`true` when `redis` is configured, otherwise `false`** — a single-replica project that upgrades must not silently gain a `cron-locks` collection, a lease write per tick, and a new way for a tick to be skipped. A Redis-less multi-replica fleet opts in per job with `distributed: true` (MongoDB lease). Mechanism: BullMQ job scheduler when Redis + the OPTIONAL peer `bullmq` are present AND `cronTime` is a string without `utcOffset`; otherwise local timer + lease (Redis `SET NX`, else a TTL-indexed `cron-locks` document). Tick lease TTL 3600 s. **Leases fail open** — an unreachable lease store runs the tick everywhere rather than stopping all scheduled work fleet-wide. **`runOnInit` (default `true`) deduplicates over a FIXED per-job key with a 300 s TTL**, because replicas do not share a boot instant: replicas booting within 5 min run the startup tick once between them, and a replica restarting inside that window SKIPS its startup tick — set `distributed: false` on jobs whose `runOnInit` work is per-process (warming a process-local cache). No constructor change needed: `CoreModule` fills `core-cron-jobs.registry.ts` via `CoreCronJobsInitializer` and `CoreCronJobs` reads it lazily; explicit `{ connection, redisService }` in `CoreCronJobsOptions` wins. With neither source it warns once and every replica runs every tick |
|
|
280
|
+
| Shutdown Delay | `shutdownDelayMs` | Numeric Sentinel — Family B (`0` = off) | `0` (default, no delay, no log). Waits N ms **in the SIGTERM/SIGINT handler, before `close()` is entered**, so a load balancer can finish deregistering while the instance is still fully healthy. NOT a lifecycle hook: `close()` runs `onModuleDestroy` → `beforeApplicationShutdown` → dispose → `onApplicationShutdown`, so a delay in `beforeApplicationShutdown` would wait with every module already torn down while the socket still accepts — worse than no delay. **Requires `installGracefulShutdown(app)` in main.ts, which REPLACES `server.enableShutdownHooks()`** — keeping both makes Nest close the app in parallel with the wait, so the delay silently never happens. At delay `0` the helper IS `enableShutdownHooks()`. **Keep the value well below the orchestrator grace period AND leave room for the drain that follows**: Compose `stop_grace_period` 10s, Kubernetes `terminationGracePeriodSeconds` 30s, and `installProcessDiagnostics()` force-exits after 30s — exceed any and the process is SIGKILLed mid-wait with no hook running. Warns above `10000`, capped at `60000`. Non-numeric / negative values behave like `0` |
|
|
281
|
+
| Trust Proxy | `trustProxy` | Explicit Value (pass-through, Express default) | `false` (Express's own default — the forwarded chain is not trusted). Passed verbatim to `app.set('trust proxy', …)` by `CoreTrustProxyInitializer`, a `CoreModule` provider, so a consumer inherits it by upgrading and needs no `main.ts` edit. Accepts `false` / a hop count (`1`, `2`) / `'loopback'` / a subnet list — **not** Express's predicate function: the value must survive `NEST_SERVER_CONFIG` / `NSC__*` (JSON) and the ConfigService deep clone. **This is what makes `request.ip` correct, and every IP-keyed rate limit depends on it**: unset behind Caddy/nginx/an ingress, `req.ip` is the PROXY address for every request, so all clients share ONE bucket and `auth.rateLimit.max` throttles everybody at once — exactly fleet-wide once `redis` is configured. Trusting MORE hops than exist is the opposite failure: a client prepends its own header entry and picks a fresh bucket per request. Applied at module init (inside `app.init()`/`listen()`, i.e. AFTER `main.ts`), so a configured value wins over a hand-written `app.set()`; an UNSET value is never applied, which keeps `app.set('trust proxy', fn)` in `main.ts` available as the escape hatch for the predicate form. **Unset + an IP-keyed limiter enabled (`auth.rateLimit` / `betterAuth.rateLimit`) logs a boot warning naming the shared-bucket consequence**; `trustProxy: false` is the explicit "nothing proxies me" answer that silences it. The AI limiter keys on the user id and is unaffected |
|
|
274
282
|
| AI Assistant | `ai` | Presence Implies Enabled | Core: `maxIterations: 5`, `defaultMode: 'auto'` (or `'plan'`), `rateLimit` (presence implies enabled: `max: 20`, `windowSeconds: 60`), `systemPrompt`, `documentation` (injected into the system prompt), `encryptionSecret`. **DB-backed LLM connections** (`aiConnections`, admin CRUD) with AES-256-GCM-encrypted API keys (`AiCryptoService`, secret from `ai.encryptionSecret` / `NSC__AI__ENCRYPTION_SECRET` / `SECRETS_ENCRYPTION_KEY`; `apiKeyEncrypted` is a global `secretFields` entry, never returned — only `hasApiKey`); optional `defaultConnection` one-time seed. **Provider abstraction** (`ILlmProvider`, default `OpenAiCompatibleProvider` for any OpenAI-compatible endpoint via `fetch`; per-connection `supportsNativeTools`/`supportsJsonResponse` capabilities, emulated tool calling when native tools are unavailable). **Tool registry** (`AiToolRegistry`, tools self-register, role-filtered; tools may be `mutating`/`destructive` and define `authorize()` for pre-flight data-level checks). **Plan mode** (`input.mode: 'plan'`): full plan → pre-flight authorize ALL steps → all-or-nothing execution with a translated (de/en) error when any step is not permitted. **Confirmation policy**: `confirmation.mutating: { default, enforced }` + client `input.requireConfirmation` (ignored when enforced); `destructive` always confirms. **Client metadata** (`input.metadata`: URL/nav/console logs, untrusted+capped). **Multi-turn conversations** (`aiConversations`, owner-scoped). **SSE streaming** (`POST /ai/stream`). **Audit** (`audit: false` → persist to `aiInteractions`, admin-readable). **Token budgets** (`budget: { period: 'day'|'month'|'none', user: { maxTokens?, maxPrompts? }, tenant: { maxTokens?, maxPrompts? } }`, requires audit): per-user AND per-tenant limits with config defaults; admins override per user/tenant at runtime (`aiBudgetLimits`, `CoreAiBudgetService`). Resolution: override → default → unlimited (missing/0 = unlimited). Enforced before the run (HTTP 429 + translated). Each response carries a compact `budget` summary (promptTokens, usedTokens, remainingTokens, resetAt); full breakdown via `aiUsage` query / `GET /ai/usage`. **Self-optimizing prompts**: the system prompt is assembled from keyed fragments (`CoreAiPromptBuilderService` ships built-in defaults; works with zero rows). Admin-editable overrides per slot (`aiSlots`, admin CRUD, `/ai/slots`) scoped by `key`/`locale`/`capability`/`tenantId`, with tenant override/reset semantics and placeholder tokens resolved at run time via the placeholder registry. **Governed learning loop** (`promptLearning: { enabled: true, autoApply: false }`): tool errors record `suggested` hints (`aiPromptHints`, admin CRUD, `/ai/prompt-hints`) that only reach the prompt once admin-approved (or auto-approved when `autoApply`); hints only ADD guidance, never relax permissions. **Context window** (`contextWindow`, default 8192; auto-detected per connection via `ILlmProvider.detectContextWindow()` — Ollama `/api/show` probe / known-model table / Claude alias — and persisted): per-user/session history is trimmed (oldest non-system turns dropped, last truncated) and tool-results capped to `maxToolResultChars` (default 12000) so a session never overflows the model. A connection's window can be seeded via `ai.defaultConnection.contextWindow` (validated: a non-positive/non-integer value is dropped with a warning). **Capability drift check** (`capabilityDriftCheck`, default `false`): opt-in boot self-check that probes each enabled connection with an EXPLICIT `supportsNativeTools`/`supportsJsonResponse` (built with those flags cleared so the endpoint is actually re-probed) and logs a warning on mismatch — the stored value is never changed. OFF by default because it makes outbound calls to the LLM endpoints on every boot; also skipped in the ci/e2e runners. **Deferred tool schemas** (`deferToolSchemas`, default `false`): the system-prompt tool catalog then lists only tool NAMES + descriptions instead of full JSON schemas, and the model fetches a schema on demand via the built-in `search_tools` meta-tool — with a large registry the schemas alone can dominate a small context window. `deferToolSummaryChars` (default `0` = untruncated) additionally caps each description in that DEFERRED catalog: whole sentences up to the cap (always at least the first), word-boundary cut when the first sentence already exceeds it, and a `…` marker appended ON TOP of the cap. The default of `0` keeps the saving opt-in, so enabling `deferToolSchemas` alone never changes what a description says; set roughly 200–400 alongside it to actually reclaim the context. Both apply to EMULATED providers only — a connection with `supportsNativeTools: true` receives every full description + schema via `buildToolSchemas()` regardless, so truncation and the banner are skipped there rather than asserting a cut the tool payload contradicts. The omitted tail is where preconditions and role restrictions usually live — the catalog banner tells the model to fetch the full text via `search_tools` first, but this is model GUIDANCE only: authorization is enforced server-side by the registry's role filter (`forUser()`), the execution-time re-check, and the `mutating`/`destructive` flags read by the confirmation gate — never by what the catalog shows. (`AiTool.authorize()` runs in PLAN MODE only; in auto mode and over MCP, data-level checks must live inside `execute()`.) **MCP server** (`mcp: false` → `/ai/mcp` Streamable HTTP, Bearer auth, lazy `@modelcontextprotocol/sdk`; `mcp: { oauth: true, oauthSecret }` adds OAuth 2.1 — HMAC tokens + PKCE S256 + dynamic registration via `mountAiMcpOAuth(app)` in main.ts). Overrides via `CoreModule.forRoot(env, { ai: { budgetService, connectionResolver, connectionService, controller, conversationService, interactionService, mcpClientService, modeService, placeholderRegistry, preferenceService, promptBuilder, promptHintService, promptService, resolver, service, slotService, toolGrantService, toolPolicyService } })` |
|
|
275
283
|
|
|
276
284
|
## Module Override Pattern (via `ICoreModuleOverrides`)
|
package/.claude/rules/testing.md
CHANGED
|
@@ -6,14 +6,73 @@
|
|
|
6
6
|
|
|
7
7
|
| Runner | Config | Test files | Needs MongoDB |
|
|
8
8
|
|--------|--------|-----------|:-------------:|
|
|
9
|
-
| Unit | `vitest.config.ts` | `
|
|
9
|
+
| Unit | `vitest.config.ts` | `tests/unit/**/*.spec.ts` | No |
|
|
10
10
|
| E2E | `vitest-e2e.config.ts` | `tests/**/*.e2e-spec.ts`, `tests/stories/**/*.story.test.ts` | Yes |
|
|
11
11
|
|
|
12
|
+
**Test files NEVER live in `src/`.** Not a style preference — `src/` is this framework's shipping
|
|
13
|
+
artifact: `package.json` → `files` ships all of `src` recursively into the npm tarball, and
|
|
14
|
+
vendor-mode consumers copy `src/core/` into their own tree as first-class project code (the CLI's
|
|
15
|
+
`convertCloneToVendored` applies no spec filter). A co-located spec therefore reaches every consumer
|
|
16
|
+
as a test file they neither run nor maintain, re-delivered on every core update. Co-location is a
|
|
17
|
+
fine default for an application; for a library whose `src/` IS the delivery, separation wins.
|
|
18
|
+
|
|
19
|
+
Enforced twice: the unit runner's glob no longer looks at `src/` at all, and
|
|
20
|
+
`tests/unit/test-file-placement.spec.ts` fails on any `.spec.ts`/`.test.ts` found there — so a
|
|
21
|
+
misplaced test surfaces as a failure rather than as silently skipped coverage.
|
|
22
|
+
|
|
23
|
+
| Kind | Where | Suffix |
|
|
24
|
+
|------|-------|--------|
|
|
25
|
+
| Unit | `tests/unit/` | `*.spec.ts` |
|
|
26
|
+
| E2E / integration | `tests/` | `*.e2e-spec.ts` |
|
|
27
|
+
| Story (e2e-grade) | `tests/stories/` | `*.story.test.ts` |
|
|
28
|
+
| Type-only (compiled, never run) | `tests/types/` | `*.type-test.ts` |
|
|
29
|
+
|
|
12
30
|
A file matching neither pattern would run nowhere. `tests/unit/test-file-routing.spec.ts` asserts
|
|
13
31
|
that every `*.spec.ts` / `*.test.ts` in the repo is claimed by **exactly one** runner, so a
|
|
14
32
|
mis-named suite fails the build instead of silently passing. Type-only tests
|
|
15
33
|
(`tests/types/*.type-test.ts`) are compiled by `pnpm run test:types`, never executed.
|
|
16
34
|
|
|
35
|
+
## Infrastructure containers (Redis + S3)
|
|
36
|
+
|
|
37
|
+
**Seven** e2e suites talk to a **real** Redis and/or a **real** S3-compatible store rather than a
|
|
38
|
+
mock, and they **fail loudly** when it is missing — a silently skipped infrastructure test is how an
|
|
39
|
+
untested driver ships:
|
|
40
|
+
|
|
41
|
+
| Suite | Needs |
|
|
42
|
+
|-------|-------|
|
|
43
|
+
| `tests/redis-infra.e2e-spec.ts` | Redis |
|
|
44
|
+
| `tests/redis-consumers.e2e-spec.ts` | Redis |
|
|
45
|
+
| `tests/multi-replica.e2e-spec.ts` | Redis |
|
|
46
|
+
| `tests/s3-infra.e2e-spec.ts` | S3 |
|
|
47
|
+
| `tests/file-storage-s3.e2e-spec.ts` | S3 |
|
|
48
|
+
| `tests/redis-s3-bootstrap.e2e-spec.ts` | Redis + S3 |
|
|
49
|
+
| `tests/graceful-shutdown.e2e-spec.ts` | Redis + S3 |
|
|
50
|
+
|
|
51
|
+
`tests/global-setup.ts` starts both containers automatically, so `pnpm test` works from a clean
|
|
52
|
+
machine with no manual docker command. It is idempotent (a running container is reused) and never
|
|
53
|
+
fatal: without Docker every other suite still runs, and the seven above report their own actionable
|
|
54
|
+
error.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm run test:infra # start + wait for readiness (also runs automatically)
|
|
58
|
+
pnpm run test:infra:status # what is running
|
|
59
|
+
pnpm run test:infra:down # stop and remove
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| | Redis | S3 (RustFS) |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Port | 6380 (6379 is an auth-protected TurboOps Redis on lt dev machines) | 9102 |
|
|
65
|
+
| Container | `nest-server-2985-redis` | `nest-server-2985-rustfs` |
|
|
66
|
+
|
|
67
|
+
**CI provisions its own** containers in `.github/workflows/*.yml` and sets `LT_TEST_INFRA=0` so the
|
|
68
|
+
automatic start stays out of the way. Same images, same ports — only who starts them differs. Set
|
|
69
|
+
`LT_TEST_INFRA=0` locally too if you want to manage them yourself.
|
|
70
|
+
|
|
71
|
+
**Bucket cleanup:** every suite that creates a bucket names it per run and removes it in `afterAll`
|
|
72
|
+
via `tests/helpers/s3-test-cleanup.ts`. Emptying and deleting belong together — a bucket cannot be
|
|
73
|
+
deleted while it holds objects — which is why that lives in one helper rather than per suite. Before
|
|
74
|
+
it existed, two empty buckets leaked into the store per run.
|
|
75
|
+
|
|
17
76
|
## Running Tests
|
|
18
77
|
|
|
19
78
|
```bash
|
|
@@ -49,6 +108,47 @@ pnpm run test:cleanup
|
|
|
49
108
|
- DB lifecycle (`tests/db-lifecycle.reporter.ts`): run passes → DB dropped immediately + stale run DBs from crashed/failed runs collected; run fails → DB kept for debugging. Additionally `tests/global-setup.ts` runs a **startup sweep** (shared `isStaleTestDb()` predicate, dead-PID/age guarded) — leftovers are removed when the NEXT run starts, which survives SIGKILL (check watchdog) and `--reporter` CLI overrides. An externally set `MONGODB_URI` (CI) opts out of the scheme.
|
|
50
109
|
- Run governor (`tests/e2e-run-slots.ts`): machine-wide slot dir (`<tmpdir>/lt-e2e-run-slots`) caps concurrent e2e runs across ALL lt projects/sessions (default 2 on ≥8 cores). Further runs wait, logging `[e2e-governor] waiting…` every 15s (keeps the check watchdog fed — a queued run is NOT hung). The e2e config counts foreign slots at load time and drops to low-resource mode (reduced forks, raised timeouts) when another run is active — deterministic, unlike the lagging 1-min load average (kept as second signal). Knobs: `LT_E2E_MAX_RUNS` (0 disables), `LT_E2E_SLOT_DIR`, `LT_E2E_SLOT_TIMEOUT` (fail-open).
|
|
51
110
|
- `retry: 2` (e2e) is deliberate — with `retry: 5`, one spec file with broken app/socket state ground through 6 attempts × 30s timeout × 22 tests ≈ an hour at 0% CPU (looked like a deadlock; the check watchdog killed it). Never raise retry to paper over contention.
|
|
111
|
+
- Infrastructure containers (E2E only, the **seven** specs listed under "Infrastructure containers"
|
|
112
|
+
above) round-trip against a REAL Redis and/or a REAL S3-compatible store.
|
|
113
|
+
`redis-s3-bootstrap` boots the assembled `CoreModule` with both
|
|
114
|
+
configured — the only test that covers the WIRING, which fakes and directly-constructed
|
|
115
|
+
services cannot: an unresolvable provider or a lifecycle hook that throws on a real
|
|
116
|
+
connection would pass every other spec and fail on a consumer's first `nest start`. `multi-replica` is the acceptance test for the
|
|
117
|
+
distributed features: it builds TWO independent service instances sharing one Redis and
|
|
118
|
+
asserts the properties a second replica must preserve — a scheduled tick and a startup tick
|
|
119
|
+
each run exactly once, one rate limit is enforced instead of one per replica, and a severed
|
|
120
|
+
Redis still yields a decision instead of an error. Single-instance specs against fakes cannot
|
|
121
|
+
show any of that: a limiter counting per process, or a lease key differing per instance,
|
|
122
|
+
passes them and fails here. `redis-consumers` covers the framework's own Redis consumers seen
|
|
123
|
+
from two replicas, `file-storage-s3` runs `CoreFileService` against a real bucket, and
|
|
124
|
+
`graceful-shutdown` needs real connections for `installGracefulShutdown()` to close.
|
|
125
|
+
Five of the seven preflight the connection in `beforeAll` and throw a written diagnosis —
|
|
126
|
+
`redis-infra`, `s3-infra` and `multi-replica` quote the full `docker run` line, `redis-consumers`
|
|
127
|
+
and `file-storage-s3` name the port and how to start it. So a forgotten container is a ~2s clear
|
|
128
|
+
error instead of a 43s opaque `MaxRetriesPerRequestError`. The other two fail fast without a
|
|
129
|
+
custom message: `graceful-shutdown` probes with `connectTimeout: 2000` /
|
|
130
|
+
`maxRetriesPerRequest: 1` and lets the raw connect error surface, and `redis-s3-bootstrap` has no
|
|
131
|
+
separate probe at all — its `beforeAll` IS the boot.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker run -d --name nest-server-2985-redis -p 6380:6379 redis:7.4-alpine
|
|
135
|
+
docker run -d --name nest-server-2985-rustfs -p 9102:9000 -e RUSTFS_ROOT_USER=rustfs -e RUSTFS_ROOT_PASSWORD=rustfs-secret -e RUSTFS_VOLUMES=/data rustfs/rustfs:1.0.0-rc.1 server /data
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Both tags are pinned, and these hints must stay identical to the tags in
|
|
139
|
+
`scripts/test-infra.mjs` / `.github/actions/test-infra/action.yml`. `containerMatches()` compares
|
|
140
|
+
the running container's image against the pinned one, so a hint that says `:latest` starts a
|
|
141
|
+
container the next `pnpm test` tears down and recreates.
|
|
142
|
+
|
|
143
|
+
| Service | Port | Overridable via |
|
|
144
|
+
|---------|------|-----------------|
|
|
145
|
+
| Redis | 6380 | `REDIS_HOST`, `REDIS_PORT` |
|
|
146
|
+
| RustFS (S3) | 9102 | `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` |
|
|
147
|
+
|
|
148
|
+
**Why 6380 and not 6379:** on lt dev machines 6379 is occupied by an auth-protected TurboOps
|
|
149
|
+
Redis. Binding the test container to 6380 keeps the two apart — no auth setup, no cross-wiring.
|
|
150
|
+
CI (`.github/workflows/build.yml` / `publish.yml`) maps the same ports and waits for readiness
|
|
151
|
+
before installing dependencies.
|
|
52
152
|
- Test helper: `src/test/test.helper.ts`
|
|
53
153
|
- Coverage: Collected from `src/**/*.{ts,js}`. The two runners are separate vitest processes, so
|
|
54
154
|
they write separate reports (`coverage/unit`, `coverage/e2e`) rather than overwriting each other.
|
package/CLAUDE.md
CHANGED
|
@@ -142,6 +142,7 @@ pnpm run reinit # Clean reinstall + tests + build
|
|
|
142
142
|
- `src/core/modules/` - Auth, BetterAuth, ErrorCode, File, HealthCheck, Hub, Migrate, Permissions, SystemSetup, Tus, User
|
|
143
143
|
- **Hub** - Build-free ADMIN-gated operator cockpit at `/hub` (config-gated per environment; 16 panels, runtime collectors, mailbox, admin actions). See `src/core/modules/hub/README.md`.
|
|
144
144
|
- **Permissions** - ADMIN-gated security-map report (routes + roles + `@Restricted` fields); also surfaced in the Hub's "Routes / Permissions" panel.
|
|
145
|
+
- **Optional central infrastructure** (11.33.0+) - `CoreRedisService` (`redis` config) and `CoreS3Service` (`s3` config) turn process-local state into shared state for multi-replica deployments: rate-limit counters, cron deduplication, GraphQL subscriptions, tenant-cache invalidation, Hub collectors, file storage (`file.storage: 's3'`; the driver is otherwise derived — S3 → GridFS → filesystem — and an unavailable one fails the boot rather than falling back) and TUS staging. Both are inert without their config, and their client libraries are **optional peer dependencies** (`ioredis`, `bullmq`, `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `@tus/s3-store`) — a project that uses neither installs nothing extra. When adding a distributed feature, always keep the non-configured fallback path.
|
|
145
146
|
|
|
146
147
|
See `.claude/rules/architecture.md` for detailed documentation.
|
|
147
148
|
See [`docs/REQUEST-LIFECYCLE.md`](docs/REQUEST-LIFECYCLE.md) for the complete request lifecycle, security architecture, and interceptor/decorator reference.
|
|
@@ -236,6 +237,10 @@ file is missing (default: tolerate with a warning) — see `src/core/modules/mig
|
|
|
236
237
|
|
|
237
238
|
1. **All code, comments, documentation in English**
|
|
238
239
|
2. **Run tests before completing changes** - `pnpm test`
|
|
240
|
+
2b. **Never put test files in `src/`** — unit tests go to `tests/unit/*.spec.ts`, e2e to
|
|
241
|
+
`tests/*.e2e-spec.ts`, stories to `tests/stories/*.story.test.ts`. `src/` ships in the npm
|
|
242
|
+
tarball and is copied verbatim into vendor-mode consumer projects, so a co-located spec becomes
|
|
243
|
+
part of THEIR codebase. Enforced by `tests/unit/test-file-placement.spec.ts`.
|
|
239
244
|
3. **Follow existing patterns** for consistency
|
|
240
245
|
4. **Never store S_ roles** in user.roles array
|
|
241
246
|
5. **Use Module Inheritance Pattern** for core modules
|
package/FRAMEWORK-API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lenne.tech/nest-server — Framework API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from source code on 2026-
|
|
3
|
+
> Auto-generated from source code on 2026-08-11 (v11.33.0)
|
|
4
4
|
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
|
|
5
5
|
|
|
6
6
|
## CoreModule.forRoot()
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
- `version?`: `string | undefined` — Semantic version of the running build (e.g. from package.json / meta.json).
|
|
30
30
|
- `errorCode?`: `IErrorCode | undefined` — Configuration for the error code module
|
|
31
31
|
- `execAfterInit?`: `string | undefined` — Exec a command after server is initialized
|
|
32
|
+
- `file?`: `IFileConfig | undefined` — Configuration of the file module: where the bytes live (`storage`) and who
|
|
32
33
|
- `filter?`: `{ maxLimit?: number; } | undefined` — Filter configuration and defaults
|
|
33
34
|
- `graphQl?`: `false | { driver?: ApolloDriverConfig; enableSubscriptionAuth?: boolean; maxC...` — Configuration of the GraphQL module
|
|
34
35
|
- `healthCheck?`: `{ configs?: { build?: { enabled?: boolean; }; database?: { enabled?: boolean;...` — Whether to activate health check endpoints
|
|
@@ -42,11 +43,15 @@
|
|
|
42
43
|
- `multiTenancy?`: `IMultiTenancy | undefined` (default: `undefined (disabled)`) — Multi-tenancy configuration for tenant-based data isolation.
|
|
43
44
|
- `permissions?`: `boolean | IPermissions | undefined` (default: `undefined (disabled)`) — Permissions report module (development tool).
|
|
44
45
|
- `port?`: `number | undefined` — Port number of the server
|
|
46
|
+
- `redis?`: `boolean | IRedisConfig | undefined` (default: `undefined (disabled)`) — Optional central Redis connection used by all distributed features
|
|
47
|
+
- `s3?`: `IS3Config | undefined` (default: `undefined (disabled)`) — Optional central S3-compatible object storage (AWS S3, MinIO, ...).
|
|
45
48
|
- `security?`: `{ checkResponseInterceptor?: boolean | { checkObjectItself?: boolean; debug?:...` — Configuration for security pipes and interceptors
|
|
46
49
|
- `sha256?`: `boolean | undefined` — Whether to enable verification and automatic encryption for received passwords that are not in sha256 format
|
|
50
|
+
- `shutdownDelayMs?`: `number | undefined` (default: `0 (no delay)`) — Delay in milliseconds between receiving a shutdown signal and starting the
|
|
47
51
|
- `staticAssets?`: `{ options?: ServeStaticOptions; path?: string; } | undefined` — Configuration for useStaticAssets
|
|
48
52
|
- `systemSetup?`: `ISystemSetup | undefined` — System setup configuration for initial admin creation.
|
|
49
53
|
- `templates?`: `{ engine?: string; path?: string; } | undefined` — Templates
|
|
54
|
+
- `trustProxy?`: `string | number | boolean | string[] | undefined` (default: `false (Express default — the forwarded chain is not trusted)`) — Express `trust proxy` setting — how far up the `X-Forwarded-For` chain this app believes.
|
|
50
55
|
- `tus?`: `boolean | ITusConfig | undefined` — TUS resumable upload configuration.
|
|
51
56
|
|
|
52
57
|
### IBetterAuth (type alias: IBetterAuthWithoutPasskey | IBetterAuthWithPasskey)
|
|
@@ -145,6 +150,56 @@ When `passkey` is enabled, `trustedOrigins` is required (compile-time enforcemen
|
|
|
145
150
|
- `supportsNativeTools?`: `boolean | undefined` — Native function/tool-calling support. Omit to auto-detect by probing the
|
|
146
151
|
- `supportsVision?`: `boolean | undefined` — Whether the model supports image input.
|
|
147
152
|
|
|
153
|
+
### IFileConfig
|
|
154
|
+
|
|
155
|
+
- `deleteRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to DELETE files (`deleteFile` mutation).
|
|
156
|
+
- `downloadRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to DOWNLOAD files and read file info
|
|
157
|
+
- `storage?`: `"filesystem" | "gridfs" | "s3" | undefined` (default: `derived — see above`) — Storage driver for CoreFileService. Three equivalent options:
|
|
158
|
+
- `storageDir?`: `string | undefined` (default: `'uploads/files'`) — Directory for the `'filesystem'` storage driver.
|
|
159
|
+
- `uploadRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to UPLOAD files (`uploadFile` / `uploadFiles` mutations).
|
|
160
|
+
|
|
161
|
+
### IRedisConfig
|
|
162
|
+
|
|
163
|
+
- `db?`: `number | undefined` (default: `0`) — Redis database index
|
|
164
|
+
- `enabled?`: `boolean | undefined` (default: `true (when config object is present)`) — Whether Redis is enabled.
|
|
165
|
+
- `host?`: `string | undefined` (default: `'localhost'`) — Redis host
|
|
166
|
+
- `keyPrefix?`: `string | undefined` (default: `the slugified `name` from package.json, or 'nest-server' if unreadable`) — Prefix prepended to every framework-managed Redis key
|
|
167
|
+
- `options?`: `Record<string, unknown> | undefined` (default: `undefined`) — Additional ioredis options passed through to the client constructor
|
|
168
|
+
- `password?`: `string | undefined` (default: `undefined (no auth)`) — Redis password
|
|
169
|
+
- `port?`: `number | undefined` (default: `6379`) — Redis port
|
|
170
|
+
- `url?`: `string | undefined` (default: `undefined`) — Full Redis connection URL (e.g. 'redis://user:pass@host:6379/0').
|
|
171
|
+
- `username?`: `string | undefined` (default: `undefined`) — Redis username (Redis 6+ ACL)
|
|
172
|
+
|
|
173
|
+
### IS3Config
|
|
174
|
+
|
|
175
|
+
- `accessKeyId?`: `string | undefined` (default: `undefined`) — Access key ID.
|
|
176
|
+
- `autoCreateBucket?`: `boolean | undefined` (default: `false`) — Create the configured buckets at startup when they do not exist.
|
|
177
|
+
- `bucket`: `string` — Bucket for files stored via CoreFileService
|
|
178
|
+
- `enabled?`: `boolean | undefined` (default: `true (when config object is present)`) — Whether S3 is enabled.
|
|
179
|
+
- `endpoint?`: `string | undefined` (default: `undefined`) — Custom endpoint URL for S3-compatible services (MinIO, RustFS, ...).
|
|
180
|
+
- `forcePathStyle?`: `boolean | undefined` (default: `false`) — Use path-style addressing (required by most self-hosted S3 services).
|
|
181
|
+
- `presignedDownloads?`: `boolean | { expiresInSeconds?: number; } | undefined` (default: `false`) — Serve downloads as presigned URL redirects instead of streaming
|
|
182
|
+
- `region?`: `string | undefined` (default: `'us-east-1'`) — AWS region
|
|
183
|
+
- `secretAccessKey?`: `string | undefined` (default: `undefined`) — Secret access key.
|
|
184
|
+
- `stagingBucket?`: `string | undefined` (default: `same as `bucket``) — Bucket used as staging area for resumable TUS uploads.
|
|
185
|
+
|
|
186
|
+
### ITusConfig
|
|
187
|
+
|
|
188
|
+
- `allowedHeaders?`: `string[] | undefined` (default: `[] (no additional headers needed)`) — Additional allowed HTTP headers for TUS requests (beyond
|
|
189
|
+
- `allowedTypes?`: `string[] | undefined` (default: `undefined (all types allowed)`) — Allowed MIME types for uploads.
|
|
190
|
+
- `checksum?`: `boolean | undefined` (default: `true`) — Checksum extension configuration.
|
|
191
|
+
- `concatenation?`: `boolean | undefined` (default: `true`) — Concatenation extension configuration.
|
|
192
|
+
- `creation?`: `boolean | ITusCreationConfig | undefined` (default: `true`) — Creation extension configuration.
|
|
193
|
+
- `creationWithUpload?`: `boolean | undefined` (default: `true`) — Creation With Upload extension configuration.
|
|
194
|
+
- `enabled?`: `boolean | undefined` (default: `true (enabled by default)`) — Whether tus uploads are enabled.
|
|
195
|
+
- `expiration?`: `boolean | ITusExpirationConfig | undefined` (default: `{ expiresIn: '24h' }`) — Expiration extension configuration.
|
|
196
|
+
- `maxSize?`: `number | undefined` (default: `50 * 1024 * 1024 * 1024 (50 GB)`) — Maximum upload size in bytes
|
|
197
|
+
- `path?`: `string | undefined` (default: `'/tus'`) — Base path for tus endpoints
|
|
198
|
+
- `roles?`: `string[] | undefined` (default: `['s_user']`) — Roles allowed to use the tus endpoints (create, write, read offset, terminate).
|
|
199
|
+
- `s3Staging?`: `boolean | undefined` (default: `true (when S3 is usable)`) — Stage upload chunks in the configured S3 bucket (`IServerOptions.s3`,
|
|
200
|
+
- `termination?`: `boolean | undefined` (default: `true`) — Termination extension configuration.
|
|
201
|
+
- `uploadDir?`: `string | undefined` (default: `'uploads/tus'`) — Directory for temporary upload chunks.
|
|
202
|
+
|
|
148
203
|
### ICoreModuleOverrides
|
|
149
204
|
|
|
150
205
|
- `ai?`: `{ budgetService?: Type<any>; connectionResolver?: Type<any>; connectionServic...` — Override AI module collaborators with project-specific subclasses.
|
|
@@ -190,7 +245,7 @@ When `passkey` is enabled, `trustedOrigins` is required (compile-time enforcemen
|
|
|
190
245
|
|
|
191
246
|
- `enabled?`: `boolean | undefined` (default: `false`) — Whether rate limiting is enabled
|
|
192
247
|
- `max?`: `number | undefined` (default: `10`) — Maximum number of requests within the time window
|
|
193
|
-
- `maxEntries?`: `number | undefined` (default: `10000`) — Maximum number of
|
|
248
|
+
- `maxEntries?`: `number | undefined` (default: `10000`) — Maximum number of distinct counters this limiter may hold — the bound on the keyspace a
|
|
194
249
|
- `message?`: `string | undefined` — Custom message when rate limit is exceeded
|
|
195
250
|
- `skipEndpoints?`: `string[] | undefined` — Endpoints to skip rate limiting entirely
|
|
196
251
|
- `strictEndpoints?`: `string[] | undefined` — Endpoints to apply stricter rate limiting (e.g., sign-in, sign-up)
|
|
@@ -287,7 +342,7 @@ Generic: `CrudService<Model, CreateInput, UpdateInput>`
|
|
|
287
342
|
| `auth` | — | `src/core/modules/auth/` |
|
|
288
343
|
| `better-auth` | README, CHECKLIST | `src/core/modules/better-auth/` |
|
|
289
344
|
| `error-code` | CHECKLIST | `src/core/modules/error-code/` |
|
|
290
|
-
| `file` | README | `src/core/modules/file/` |
|
|
345
|
+
| `file` | README, CHECKLIST | `src/core/modules/file/` |
|
|
291
346
|
| `health-check` | — | `src/core/modules/health-check/` |
|
|
292
347
|
| `hub` | README, CHECKLIST | `src/core/modules/hub/` |
|
|
293
348
|
| `migrate` | README | `src/core/modules/migrate/` |
|
package/bin/migrate.js
CHANGED
|
@@ -93,7 +93,17 @@ if (require.main === module) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Load and run the CLI
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// Load and run the CLI.
|
|
97
|
+
//
|
|
98
|
+
// Prefer runCli(): it drains stdout and then exits explicitly, so a handle left
|
|
99
|
+
// behind by MongoDB/GridFS cannot keep the process alive after the work is done
|
|
100
|
+
// (a migration step that never returns blocks the container before the server
|
|
101
|
+
// ever starts). main() is the fallback for a dist built before runCli existed —
|
|
102
|
+
// it resolves normally but relies on the event loop draining by itself.
|
|
103
|
+
const cli = require(cliPath);
|
|
104
|
+
const run = cli.runCli || cli.main;
|
|
105
|
+
Promise.resolve(run()).catch((error) => {
|
|
106
|
+
console.error('Fatal error:', error);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
});
|
|
99
109
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildContentDisposition(filename?: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildContentDisposition = buildContentDisposition;
|
|
4
|
+
const NON_ATTR_CHAR = /['()*]/g;
|
|
5
|
+
const NON_QUOTABLE_ASCII = /[^\x20-\x21\x23-\x5B\x5D-\x7E]/g;
|
|
6
|
+
function toExtValue(filename) {
|
|
7
|
+
return encodeURIComponent(filename).replace(NON_ATTR_CHAR, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
8
|
+
}
|
|
9
|
+
function buildContentDisposition(filename) {
|
|
10
|
+
const name = filename || 'download';
|
|
11
|
+
const quotable = name.replace(NON_QUOTABLE_ASCII, '_');
|
|
12
|
+
return `attachment; filename="${quotable}"; filename*=UTF-8''${toExtValue(name)}`;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=content-disposition.helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-disposition.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/content-disposition.helper.ts"],"names":[],"mappings":";;AAqFA,0DAIC;AAhED,MAAM,aAAa,GAAG,SAAS,CAAC;AAehC,MAAM,kBAAkB,GAAG,iCAAiC,CAAC;AAK7D,SAAS,UAAU,CAAC,QAAgB;IAClC,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC,OAAO,CACzC,aAAa,EACb,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAC9D,CAAC;AACJ,CAAC;AAmCD,SAAgB,uBAAuB,CAAC,QAAiB;IACvD,MAAM,IAAI,GAAG,QAAQ,IAAI,UAAU,CAAC;IACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvD,OAAO,yBAAyB,QAAQ,uBAAuB,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;AACpF,CAAC"}
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
|
2
|
+
import type { FileUploadSource } from '../../modules/file/interfaces/file-upload.interface';
|
|
3
|
+
export interface UploadAllowList {
|
|
4
|
+
extensions: readonly string[];
|
|
5
|
+
mimeTypes: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const IMAGE_UPLOAD_ALLOW_LIST: UploadAllowList;
|
|
8
|
+
export declare const SCRIPTABLE_UPLOAD_MIME_TYPES: readonly string[];
|
|
9
|
+
export declare const SCRIPTABLE_UPLOAD_EXTENSIONS: readonly string[];
|
|
10
|
+
export interface MulterFileFilterOptions {
|
|
11
|
+
allowScriptableTypes?: boolean;
|
|
12
|
+
}
|
|
2
13
|
export default class FileHelper {
|
|
3
14
|
static multerRandomFileName(numberOfDigits?: number): (req: any, file: any, cb: any) => void;
|
|
4
|
-
static multerFileFilter(
|
|
15
|
+
static multerFileFilter(accept?: RegExp | UploadAllowList, options?: MulterFileFilterOptions): (req: any, file: any, cb: any) => any;
|
|
5
16
|
static multerOptionsForImageUpload(options: {
|
|
17
|
+
allowList?: UploadAllowList;
|
|
6
18
|
destination?: string;
|
|
7
19
|
fileSize?: number;
|
|
8
20
|
fileTypeRegex?: RegExp;
|
|
9
21
|
}): MulterOptions;
|
|
10
22
|
}
|
|
11
|
-
export declare function multerFileFilter(
|
|
23
|
+
export declare function multerFileFilter(accept?: RegExp | UploadAllowList, options?: MulterFileFilterOptions): (req: any, file: any, cb: any) => any;
|
|
12
24
|
export declare function multerOptionsForImageUpload(options: {
|
|
25
|
+
allowList?: UploadAllowList;
|
|
13
26
|
destination?: string;
|
|
14
27
|
fileSize?: number;
|
|
15
28
|
fileTypeRegex?: RegExp;
|
|
29
|
+
memory?: boolean;
|
|
16
30
|
}): MulterOptions;
|
|
31
|
+
export declare function multerFileToUpload(file: {
|
|
32
|
+
buffer?: Buffer;
|
|
33
|
+
mimetype?: string;
|
|
34
|
+
originalname?: string;
|
|
35
|
+
}): FileUploadSource;
|
|
17
36
|
export declare function multerRandomFileName(numberOfDigits?: number): (req: any, file: any, cb: any) => void;
|
|
@@ -1,47 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCRIPTABLE_UPLOAD_EXTENSIONS = exports.SCRIPTABLE_UPLOAD_MIME_TYPES = exports.IMAGE_UPLOAD_ALLOW_LIST = void 0;
|
|
3
4
|
exports.multerFileFilter = multerFileFilter;
|
|
4
5
|
exports.multerOptionsForImageUpload = multerOptionsForImageUpload;
|
|
6
|
+
exports.multerFileToUpload = multerFileToUpload;
|
|
5
7
|
exports.multerRandomFileName = multerRandomFileName;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
6
9
|
const multer_1 = require("multer");
|
|
7
10
|
const path_1 = require("path");
|
|
11
|
+
const stream_1 = require("stream");
|
|
12
|
+
exports.IMAGE_UPLOAD_ALLOW_LIST = {
|
|
13
|
+
extensions: ['.jpeg', '.jpg', '.png'],
|
|
14
|
+
mimeTypes: ['image/jpeg', 'image/png'],
|
|
15
|
+
};
|
|
16
|
+
exports.SCRIPTABLE_UPLOAD_MIME_TYPES = [
|
|
17
|
+
'application/javascript',
|
|
18
|
+
'application/xhtml+xml',
|
|
19
|
+
'application/xml',
|
|
20
|
+
'image/svg+xml',
|
|
21
|
+
'text/html',
|
|
22
|
+
'text/javascript',
|
|
23
|
+
'text/xml',
|
|
24
|
+
];
|
|
25
|
+
exports.SCRIPTABLE_UPLOAD_EXTENSIONS = [
|
|
26
|
+
'.htm',
|
|
27
|
+
'.html',
|
|
28
|
+
'.js',
|
|
29
|
+
'.mjs',
|
|
30
|
+
'.svg',
|
|
31
|
+
'.xhtml',
|
|
32
|
+
'.xml',
|
|
33
|
+
];
|
|
8
34
|
class FileHelper {
|
|
9
35
|
static multerRandomFileName(numberOfDigits = 32) {
|
|
10
36
|
return multerRandomFileName(numberOfDigits);
|
|
11
37
|
}
|
|
12
|
-
static multerFileFilter(
|
|
13
|
-
return multerFileFilter(
|
|
38
|
+
static multerFileFilter(accept = exports.IMAGE_UPLOAD_ALLOW_LIST, options) {
|
|
39
|
+
return multerFileFilter(accept, options);
|
|
14
40
|
}
|
|
15
41
|
static multerOptionsForImageUpload(options) {
|
|
16
42
|
return multerOptionsForImageUpload(options);
|
|
17
43
|
}
|
|
18
44
|
}
|
|
19
45
|
exports.default = FileHelper;
|
|
20
|
-
function
|
|
46
|
+
function normalizeMimeType(value) {
|
|
47
|
+
return String(value || '')
|
|
48
|
+
.split(';')[0]
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
function multerFileFilter(accept = exports.IMAGE_UPLOAD_ALLOW_LIST, options) {
|
|
21
53
|
return (req, file, cb) => {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
54
|
+
const mimeType = normalizeMimeType(file?.mimetype);
|
|
55
|
+
const extension = (0, path_1.extname)(String(file?.originalname || '')).toLowerCase();
|
|
56
|
+
if (!options?.allowScriptableTypes &&
|
|
57
|
+
(exports.SCRIPTABLE_UPLOAD_MIME_TYPES.includes(mimeType) || exports.SCRIPTABLE_UPLOAD_EXTENSIONS.includes(extension))) {
|
|
58
|
+
return cb(new Error(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
|
|
59
|
+
}
|
|
60
|
+
const accepted = accept instanceof RegExp
|
|
61
|
+
? accept.test(mimeType) && accept.test(extension)
|
|
62
|
+
: accept.mimeTypes.includes(mimeType) && accept.extensions.includes(extension);
|
|
63
|
+
if (accepted) {
|
|
25
64
|
return cb(null, true);
|
|
26
65
|
}
|
|
27
|
-
cb(`
|
|
66
|
+
cb(new Error(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
|
|
28
67
|
};
|
|
29
68
|
}
|
|
69
|
+
function describeAccept(accept) {
|
|
70
|
+
return accept instanceof RegExp ? String(accept) : accept.extensions.join(', ');
|
|
71
|
+
}
|
|
30
72
|
function multerOptionsForImageUpload(options) {
|
|
31
73
|
const config = {
|
|
32
74
|
fileSize: 1024 * 1024,
|
|
33
|
-
fileTypeRegex: /jpeg|jpg|png/,
|
|
34
75
|
...options,
|
|
35
76
|
};
|
|
77
|
+
const accept = config.fileTypeRegex ?? config.allowList ?? exports.IMAGE_UPLOAD_ALLOW_LIST;
|
|
36
78
|
return {
|
|
37
|
-
fileFilter:
|
|
79
|
+
fileFilter: multerFileFilter(accept),
|
|
38
80
|
limits: {
|
|
39
81
|
fileSize: config.fileSize ? config.fileSize : undefined,
|
|
40
82
|
},
|
|
41
|
-
storage:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
83
|
+
storage: config.memory
|
|
84
|
+
? (0, multer_1.memoryStorage)()
|
|
85
|
+
: (0, multer_1.diskStorage)({
|
|
86
|
+
destination: config.destination ? config.destination : undefined,
|
|
87
|
+
filename: multerRandomFileName(),
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function multerFileToUpload(file) {
|
|
92
|
+
if (!file?.buffer) {
|
|
93
|
+
throw new Error('multerFileToUpload() needs an in-memory upload: pass `memory: true` to the multer options ' +
|
|
94
|
+
'(a disk-stored file has no buffer).');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
createReadStream: () => stream_1.Readable.from(file.buffer),
|
|
98
|
+
encoding: '7bit',
|
|
99
|
+
filename: file.originalname || (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
100
|
+
mimetype: file.mimetype || 'application/octet-stream',
|
|
45
101
|
};
|
|
46
102
|
}
|
|
47
103
|
function multerRandomFileName(numberOfDigits = 32) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/file.helper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/file.helper.ts"],"names":[],"mappings":";;;AAyJA,4CAyBC;AAcD,kEA8CC;AAcD,gDAiBC;AAMD,oDAYC;AA9RD,mCAAqC;AACrC,mCAAoD;AACpD,+BAA+B;AAC/B,mCAAkC;AA8BrB,QAAA,uBAAuB,GAAoB;IACtD,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC;IACrC,SAAS,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC;CACvC,CAAC;AAYW,QAAA,4BAA4B,GAAsB;IAC7D,wBAAwB;IACxB,uBAAuB;IACvB,iBAAiB;IACjB,eAAe;IACf,WAAW;IACX,iBAAiB;IACjB,UAAU;CACX,CAAC;AAGW,QAAA,4BAA4B,GAAsB;IAC7D,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,MAAM;CACP,CAAC;AAmBF,MAAqB,UAAU;IAKtB,MAAM,CAAC,oBAAoB,CAAC,cAAc,GAAG,EAAE;QACpD,OAAO,oBAAoB,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC;IAKM,MAAM,CAAC,gBAAgB,CAC5B,SAAmC,+BAAuB,EAC1D,OAAiC;QAEjC,OAAO,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAKM,MAAM,CAAC,2BAA2B,CAAC,OAKzC;QACC,OAAO,2BAA2B,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;CACF;AA9BD,6BA8BC;AAMD,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACb,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;AACnB,CAAC;AAyBD,SAAgB,gBAAgB,CAC9B,SAAmC,+BAAuB,EAC1D,OAAiC;IAEjC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,IAAA,cAAO,EAAC,MAAM,CAAC,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAE1E,IACE,CAAC,OAAO,EAAE,oBAAoB;YAC9B,CAAC,oCAA4B,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,oCAA4B,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EACrG,CAAC;YACD,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,yBAAyB,QAAQ,IAAI,cAAc,wBAAwB,CAAC,CAAC,CAAC;QACpG,CAAC;QAED,MAAM,QAAQ,GACZ,MAAM,YAAY,MAAM;YACtB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;YACjD,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAEnF,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,EAAE,CAAC,IAAI,KAAK,CAAC,uDAAuD,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC,CAAC;AACJ,CAAC;AAGD,SAAS,cAAc,CAAC,MAAgC;IACtD,OAAO,MAAM,YAAY,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClF,CAAC;AASD,SAAgB,2BAA2B,CAAC,OAM3C;IAEC,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,IAAI,GAAG,IAAI;QACrB,GAAG,OAAO;KACX,CAAC;IAIF,MAAM,MAAM,GAA6B,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,SAAS,IAAI,+BAAuB,CAAC;IAE7G,OAAO;QAEL,UAAU,EAAE,gBAAgB,CAAC,MAAM,CAAC;QAGpC,MAAM,EAAE;YAEN,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;SACxD;QASD,OAAO,EAAE,MAAM,CAAC,MAAM;YACpB,CAAC,CAAC,IAAA,sBAAa,GAAE;YACjB,CAAC,CAAC,IAAA,oBAAW,EAAC;gBAIV,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAGhE,QAAQ,EAAE,oBAAoB,EAAE;aACjC,CAAC;KACP,CAAC;AACJ,CAAC;AAcD,SAAgB,kBAAkB,CAAC,IAIlC;IACC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,4FAA4F;YAC1F,qCAAqC,CACxC,CAAC;IACJ,CAAC;IACD,OAAO;QACL,gBAAgB,EAAE,GAAG,EAAE,CAAC,iBAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAClD,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,IAAI,CAAC,YAAY,IAAI,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC9D,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,0BAA0B;KACtD,CAAC;AACJ,CAAC;AAMD,SAAgB,oBAAoB,CAAC,cAAc,GAAG,EAAE;IACtD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAEvB,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC;aACrC,IAAI,CAAC,IAAI,CAAC;aACV,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;aACtD,IAAI,CAAC,EAAE,CAAC,CAAC;QAIZ,EAAE,CAAC,IAAI,EAAE,GAAG,UAAU,GAAG,IAAA,cAAO,EAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC"}
|