@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
|
@@ -40,7 +40,9 @@ The `CoreModule` is a dynamic module that bootstraps the entire framework:
|
|
|
40
40
|
| **Dual API Support** | GraphQL and REST in the same application |
|
|
41
41
|
| **Security Pipeline** | 4 global interceptors, global validation pipe, middleware stack |
|
|
42
42
|
| **Mongoose Plugins** | Auto-registration of ID, password, audit, and role guard plugins |
|
|
43
|
-
| **GraphQL Subscriptions** | WebSocket support with JWT/session authentication |
|
|
43
|
+
| **GraphQL Subscriptions** | WebSocket support with JWT/session authentication (cluster-wide when `redis` is configured — see the Subscriptions row under GraphQL Features) |
|
|
44
|
+
| **Central Redis (optional)** | `CoreRedisService` — globally provided **and exported** by `CoreModule`, always present but **inert unless `redis` is configured** ("presence implies enabled"). Injected with `@Optional()`; every consumer keeps a process-local fallback. One service serves all features: shared client (`getClient()`), one cached subscriber (`getSubscriber()` — a subscribing client cannot run commands), dedicated connections (`createClient(label)`); all tracked and quit on shutdown. Keys are namespaced by the framework per key via `key(...)`, **not** through ioredis's own `keyPrefix` (that would collide with BullMQ's prefix). Requires the OPTIONAL peer `ioredis` — configured but missing **fails the boot**. Switches on: exact cross-replica rate limits, cron deduplication, `CoreRedisPubSub` as `PUB_SUB`, tenant-cache invalidation broadcast, Hub collector mirroring, MCP session registry |
|
|
45
|
+
| **Central S3 (optional)** | `CoreS3Service` — globally provided **and exported** by `CoreModule`, inert unless `s3` is configured *and* names a `bucket` (a bucket-less block is ignored with a warning). Backs `file.storage: 's3'` and TUS staging (`tus.s3Staging`). Requires the OPTIONAL peers `@aws-sdk/client-s3` (+ `@aws-sdk/s3-request-presigner` for `presignedDownloads`) — configured but missing **fails the boot** |
|
|
44
46
|
| **Configuration System** | `config.env.ts` with ENV variables, `NEST_SERVER_CONFIG` JSON, `NSC__*` prefixes |
|
|
45
47
|
| **Cookie Handling** | Enabled by default (`cookies: true`), configurable via `ICookiesConfig` with `exposeTokenInBody` option |
|
|
46
48
|
| **Unified CORS** | Single `cors` config propagates to GraphQL, REST, and BetterAuth layers |
|
|
@@ -61,7 +63,7 @@ Modern OAuth-compatible authentication with plugin architecture:
|
|
|
61
63
|
| **Social Login** | OAuth providers: Google, GitHub, Apple, Discord, etc. (plugin) |
|
|
62
64
|
| **Email Verification** | Configurable email verification flow |
|
|
63
65
|
| **Sign-Up Validation** | Custom validation hooks for registration |
|
|
64
|
-
| **Rate Limiting** | Per-endpoint rate limits (configurable) |
|
|
66
|
+
| **Rate Limiting** | Per-endpoint rate limits (`betterAuth.rateLimit`, configurable). Counters live behind a `RateLimitStore`: `RedisRateLimitStore` when `redis` is configured, so `max` is enforced **exactly across replicas** instead of `max × replicas`; otherwise the process-local `InMemoryRateLimitStore` as before. `check()` / `reset()` / `clear()` are **async** since 11.33.0. On a Redis outage it degrades to the in-memory counter and logs once per transition — never a 500, never "allowed". Counters are keyed on `request.ip`, which Express derives from `X-Forwarded-For` only as far as `trust proxy` allows — set `trustProxy` (§ServerOptions) behind a reverse proxy or every client resolves to the proxy and shares ONE bucket; unset with a limiter enabled logs a boot warning |
|
|
65
67
|
| **Cross-Subdomain Cookies** | Automatic cookie domain configuration |
|
|
66
68
|
| **Organization / Multi-Tenant** | Teams and organization management (plugin) |
|
|
67
69
|
| **3 Registration Patterns** | Zero-config, overrides parameter, or manual (`autoRegister: false`) |
|
|
@@ -75,7 +77,7 @@ JWT-based authentication for existing projects:
|
|
|
75
77
|
| **JWT Authentication** | Bearer token auth with Passport strategies |
|
|
76
78
|
| **Refresh Tokens** | Automatic token renewal |
|
|
77
79
|
| **Sign In / Sign Up / Logout** | GraphQL mutations + REST endpoints |
|
|
78
|
-
| **Rate Limiting** | Configurable per-endpoint rate limits |
|
|
80
|
+
| **Rate Limiting** | Configurable per-endpoint rate limits (`auth.rateLimit`). Same `RateLimitStore` selection, async signatures and Redis-outage degradation as the BetterAuth row above (namespace `legacy-auth`) |
|
|
79
81
|
| **Legacy Endpoint Controls** | Disable legacy endpoints after migration (`auth.legacyEndpoints`) |
|
|
80
82
|
| **Migration Tracking** | `betterAuthMigrationStatus` query for monitoring |
|
|
81
83
|
|
|
@@ -158,9 +160,10 @@ JWT-based authentication for existing projects:
|
|
|
158
160
|
| Feature | Description |
|
|
159
161
|
|---------|-------------|
|
|
160
162
|
| **File Module** | Upload/download with MongoDB GridFS storage |
|
|
161
|
-
| **REST Endpoints** | `GET /files/:id`, `POST /files/upload`, `DELETE /files/:id` |
|
|
162
|
-
| **GraphQL Endpoints** | `
|
|
163
|
-
| **
|
|
163
|
+
| **REST Endpoints** | `GET /files/id/:id`, `GET /files/:filename` (core, gated by `file.downloadRoles`, default ADMIN); `POST /files/upload`, `DELETE /files/:id` (project-specific) |
|
|
164
|
+
| **GraphQL Endpoints** | `getFileInfo` (`file.downloadRoles`), `uploadFile` / `uploadFiles` (`file.uploadRoles`), `deleteFile` (`file.deleteRoles`) — all default ADMIN |
|
|
165
|
+
| **File access control** | Roles are the coarse filter; per-file rules go in `CoreFileService.checkRights()` using metadata written at upload time. Both file classes carry `@SkipTenantCheck()` — GridFS is not tenant-scoped |
|
|
166
|
+
| **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration), gated by `tus.roles` (default `S_USER`); `OPTIONS` stays public for the CORS preflight |
|
|
164
167
|
| **GridFS Migration** | Completed TUS uploads auto-migrate to GridFS |
|
|
165
168
|
| **CORS Support** | Automatic CORS headers for browser uploads |
|
|
166
169
|
|
|
@@ -201,7 +204,7 @@ JWT-based authentication for existing projects:
|
|
|
201
204
|
|---------|-------------|
|
|
202
205
|
| **Apollo Server** | Full GraphQL server with schema-first or code-first |
|
|
203
206
|
| **Custom Scalars** | `Date`, `DateTime` (timestamp), `JSON`, `Any` |
|
|
204
|
-
| **Subscriptions** | WebSocket support via `graphql-ws` with auth |
|
|
207
|
+
| **Subscriptions** | WebSocket support via `graphql-ws` with auth. The `PUB_SUB` provider is built from a factory: `CoreRedisPubSub` when `redis` is configured (delivery is then cluster-wide), the in-memory `PubSub` otherwise (delivery only to clients connected to the publishing replica). **Constraint once Redis is in play: every published payload must be JSON-serializable** — it crosses the wire as JSON, so `Date`, class instances, `Map`/`Set` and `undefined` do not survive the round trip. An in-process `PubSub` never had this constraint, so a payload that worked on one replica can silently lose fields on a cluster. Publish plain objects and ISO strings |
|
|
205
208
|
| **Complexity Analysis** | Query cost calculation to prevent DoS attacks |
|
|
206
209
|
| **Enum Registration** | `registerEnum()` helper for GraphQL enum types |
|
|
207
210
|
| **Upload Support** | `graphqlUploadExpress()` for multipart file uploads |
|
|
@@ -214,7 +217,8 @@ JWT-based authentication for existing projects:
|
|
|
214
217
|
| **Error Code Module** | Centralized error registry with unique IDs |
|
|
215
218
|
| **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
|
|
216
219
|
| **Hub (Operator Cockpit)** | Build-free ADMIN-gated dashboard at `/hub` (config-gated per environment). Adds an optional HTTP trace middleware (registered by `CoreHubModule.configure()` only when traces are enabled), a chaining `Logger.overrideLogger()` delegate for the log buffer, an optional `EmailService` capture hook (`HUB_EMAIL_CAPTURE` token) for the mailbox, and — when the query profiler is enabled — opts the MongoDB driver into `monitorCommands` from `core.module.ts`. See `src/core/modules/hub/README.md`. |
|
|
217
|
-
| **Process Diagnostics** | Opt-in process-level exit diagnostics (`installProcessDiagnostics()` + `handleFatalBootstrapError`, `src/core/common/helpers/process-diagnostics.helper.ts`). Wired into `main.ts` — **NOT** into `CoreModule.forRoot()`, because it must run before `NestFactory.create()` and installs a `process.exit(1)` path that must never arm inside `Test.createTestingModule()`. Logs unhandled rejections without crashing (configurable), uncaught exceptions before the exit, non-zero exit codes, and labels SIGTERM/SIGINT/SIGHUP/SIGQUIT as external terminations. Pair with `server.enableShutdownHooks()
|
|
220
|
+
| **Process Diagnostics** | Opt-in process-level exit diagnostics (`installProcessDiagnostics()` + `handleFatalBootstrapError`, `src/core/common/helpers/process-diagnostics.helper.ts`). Wired into `main.ts` — **NOT** into `CoreModule.forRoot()`, because it must run before `NestFactory.create()` and installs a `process.exit(1)` path that must never arm inside `Test.createTestingModule()`. Logs unhandled rejections without crashing (configurable), uncaught exceptions before the exit, non-zero exit codes, and labels SIGTERM/SIGINT/SIGHUP/SIGQUIT as external terminations. Pair with `installGracefulShutdown(server)` — **not** with `server.enableShutdownHooks()`, see the Graceful Shutdown row below |
|
|
221
|
+
| **Graceful Shutdown** | `installGracefulShutdown(app)` (`src/core/common/helpers/graceful-shutdown.helper.ts`), wired in `main.ts`. It **REPLACES** `server.enableShutdownHooks()` and must not be used alongside it: with `shutdownDelayMs` set, Nest would register its own listener for the same signals and close the app in parallel with the wait, so the delay silently never happens. At `shutdownDelayMs: 0` (the default) the helper simply *is* `enableShutdownHooks()`, so the single call is correct either way. With a delay it waits **inside the SIGTERM/SIGINT handler, before `close()` is entered** — a NestJS lifecycle hook cannot do this, because `close()` runs `onModuleDestroy` → `beforeApplicationShutdown` → dispose → `onApplicationShutdown`, i.e. a delay in a hook would wait with every module already torn down while the socket still accepts. A second signal cancels the pending wait and closes immediately. Warns above 10 000 ms, capped at 60 000 ms — keep it below the orchestrator grace period (Compose 10 s, Kubernetes 30 s) and below `installProcessDiagnostics()`'s 30 s force-exit |
|
|
218
222
|
| **System Setup Module** | Initial admin creation for fresh deployments |
|
|
219
223
|
| **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
|
|
220
224
|
| **Model Documentation** | Auto-generated model docs via `ModelDocService` |
|
|
@@ -425,6 +429,51 @@ The following diagram shows the exact order of execution from HTTP request to re
|
|
|
425
429
|
+---------------------+
|
|
426
430
|
```
|
|
427
431
|
|
|
432
|
+
### Shutdown Flow (SIGTERM / SIGINT)
|
|
433
|
+
|
|
434
|
+
The mirror image of the request flow, and the one place where an ordering mistake is invisible until
|
|
435
|
+
a rolling deploy drops requests. Installed in `main.ts` by `installGracefulShutdown(server)` — which
|
|
436
|
+
**replaces** `server.enableShutdownHooks()`, never accompanies it.
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
SIGTERM / SIGINT
|
|
440
|
+
|
|
|
441
|
+
v
|
|
442
|
+
+-------------------------------------------------------------+
|
|
443
|
+
| installGracefulShutdown() signal handler |
|
|
444
|
+
| |
|
|
445
|
+
| shutdownDelayMs === 0 (default) |
|
|
446
|
+
| -> this IS app.enableShutdownHooks(): close() immediately |
|
|
447
|
+
| |
|
|
448
|
+
| shutdownDelayMs > 0 |
|
|
449
|
+
| -> stay FULLY HEALTHY for N ms (routes still served), |
|
|
450
|
+
| so the load balancer can finish deregistering |
|
|
451
|
+
| -> a second signal cancels the wait and closes now |
|
|
452
|
+
+----------------------------+---------------------------------+
|
|
453
|
+
| (only after the wait)
|
|
454
|
+
v
|
|
455
|
+
+-------------------------------------------------------------+
|
|
456
|
+
| app.close() |
|
|
457
|
+
| 1. onModuleDestroy |
|
|
458
|
+
| 2. beforeApplicationShutdown |
|
|
459
|
+
| 3. dispose (HTTP server socket closes HERE) |
|
|
460
|
+
| 4. onApplicationShutdown |
|
|
461
|
+
| - CoreRedisService quits every tracked connection |
|
|
462
|
+
+-------------------------------------------------------------+
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Why the wait cannot be a lifecycle hook:** the socket only closes at step 3, so a delay placed in
|
|
466
|
+
`beforeApplicationShutdown` (step 2) would keep accepting traffic with every module already torn
|
|
467
|
+
down — strictly worse than not waiting at all.
|
|
468
|
+
|
|
469
|
+
**Why both together is a bug:** with `enableShutdownHooks()` also installed, Nest registers its own
|
|
470
|
+
listener for the same signals and enters `close()` in parallel with the wait. Nothing errors; the
|
|
471
|
+
delay simply never happens.
|
|
472
|
+
|
|
473
|
+
| Knob | Default | Notes |
|
|
474
|
+
|------|---------|-------|
|
|
475
|
+
| `shutdownDelayMs` | `0` (no delay, no log) | Warns above `10000`, capped at `60000`. Keep it below the orchestrator grace period (Compose `stop_grace_period` 10 s, Kubernetes `terminationGracePeriodSeconds` 30 s) **and** below `installProcessDiagnostics()`'s 30 s force-exit — exceed any and the process is SIGKILLed mid-wait with no hook running. Non-numeric/negative behaves like `0` |
|
|
476
|
+
|
|
428
477
|
---
|
|
429
478
|
|
|
430
479
|
## Phase 1: Incoming Request
|
|
@@ -11,16 +11,18 @@ A green `pnpm audit` inside the framework repo says nothing about your tree.
|
|
|
11
11
|
|
|
12
12
|
## What this concretely means for you
|
|
13
13
|
|
|
14
|
-
The framework pulls in
|
|
14
|
+
The framework pulls in three transitive packages that resolve to a **vulnerable** version unless you
|
|
15
15
|
override them yourself:
|
|
16
16
|
|
|
17
17
|
| Package | Advisory | Why it cannot resolve forward on its own |
|
|
18
18
|
|---------|----------|------------------------------------------|
|
|
19
19
|
| `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | `@nestjs/graphql` declares `"ws": "8.20.1"` — an **exact pin**, not a caret. No amount of updating moves it |
|
|
20
20
|
| `@hono/node-server` | [GHSA-frvp-7c67-39w9](https://github.com/advisories/GHSA-frvp-7c67-39w9) (static-file path traversal) + [GHSA-9mqv-5hh9-4cgg](https://github.com/advisories/GHSA-9mqv-5hh9-4cgg) (unauthenticated memory leak). Patched `>=2.0.10` | `@modelcontextprotocol/sdk` declares `^1.19.9` and ships **no 1.x fix line**, so the fix is only available across a major |
|
|
21
|
+
| `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS). Patched `>=5.2.2` | `@nestjs/swagger` declares `"js-yaml": "5.2.1"` — an **exact pin**, the same shape as the `ws` case. It cannot resolve forward |
|
|
21
22
|
|
|
22
23
|
`@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
|
|
23
|
-
`graphQl: false`.
|
|
24
|
+
`graphQl: false`. `@nestjs/swagger` is likewise a plain `dependencies` entry. None of the three is
|
|
25
|
+
optional in practice.
|
|
24
26
|
|
|
25
27
|
## The fix
|
|
26
28
|
|
|
@@ -31,8 +33,9 @@ Add this to your project's `pnpm-workspace.yaml` (pnpm 11+; in pnpm 10 and earli
|
|
|
31
33
|
overrides:
|
|
32
34
|
# @nestjs/graphql exact-pins ws@8.20.1 (GHSA-96hv-2xvq-fx4p, high, patched >=8.21.0).
|
|
33
35
|
# An exact pin cannot resolve forward — the override is the only fix.
|
|
36
|
+
# Keep the target in LOCKSTEP with your direct `ws` dependency (see the note below).
|
|
34
37
|
# Remove once @nestjs/graphql stops pinning it.
|
|
35
|
-
'ws@>=8.0.0 <8.21.0': '8.21.
|
|
38
|
+
'ws@>=8.0.0 <8.21.0': '8.21.3'
|
|
36
39
|
|
|
37
40
|
# @modelcontextprotocol/sdk declares @hono/node-server ^1.19.9 with no 1.x fix line
|
|
38
41
|
# (GHSA-frvp-7c67-39w9, GHSA-9mqv-5hh9-4cgg). Deliberately a CROSS-MAJOR override.
|
|
@@ -40,8 +43,30 @@ overrides:
|
|
|
40
43
|
# `(fetchCallback, options?)` is unchanged in 2.x. Engines >=20 and peer hono@^4 both fit.
|
|
41
44
|
# Remove once @modelcontextprotocol/sdk moves its own range to ^2.
|
|
42
45
|
'@hono/node-server@<2.0.10': '2.0.11'
|
|
46
|
+
|
|
47
|
+
# @nestjs/swagger exact-pins js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
|
|
48
|
+
# Same shape as the ws entry: an exact pin cannot resolve forward.
|
|
49
|
+
# Remove once @nestjs/swagger stops pinning it.
|
|
50
|
+
'js-yaml@>=5.0.0 <5.2.2': '5.2.2'
|
|
43
51
|
```
|
|
44
52
|
|
|
53
|
+
### The `ws` target must stay in lockstep with the declared `ws` version
|
|
54
|
+
|
|
55
|
+
`8.21.3` is not an arbitrary "latest patch" — it is the version `@lenne.tech/nest-server` declares as
|
|
56
|
+
an ordinary **dependency**, so it is already in your tree whether or not you list `ws` yourself.
|
|
57
|
+
The override target has to equal it.
|
|
58
|
+
|
|
59
|
+
Under `nodeLinker: hoisted` (what this framework and the starters use), a lower target does not
|
|
60
|
+
merely leave you one patch behind: it puts a **second, older `ws` copy** in the tree next to the
|
|
61
|
+
declared one. The transitive consumers resolve to the override target, the declared dependency stays
|
|
62
|
+
where its `package.json` pins it, and you now carry two `ws` versions — of which only one is visible
|
|
63
|
+
when you read a manifest. The target was raised `8.21.1` → `8.21.3` on 2026-08-10 for exactly this
|
|
64
|
+
reason.
|
|
65
|
+
|
|
66
|
+
**Rule:** whenever the `ws` version declared by `@lenne.tech/nest-server` (or by your own
|
|
67
|
+
`package.json`, if you list it) moves, move this override target with it in the same commit. The
|
|
68
|
+
same applies to any other override whose package also appears as a declared dependency.
|
|
69
|
+
|
|
45
70
|
Then:
|
|
46
71
|
|
|
47
72
|
```bash
|
|
@@ -52,8 +77,8 @@ pnpm test # nothing should regress
|
|
|
52
77
|
|
|
53
78
|
Commit `package.json`/`pnpm-workspace.yaml` **and** `pnpm-lock.yaml` together.
|
|
54
79
|
|
|
55
|
-
> Projects generated from `nest-server-starter` or `lt-monorepo` already carry
|
|
56
|
-
> page is for projects that predate that, or that were assembled by hand.
|
|
80
|
+
> Projects generated from `nest-server-starter` or `lt-monorepo` already carry all three entries.
|
|
81
|
+
> This page is for projects that predate that, or that were assembled by hand.
|
|
57
82
|
|
|
58
83
|
## Rules for writing your own overrides
|
|
59
84
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Migration Guide: 11.32.3 → 11.32.4
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None in signatures. Six **runtime behaviour** changes you inherit automatically — see §1, §2, §4, §5, §6, §7 |
|
|
8
|
+
| **New Features** | `assertGridFsFileComplete()` — verify a stored GridFS file's chunk completeness; `UploadAllowList` — exact-matching upload filters, with `SCRIPTABLE_UPLOAD_MIME_TYPES` / `SCRIPTABLE_UPLOAD_EXTENSIONS` and the `allowScriptableTypes` opt-out |
|
|
9
|
+
| **Bugfixes** | GridFS uploads no longer report success for incomplete files, no longer hang on an unreadable source, and no longer leak their connection; file downloads answer an honest 404 instead of dropping the socket; the migrate CLI now actually terminates and no longer truncates its own output; the upload filter no longer matches mimetypes and extensions as SUBSTRINGS (`te?xt` accepted `text/html`), no longer reports rejections as a bare string, and `multerOptionsForImageUpload` no longer silently disables all filtering when `fileTypeRegex` is `undefined` |
|
|
10
|
+
| **Migration Effort** | No code changes for most projects, and nothing to configure. Read §1 if you have seed migrations that upload assets, §4 if any test asserts on download error messages, §5–§7 if your project accepts uploads |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm update @lenne.tech/nest-server@11.32.4
|
|
18
|
+
pnpm run build
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No configuration change is required. Existing `fileTypeRegex` arguments keep working.
|
|
23
|
+
|
|
24
|
+
**Vendor-mode projects:** four modified files under `src/core/`
|
|
25
|
+
(`common/helpers/file.helper.ts`, `modules/file/core-file.controller.ts`,
|
|
26
|
+
`modules/migrate/cli/migrate-cli.ts`, `modules/migrate/helpers/migration.helper.ts`) plus
|
|
27
|
+
`bin/migrate.js`. No moved files — there is no atomic file-set hazard for a partial sync.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. `uploadFileToGridFS()` now fails loudly instead of quietly
|
|
32
|
+
|
|
33
|
+
Previously the helper resolved as soon as the write stream emitted `'finish'`. That event says the
|
|
34
|
+
stream ended — it does **not** prove every chunk document is durably stored. A connection lost at the
|
|
35
|
+
wrong moment left a files document promising more bytes than existed, the migration reported success,
|
|
36
|
+
and the defect surfaced much later as a broken download from a record that looked perfectly healthy.
|
|
37
|
+
|
|
38
|
+
It now verifies chunk completeness before resolving, and **rejects** when the file is incomplete
|
|
39
|
+
(removing the incomplete file so retries do not accumulate orphans).
|
|
40
|
+
|
|
41
|
+
**What you may notice:** a seed migration that previously "succeeded" while silently storing a broken
|
|
42
|
+
asset now fails.
|
|
43
|
+
|
|
44
|
+
**What that does to a deployment depends on the `docker-entrypoint.sh` your project copied — check
|
|
45
|
+
yours, and check its default.**
|
|
46
|
+
|
|
47
|
+
| Your entrypoint | A failed migration |
|
|
48
|
+
|-----------------|--------------------|
|
|
49
|
+
| The one shipped with this framework | Refuses the start (`MIGRATE_FAILURE_POLICY` defaults to `abort`) |
|
|
50
|
+
| From `nest-server-starter` 11.32.4 or newer | Starts the server anyway — the same variable exists, but it defaults to `warn`. Set `MIGRATE_FAILURE_POLICY=abort` per stage to refuse the start |
|
|
51
|
+
| Older, without that variable | Starts the server anyway, with no way to change it short of updating the file |
|
|
52
|
+
|
|
53
|
+
In the two lower rows the failure is visible only in the container log, and the broken asset still
|
|
54
|
+
ships — so if you deploy from a starter-derived project, decide on `abort` explicitly rather than
|
|
55
|
+
assuming this release stops a bad boot for you.
|
|
56
|
+
|
|
57
|
+
**Failing is intentional** — finding exactly this is why migrations run before the server. To recover:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 1. Identify the incomplete file from the migration error, which names it:
|
|
61
|
+
# GridFS file 'logo.png' is incomplete: 1 of 3 chunks stored (id 6a6b…)
|
|
62
|
+
# 2. Verify the source asset is actually present and readable in the image/checkout.
|
|
63
|
+
# 3. Re-run. The helper deletes its own incomplete upload, but a file left behind by
|
|
64
|
+
# an OLDER version of the helper must be removed manually:
|
|
65
|
+
# db.getCollection('<bucket>.files').deleteOne({ _id: ObjectId('…') })
|
|
66
|
+
# db.getCollection('<bucket>.chunks').deleteMany({ files_id: ObjectId('…') })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Two more changes to the same helper, both strictly better and requiring no action:
|
|
70
|
+
|
|
71
|
+
- **An unreadable source rejects instead of hanging.** `pipe()` does not forward read-stream errors,
|
|
72
|
+
so a missing file used to leave the promise pending forever — a migration that never returned.
|
|
73
|
+
It now rejects with the underlying `ENOENT`.
|
|
74
|
+
- **The connection is always closed.** The client it opens is registered and closed on every path.
|
|
75
|
+
A leaked client keeps an SDAM monitor timer alive, which kept the whole CLI from exiting.
|
|
76
|
+
|
|
77
|
+
**Note on empty files:** GridFS stores a zero-byte file with **no** chunk documents at all, and the
|
|
78
|
+
completeness check accounts for that. Uploading an empty placeholder asset is valid and passes.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 2. `migrate up` now terminates explicitly
|
|
83
|
+
|
|
84
|
+
The CLI drains stdout/stderr and then exits, instead of waiting for the event loop to empty. A single
|
|
85
|
+
handle left behind by MongoDB, GridFS or the state store used to keep the process alive after the work
|
|
86
|
+
was done: the CLI printed "All migrations completed successfully" and never returned. On a developer
|
|
87
|
+
machine that is invisible; in CI the job blocks until its timeout, and a container that runs
|
|
88
|
+
migrations before starting the server never reaches the server at all.
|
|
89
|
+
|
|
90
|
+
Exit codes are unchanged (`0` success, `1` failure), and the drain happens **before** the exit, so no
|
|
91
|
+
output is lost — `process.exit()` does not flush an asynchronous pipe, which is exactly what Docker's
|
|
92
|
+
log driver and CI log collectors are.
|
|
93
|
+
|
|
94
|
+
**Action required: none**, unless you invoke the CLI in a non-standard way. If you call the CLI
|
|
95
|
+
module directly rather than through the `migrate` / `nest-migrate` bin, use the new exported
|
|
96
|
+
`runCli()` (which drains and exits) instead of `main()` (which merely resolves):
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Before — resolves, but relies on the event loop draining by itself
|
|
100
|
+
const { main } = require('@lenne.tech/nest-server/dist/core/modules/migrate/cli/migrate-cli');
|
|
101
|
+
main();
|
|
102
|
+
|
|
103
|
+
// After
|
|
104
|
+
const { runCli } = require('@lenne.tech/nest-server/dist/core/modules/migrate/cli/migrate-cli');
|
|
105
|
+
void runCli();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The shipped `bin/migrate.js` already prefers `runCli()` and falls back to `main()`, so a mixed
|
|
109
|
+
version pair keeps working.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 3. New: `assertGridFsFileComplete()`
|
|
114
|
+
|
|
115
|
+
Exported for direct use when you need to check a file that something else wrote — a restored dump,
|
|
116
|
+
another service, a manual upload:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { assertGridFsFileComplete, getDb } from '@lenne.tech/nest-server';
|
|
120
|
+
|
|
121
|
+
const db = await getDb(process.env.MONGODB_URL);
|
|
122
|
+
await assertGridFsFileComplete(db, 'images', fileId, 'logo.png'); // throws if incomplete
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
It counts chunk documents rather than reading bytes back, so a chunk that was written but truncated
|
|
126
|
+
is not detected. See `src/core/modules/migrate/README.md`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 4. File downloads: error responses changed
|
|
131
|
+
|
|
132
|
+
Two changes on `GET /files/id/:id` and `GET /files/:filename`.
|
|
133
|
+
|
|
134
|
+
**(a) A GridFS read error is now a 404, not a dropped socket.** The stream error previously went
|
|
135
|
+
unhandled, Node destroyed the socket mid-response, and a reverse proxy turned that into
|
|
136
|
+
**502 Bad Gateway** — reading as "the server is down" while every other route answered normally.
|
|
137
|
+
The response is now:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{ "error": "Not Found", "message": "#LTNS_0500: File not found", "statusCode": 404 }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Once bytes are already on the wire there is no status left to send, so the connection is still closed
|
|
144
|
+
— at that point it genuinely is a truncated transfer.
|
|
145
|
+
|
|
146
|
+
**(b) Exception messages now carry their ErrorCode.** The controller uses the framework registry
|
|
147
|
+
instead of raw strings, matching every other core module:
|
|
148
|
+
|
|
149
|
+
| Before | After |
|
|
150
|
+
|--------|-------|
|
|
151
|
+
| `'File not found'` | `ErrorCode.FILE_NOT_FOUND` → `'#LTNS_0500: File not found'` |
|
|
152
|
+
| `'Missing file ID for download'` | `ErrorCode.REQUIRED_FIELD_MISSING` → `'#LTNS_0301: Required field missing'` |
|
|
153
|
+
|
|
154
|
+
**Action required:** if a test asserts on the exact message, loosen it to a substring match — the old
|
|
155
|
+
text is still contained in the new one. Frontends using `useLtErrorTranslation()` gain a translated
|
|
156
|
+
message where they previously showed raw English.
|
|
157
|
+
|
|
158
|
+
**Also:** a project that restricted downloads by overriding `CoreFileService.checkRights()` used to
|
|
159
|
+
get a **500** (the refusal produced `null.pipe(res)` → `TypeError`). It is now a **404** —
|
|
160
|
+
deliberately the same answer as an unknown id, so the endpoint cannot be used to probe which files
|
|
161
|
+
exist.
|
|
162
|
+
|
|
163
|
+
To customise the status, body or logging of the streaming error path, override the new
|
|
164
|
+
`protected pipeFileToResponse()` method on your controller:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
export class FileController extends CoreFileController {
|
|
168
|
+
protected override pipeFileToResponse(stream: Readable, res: Response): Response {
|
|
169
|
+
// e.g. report a different status, or add your own telemetry
|
|
170
|
+
return super.pipeFileToResponse(stream, res);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 5. The upload filter matched substrings, not values
|
|
178
|
+
|
|
179
|
+
**What changed:** `multerFileFilter` compared the mimetype and the extension by
|
|
180
|
+
`RegExp.test()` — a substring search. Every alternative therefore matched
|
|
181
|
+
anywhere inside either value.
|
|
182
|
+
|
|
183
|
+
**Why it matters:** an allow-list containing `text` or `txt` also accepted
|
|
184
|
+
`text/html` and `text/xml`. A file named `x.txt` and sent as `text/html` passed
|
|
185
|
+
both halves of a filter whose own comment said "no html". `md` matched every
|
|
186
|
+
mimetype containing "md", `zip` every one containing "zip". The single
|
|
187
|
+
alternative was never the bug — the substring semantics were.
|
|
188
|
+
|
|
189
|
+
Anchoring the expression was not an option: the SAME expression was tested
|
|
190
|
+
against two different value spaces, so it had to carry mimetype FRAGMENTS
|
|
191
|
+
(`wordprocessingml`, `ms-excel`) next to bare extensions, and no `^…$` satisfies
|
|
192
|
+
both at once.
|
|
193
|
+
|
|
194
|
+
**What to do:** nothing, if your filter only ever listed image formats — those
|
|
195
|
+
never matched markup. If your filter lists text-ish or document formats, move to
|
|
196
|
+
the new exact-matching form:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// Before — substring matching
|
|
200
|
+
multerOptionsForImageUpload({ fileTypeRegex: /jpeg|jpg|png|pdf|te?xt|csv/ });
|
|
201
|
+
|
|
202
|
+
// After — whole-value matching
|
|
203
|
+
multerOptionsForImageUpload({
|
|
204
|
+
allowList: {
|
|
205
|
+
extensions: ['.csv', '.jpeg', '.jpg', '.pdf', '.png', '.txt'],
|
|
206
|
+
mimeTypes: ['application/pdf', 'image/jpeg', 'image/png', 'text/csv', 'text/plain'],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The two conditions stay **independent**: either one alone rejects the file, but a
|
|
212
|
+
pair that is odd yet individually allowed (`report.txt` announced as
|
|
213
|
+
`application/pdf`) passes. An extension→mimetype MAPPING is deliberately not
|
|
214
|
+
enforced — user agents genuinely disagree about office and audio types (macOS
|
|
215
|
+
reports `.csv` as `text/plain`), so a mapping rejects legitimate uploads.
|
|
216
|
+
|
|
217
|
+
`fileTypeRegex` still works and is not removed; it is marked deprecated.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 6. Scriptable types are now rejected on BOTH forms
|
|
222
|
+
|
|
223
|
+
**What changed:** `text/html`, `text/xml`, `application/xhtml+xml`,
|
|
224
|
+
`image/svg+xml`, JavaScript types and the matching extensions (`.html`, `.svg`,
|
|
225
|
+
`.js`, `.xml`, …) are rejected before the allow-list is consulted — including
|
|
226
|
+
when a legacy `fileTypeRegex` would have matched them.
|
|
227
|
+
|
|
228
|
+
**Why it matters:** this is what closes §5 for expressions that already exist in
|
|
229
|
+
consumer projects, without anyone having to rewrite them. The danger does not
|
|
230
|
+
depend on what an endpoint meant to accept: a stored upload served back from the
|
|
231
|
+
API origin with one of these content types executes in that origin, with the
|
|
232
|
+
victim's session.
|
|
233
|
+
|
|
234
|
+
**What to do:** if you deliberately accept SVG logos or HTML fragments, opt out
|
|
235
|
+
explicitly:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
multerFileFilter(
|
|
239
|
+
{ extensions: ['.svg'], mimeTypes: ['image/svg+xml'] },
|
|
240
|
+
{ allowScriptableTypes: true },
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Only do that when the file is never served from an origin that carries a session
|
|
245
|
+
— e.g. a separate download host, or a route that always answers with
|
|
246
|
+
`Content-Disposition: attachment` **and** `X-Content-Type-Options: nosniff`.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 7. `fileTypeRegex: undefined` no longer disables filtering
|
|
251
|
+
|
|
252
|
+
**What changed:** `multerOptionsForImageUpload` built its config as
|
|
253
|
+
`{ fileTypeRegex: /jpeg|jpg|png/, ...options }` and then installed a filter only
|
|
254
|
+
`if (config.fileTypeRegex)`. Passing the key explicitly as `undefined`
|
|
255
|
+
overwrote the default, so **no filter was installed at all** and every file type
|
|
256
|
+
was accepted — on a helper named "ImageUpload".
|
|
257
|
+
|
|
258
|
+
**Why it matters:** this is easy to trigger by accident rather than intent:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// The author meant "then just the standard image types".
|
|
262
|
+
// Before: they got "then everything".
|
|
263
|
+
multerOptionsForImageUpload({ fileTypeRegex: allowSvg ? /jpeg|jpg|png|svg/ : undefined });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Any optional variable threaded into that option had the same effect.
|
|
267
|
+
|
|
268
|
+
**What to do:** if a project relied on this to accept arbitrary types, it now
|
|
269
|
+
gets JPEG/PNG only and those uploads start failing. Pass an explicit `allowList`
|
|
270
|
+
naming what the endpoint really accepts. There is intentionally no "accept
|
|
271
|
+
everything" switch — an upload endpoint that takes any type should not be built
|
|
272
|
+
on `multerOptionsForImageUpload`.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 8. Upload rejections are a real `Error`
|
|
277
|
+
|
|
278
|
+
**What changed:** the filter called `cb('Error: File upload only supports …')`
|
|
279
|
+
with a bare **string**. It now passes an `Error`.
|
|
280
|
+
|
|
281
|
+
**Why it matters:** a string has no `message`, so NestJS's `transformException`
|
|
282
|
+
could not map it and the request surfaced as a 500 instead of a 4xx.
|
|
283
|
+
|
|
284
|
+
**What to do:** nothing, unless a test asserts on the exact error value. Assert
|
|
285
|
+
on `error.message` instead.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 9. Dependency housekeeping (no action required)
|
|
290
|
+
|
|
291
|
+
- `js-yaml` gained an override for [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5)
|
|
292
|
+
(high). **`@nestjs/swagger` exact-pins `js-yaml@5.2.1`, so your project needs this override too** —
|
|
293
|
+
a framework override does not reach consumer trees. See
|
|
294
|
+
[`docs/security-overrides.md`](../docs/security-overrides.md), which now lists all three affected
|
|
295
|
+
packages.
|
|
296
|
+
- The `minimatch` override range was narrowed to `>=9.0.0`; majors 5–8 export a callable function and
|
|
297
|
+
would break under a forced lift to 10.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Troubleshooting
|
|
302
|
+
|
|
303
|
+
| Symptom | Cause | Fix |
|
|
304
|
+
|---------|-------|-----|
|
|
305
|
+
| Migration now fails with `is incomplete: N of M chunks stored` | The asset was already broken in the database; the old helper never checked | See §1 — verify the source asset, remove the orphan, re-run |
|
|
306
|
+
| Migration now fails with `ENOENT` | The source path was always wrong; it used to hang instead of failing | Fix the path. It is resolved against the **helper module's** directory |
|
|
307
|
+
| A test asserting `'File not found'` fails | The message now carries its ErrorCode prefix | Match on a substring, or on `ErrorCode.FILE_NOT_FOUND` |
|
|
308
|
+
| A download that returned 500 now returns 404 | `checkRights()` refusal is handled properly instead of crashing | Intended — see §4 |
|
|
309
|
+
| An upload fails with `may execute as script` | The type is markup or script and is now rejected regardless of the filter | Intended — see §6, and opt out only under the conditions named there |
|
|
310
|
+
| An upload of a document type that used to pass now fails | The filter matched it as a substring before | Move to `allowList` naming the type exactly — see §5 |
|
|
311
|
+
| Uploads that accepted every type now accept JPEG/PNG only | `fileTypeRegex: undefined` no longer disables the filter | Pass an explicit `allowList` — see §7 |
|
|
312
|
+
| A test asserting on the filter's rejection string fails | Rejections are an `Error` now, not a string | Assert on `error.message` — see §8 |
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Verification
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
pnpm test
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`src/core/common/helpers/file.helper.spec.ts` covers the substring class (values that merely CONTAIN
|
|
323
|
+
an allowed token), the scriptable-type rejection on both forms, the opt-out, and that the legacy
|
|
324
|
+
`RegExp` form still accepts what it accepted before.
|
|
325
|
+
`src/core/modules/migrate/helpers/migration.helper.spec.ts` and
|
|
326
|
+
`tests/migrate/upload-file-to-gridfs.e2e-spec.ts` cover the completeness check against a real GridFS
|
|
327
|
+
bucket; `src/core/modules/file/core-file.controller.spec.ts` covers the download error paths.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Related Documentation
|
|
332
|
+
|
|
333
|
+
- [`src/core/modules/migrate/README.md`](../src/core/modules/migrate/README.md) — `uploadFileToGridFS()`, `assertGridFsFileComplete()`
|
|
334
|
+
- [`src/core/modules/file/README.md`](../src/core/modules/file/README.md) — download endpoints, error responses
|
|
335
|
+
- [`docs/security-overrides.md`](../docs/security-overrides.md) — overrides consumers must replicate
|