@lenne.tech/nest-server 11.32.4 → 11.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/dist/config.env.js +6 -0
- package/dist/config.env.js.map +1 -1
- 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 +7 -0
- package/dist/core/common/helpers/file.helper.js +21 -4
- 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 +11 -2
- package/dist/core/modules/file/core-file.controller.js +52 -15
- 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 +228 -8
- 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/helpers/migration.helper.d.ts +2 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
- 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 +2 -2
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +3 -14
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +10 -2
- package/dist/server/modules/file/file.service.js +23 -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 +21 -3
- package/migration-guides/11.32.3-to-11.32.4.md +15 -3
- package/migration-guides/11.32.x-to-11.33.x.md +1584 -0
- package/package.json +74 -42
- package/src/config.env.ts +45 -0
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +53 -10
- 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 +120 -0
- package/src/core/modules/file/README.md +308 -53
- package/src/core/modules/file/core-file.controller.ts +168 -12
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +517 -17
- 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 +33 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
- 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 +14 -5
- package/src/server/modules/file/file.resolver.ts +11 -19
- package/src/server/modules/file/file.service.ts +89 -4
- package/src/server/modules/user/avatar.controller.ts +46 -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/file.helper.spec.ts +0 -145
- 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/file/core-file.controller.spec.ts +0 -164
- 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
- package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.33.1",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -15,21 +15,20 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "rimraf dist && nest build && pnpm run build:copy-types && pnpm run build:copy-templates && pnpm run build:add-type-references && pnpm run build:framework-api",
|
|
18
|
-
"build:framework-api": "tsx scripts/generate-framework-api.ts",
|
|
19
|
-
"build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
|
|
20
|
-
"build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
|
|
21
18
|
"build:add-type-references": "node scripts/add-type-references.js",
|
|
22
|
-
"build:
|
|
19
|
+
"build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
|
|
20
|
+
"build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
|
|
23
21
|
"build:dev": "pnpm run build",
|
|
22
|
+
"build:framework-api": "tsx scripts/generate-framework-api.ts",
|
|
23
|
+
"build:pack": "pnpm pack && echo 'use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package'",
|
|
24
24
|
"c": "pnpm run check",
|
|
25
|
+
"cf": "pnpm run check:fix",
|
|
25
26
|
"check": "node scripts/check.mjs",
|
|
26
|
-
"check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
|
|
27
27
|
"check:fix": "pnpm install && pnpm run spectaql:sync && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
|
|
28
|
-
"check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
|
|
29
28
|
"check:manifest": "node scripts/check-package-manifest.mjs",
|
|
29
|
+
"check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
|
|
30
|
+
"check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
|
|
30
31
|
"check:swc-tdz": "nest build -b swc -p tsconfig.swc-tdz.json && node scripts/check-swc-tdz.mjs",
|
|
31
|
-
"spectaql:sync": "node extras/update-spectaql-version.mjs",
|
|
32
|
-
"cf": "pnpm run check:fix",
|
|
33
32
|
"cnaf": "pnpm run check:naf",
|
|
34
33
|
"docs": "pnpm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
|
|
35
34
|
"docs:bootstrap": "node extras/update-spectaql-version.mjs && node scripts/run-spectaql.mjs",
|
|
@@ -38,36 +37,40 @@
|
|
|
38
37
|
"format:check": "oxfmt --check src/ scripts/",
|
|
39
38
|
"lint": "oxlint --ignore-path .oxlintignore src/ tests/ scripts/",
|
|
40
39
|
"lint:fix": "oxlint --fix --fix-suggestions --ignore-path .oxlintignore src/ tests/ scripts/",
|
|
40
|
+
"prepack": "pnpm run prestart:prod",
|
|
41
|
+
"prepublishOnly": "pnpm run lint && pnpm run test:ci",
|
|
41
42
|
"prestart:prod": "pnpm run build",
|
|
43
|
+
"preversion": "pnpm run lint",
|
|
42
44
|
"reinit": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm install && pnpm run lint && pnpm run test:e2e && pnpm run test:ci && pnpm run build",
|
|
43
45
|
"reinit:clean": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install && pnpm run test:e2e && pnpm run build",
|
|
44
46
|
"reinit:force": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install --force && pnpm run test:e2e",
|
|
45
47
|
"reinit:legacy": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install && pnpm run test:e2e",
|
|
48
|
+
"spectaql:sync": "node extras/update-spectaql-version.mjs",
|
|
46
49
|
"start": "pnpm run start:local",
|
|
47
|
-
"start:prod": "NODE_ENV=production node dist/main.js",
|
|
48
|
-
"start:nodemon": "ts-node -r tsconfig-paths/register src/main.ts",
|
|
49
50
|
"start:debug": "nodemon --config nodemon-debug.json",
|
|
50
51
|
"start:dev": "nodemon",
|
|
51
52
|
"start:dev:swc": "nest start -b swc -w --type-check",
|
|
52
53
|
"start:local": "NODE_ENV=local nodemon",
|
|
53
54
|
"start:local:swc": "NODE_ENV=local nest start -b swc -w --type-check",
|
|
55
|
+
"start:nodemon": "ts-node -r tsconfig-paths/register src/main.ts",
|
|
56
|
+
"start:prod": "NODE_ENV=production node dist/main.js",
|
|
54
57
|
"test": "pnpm run vitest:unit && pnpm run vitest",
|
|
55
58
|
"test:ci": "pnpm run vitest:unit && pnpm run vitest:ci",
|
|
59
|
+
"test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' -o -name '*.png' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
|
|
56
60
|
"test:cov": "pnpm run vitest:unit:cov && pnpm run vitest:cov",
|
|
57
61
|
"test:e2e": "pnpm run vitest",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
62
|
+
"test:infra": "node scripts/test-infra.mjs up",
|
|
63
|
+
"test:infra:down": "node scripts/test-infra.mjs down",
|
|
64
|
+
"test:infra:status": "node scripts/test-infra.mjs status",
|
|
65
|
+
"test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
|
|
66
|
+
"test:unit:watch": "vitest --config vitest.config.ts",
|
|
67
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
|
|
61
68
|
"vitest": "NODE_ENV=e2e vitest run --config vitest-e2e.config.ts",
|
|
62
69
|
"vitest:ci": "NODE_ENV=ci vitest run --config vitest-e2e.config.ts",
|
|
63
70
|
"vitest:cov": "NODE_ENV=e2e vitest run --coverage --config vitest-e2e.config.ts",
|
|
64
|
-
"vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
|
|
65
71
|
"vitest:unit": "vitest run --config vitest.config.ts",
|
|
66
72
|
"vitest:unit:cov": "vitest run --coverage --config vitest.config.ts",
|
|
67
|
-
"
|
|
68
|
-
"test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
|
|
69
|
-
"typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
|
|
70
|
-
"test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
|
|
73
|
+
"vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
|
|
71
74
|
"watch": "npm-watch"
|
|
72
75
|
},
|
|
73
76
|
"repository": {
|
|
@@ -85,25 +88,25 @@
|
|
|
85
88
|
"dependencies": {
|
|
86
89
|
"@apollo/server": "5.5.1",
|
|
87
90
|
"@as-integrations/express5": "1.1.2",
|
|
88
|
-
"@better-auth/passkey": "1.6.
|
|
91
|
+
"@better-auth/passkey": "1.6.26",
|
|
89
92
|
"@getbrevo/brevo": "6.0.2",
|
|
90
|
-
"@modelcontextprotocol/sdk": "1.
|
|
93
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
91
94
|
"@nestjs/apollo": "13.4.2",
|
|
92
|
-
"@nestjs/common": "11.1.
|
|
93
|
-
"@nestjs/core": "11.1.
|
|
95
|
+
"@nestjs/common": "11.1.29",
|
|
96
|
+
"@nestjs/core": "11.1.29",
|
|
94
97
|
"@nestjs/graphql": "13.4.2",
|
|
95
98
|
"@nestjs/jwt": "11.0.2",
|
|
96
99
|
"@nestjs/mongoose": "11.0.4",
|
|
97
100
|
"@nestjs/passport": "11.0.5",
|
|
98
|
-
"@nestjs/platform-express": "11.1.
|
|
101
|
+
"@nestjs/platform-express": "11.1.29",
|
|
99
102
|
"@nestjs/schedule": "6.1.3",
|
|
100
103
|
"@nestjs/swagger": "11.4.6",
|
|
101
104
|
"@nestjs/terminus": "11.1.1",
|
|
102
|
-
"@tus/file-store": "2.1.
|
|
103
|
-
"@tus/server": "2.4.
|
|
105
|
+
"@tus/file-store": "2.1.1",
|
|
106
|
+
"@tus/server": "2.4.3",
|
|
104
107
|
"@types/supertest": "7.2.1",
|
|
105
108
|
"bcrypt": "6.0.0",
|
|
106
|
-
"better-auth": "1.6.
|
|
109
|
+
"better-auth": "1.6.26",
|
|
107
110
|
"class-transformer": "0.5.1",
|
|
108
111
|
"class-validator": "0.15.1",
|
|
109
112
|
"compression": "1.8.1",
|
|
@@ -113,19 +116,19 @@
|
|
|
113
116
|
"ejs": "6.0.1",
|
|
114
117
|
"express": "5.2.1",
|
|
115
118
|
"graphql": "16.14.0",
|
|
116
|
-
"graphql-query-complexity": "
|
|
119
|
+
"graphql-query-complexity": "2.0.0",
|
|
117
120
|
"graphql-subscriptions": "3.0.0",
|
|
118
121
|
"graphql-upload": "15.0.2",
|
|
119
|
-
"graphql-ws": "6.1
|
|
120
|
-
"jose": "6.2.
|
|
121
|
-
"js-sha256": "0.
|
|
122
|
+
"graphql-ws": "6.2.1",
|
|
123
|
+
"jose": "6.2.8",
|
|
124
|
+
"js-sha256": "1.0.0",
|
|
122
125
|
"json-to-graphql-query": "2.3.0",
|
|
123
126
|
"lodash": "4.18.1",
|
|
124
127
|
"mongodb": "7.5.0",
|
|
125
|
-
"mongoose": "9.
|
|
128
|
+
"mongoose": "9.9.1",
|
|
126
129
|
"multer": "2.2.0",
|
|
127
130
|
"node-mailjet": "6.0.11",
|
|
128
|
-
"nodemailer": "9.0.
|
|
131
|
+
"nodemailer": "9.0.5",
|
|
129
132
|
"passport": "0.7.0",
|
|
130
133
|
"passport-jwt": "4.0.1",
|
|
131
134
|
"reflect-metadata": "0.2.2",
|
|
@@ -133,43 +136,72 @@
|
|
|
133
136
|
"rxjs": "7.8.2",
|
|
134
137
|
"supertest": "7.2.2",
|
|
135
138
|
"ts-morph": "28.0.0",
|
|
136
|
-
"ws": "8.21.
|
|
139
|
+
"ws": "8.21.3",
|
|
137
140
|
"yuml-diagram": "1.2.0"
|
|
138
141
|
},
|
|
142
|
+
"peerDependencies": {
|
|
143
|
+
"@aws-sdk/client-s3": ">=3.1045.0 <4",
|
|
144
|
+
"@aws-sdk/s3-request-presigner": ">=3.1045.0 <4",
|
|
145
|
+
"@tus/s3-store": ">=2.0.5 <3",
|
|
146
|
+
"bullmq": ">=5.16.0 <7",
|
|
147
|
+
"ioredis": ">=5.0.0 <7"
|
|
148
|
+
},
|
|
149
|
+
"peerDependenciesMeta": {
|
|
150
|
+
"@aws-sdk/client-s3": {
|
|
151
|
+
"optional": true
|
|
152
|
+
},
|
|
153
|
+
"@aws-sdk/s3-request-presigner": {
|
|
154
|
+
"optional": true
|
|
155
|
+
},
|
|
156
|
+
"@tus/s3-store": {
|
|
157
|
+
"optional": true
|
|
158
|
+
},
|
|
159
|
+
"bullmq": {
|
|
160
|
+
"optional": true
|
|
161
|
+
},
|
|
162
|
+
"ioredis": {
|
|
163
|
+
"optional": true
|
|
164
|
+
}
|
|
165
|
+
},
|
|
139
166
|
"devDependencies": {
|
|
167
|
+
"@aws-sdk/client-s3": "3.1106.0",
|
|
168
|
+
"@aws-sdk/s3-request-presigner": "3.1106.0",
|
|
140
169
|
"@compodoc/compodoc": "2.0.0",
|
|
141
170
|
"@nestjs/cli": "11.0.24",
|
|
142
171
|
"@nestjs/schematics": "11.1.0",
|
|
143
172
|
"@nestjs/testing": "11.1.28",
|
|
144
173
|
"@swc/cli": "0.8.1",
|
|
145
|
-
"@swc/core": "1.15.
|
|
174
|
+
"@swc/core": "1.15.47",
|
|
175
|
+
"@tus/s3-store": "2.0.5",
|
|
146
176
|
"@types/compression": "1.8.1",
|
|
147
177
|
"@types/cookie-parser": "1.4.10",
|
|
148
178
|
"@types/ejs": "3.1.5",
|
|
149
179
|
"@types/express": "5.0.6",
|
|
150
|
-
"@types/lodash": "4.17.
|
|
180
|
+
"@types/lodash": "4.17.25",
|
|
151
181
|
"@types/multer": "2.2.0",
|
|
152
|
-
"@types/node": "26.
|
|
182
|
+
"@types/node": "26.2.0",
|
|
153
183
|
"@types/nodemailer": "8.0.1",
|
|
154
184
|
"@types/passport": "1.0.17",
|
|
155
185
|
"@vitest/coverage-v8": "4.1.10",
|
|
156
186
|
"@vitest/ui": "4.1.10",
|
|
157
187
|
"ansi-colors": "4.1.3",
|
|
188
|
+
"bullmq": "6.0.9",
|
|
158
189
|
"find-file-up": "2.0.1",
|
|
159
190
|
"husky": "9.1.7",
|
|
191
|
+
"ioredis": "6.0.0",
|
|
160
192
|
"nodemon": "3.1.14",
|
|
161
193
|
"npm-watch": "0.13.0",
|
|
162
194
|
"otpauth": "9.5.1",
|
|
163
|
-
"oxfmt": "0.
|
|
164
|
-
"oxlint": "1.
|
|
195
|
+
"oxfmt": "0.62.0",
|
|
196
|
+
"oxlint": "1.77.0",
|
|
165
197
|
"rimraf": "6.1.3",
|
|
166
198
|
"ts-node": "10.9.2",
|
|
167
199
|
"tsconfig-paths": "4.2.0",
|
|
168
|
-
"tsx": "4.23.
|
|
200
|
+
"tsx": "4.23.11",
|
|
169
201
|
"tus-js-client": "4.3.1",
|
|
170
202
|
"typescript": "5.9.3",
|
|
171
|
-
"unplugin-swc": "1.5.
|
|
172
|
-
"vite": "8.1
|
|
203
|
+
"unplugin-swc": "1.5.10",
|
|
204
|
+
"vite": "8.2.1",
|
|
173
205
|
"vite-plugin-node": "8.0.0",
|
|
174
206
|
"vitest": "4.1.10"
|
|
175
207
|
},
|
package/src/config.env.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CronExpression } from '@nestjs/schedule';
|
|
|
2
2
|
import * as dotenv from 'dotenv';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
|
+
import { RoleEnum } from './core/common/enums/role.enum';
|
|
5
6
|
import { getEnvironmentConfig } from './core/common/helpers/config.helper';
|
|
6
7
|
import { IServerOptions } from './core/common/interfaces/server-options.interface';
|
|
7
8
|
|
|
@@ -100,6 +101,36 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
100
101
|
autoRegister: false,
|
|
101
102
|
},
|
|
102
103
|
execAfterInit: 'pnpm run docs:bootstrap',
|
|
104
|
+
// File access — the COARSE gate on the two inherited download routes
|
|
105
|
+
// (`GET /files/id/:id`, `GET /files/:filename`), which `FileController` inherits
|
|
106
|
+
// from `CoreFileController` unchanged so this config can reach them.
|
|
107
|
+
//
|
|
108
|
+
// Since 11.33.0 those routes are role-gated and default to `[ADMIN]`. That default
|
|
109
|
+
// is the safe one for a project that has not thought about file access yet, but it
|
|
110
|
+
// is the WRONG one for this reference server: it ships an avatar upload every
|
|
111
|
+
// signed-in user may use, so an admin-only download would make `user.avatar`
|
|
112
|
+
// unreadable by the very user it belongs to.
|
|
113
|
+
//
|
|
114
|
+
// So the gate is widened to "signed in" and the real decision is made per file in
|
|
115
|
+
// `FileService.checkRights()` (own file, or ADMIN). Roles can only answer "may this
|
|
116
|
+
// caller reach the route"; they can never answer "may this caller have THIS file".
|
|
117
|
+
// Widening here is therefore not a relaxation — it MOVES the decision to the layer
|
|
118
|
+
// that can actually make it. It is also what makes the pairing testable at all: under
|
|
119
|
+
// `[ADMIN]` the guard answers first and an ownership rule could never fire, which is
|
|
120
|
+
// how the two halves drifted apart and shipped a bug. See
|
|
121
|
+
// `migration-guides/11.32.x-to-11.33.x.md` § A.3.
|
|
122
|
+
//
|
|
123
|
+
// `uploadRoles` / `deleteRoles` stay at their `[ADMIN]` default. They govern
|
|
124
|
+
// `CoreFileResolver` members only, and this server registers its own `FileResolver`
|
|
125
|
+
// and `FileController` write endpoints, each carrying `@Roles(RoleEnum.ADMIN)`
|
|
126
|
+
// explicitly — so the two knobs are inert here, and leaving them unset keeps the
|
|
127
|
+
// framework default visible rather than restating it.
|
|
128
|
+
//
|
|
129
|
+
// NOTE for markup-driven downloads: anything stricter than `S_EVERYONE` only works
|
|
130
|
+
// from an `<img>` / `<a download>` when the session travels as a COOKIE on a
|
|
131
|
+
// same-site request. A Bearer-only frontend has to fetch such URLs programmatically
|
|
132
|
+
// and build an object URL.
|
|
133
|
+
file: { downloadRoles: [RoleEnum.S_USER] },
|
|
103
134
|
filter: {
|
|
104
135
|
maxLimit: null,
|
|
105
136
|
},
|
|
@@ -224,6 +255,9 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
224
255
|
},
|
|
225
256
|
env: 'development',
|
|
226
257
|
execAfterInit: 'pnpm run docs:bootstrap',
|
|
258
|
+
// Coarse download gate widened to any signed-in user; the per-file rule lives in
|
|
259
|
+
// `FileService.checkRights()`. Full rationale in the `ci` block above.
|
|
260
|
+
file: { downloadRoles: [RoleEnum.S_USER] },
|
|
227
261
|
filter: {
|
|
228
262
|
maxLimit: null,
|
|
229
263
|
},
|
|
@@ -376,6 +410,9 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
376
410
|
autoRegister: false,
|
|
377
411
|
},
|
|
378
412
|
execAfterInit: 'pnpm run docs:bootstrap',
|
|
413
|
+
// Coarse download gate widened to any signed-in user; the per-file rule lives in
|
|
414
|
+
// `FileService.checkRights()`. Full rationale in the `ci` block above.
|
|
415
|
+
file: { downloadRoles: [RoleEnum.S_USER] },
|
|
379
416
|
filter: {
|
|
380
417
|
maxLimit: null,
|
|
381
418
|
},
|
|
@@ -534,6 +571,9 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
534
571
|
autoRegister: false,
|
|
535
572
|
},
|
|
536
573
|
execAfterInit: 'pnpm run docs:bootstrap',
|
|
574
|
+
// Coarse download gate widened to any signed-in user; the per-file rule lives in
|
|
575
|
+
// `FileService.checkRights()`. Full rationale in the `ci` block above.
|
|
576
|
+
file: { downloadRoles: [RoleEnum.S_USER] },
|
|
537
577
|
filter: {
|
|
538
578
|
maxLimit: null,
|
|
539
579
|
},
|
|
@@ -670,6 +710,11 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
670
710
|
},
|
|
671
711
|
env: 'production',
|
|
672
712
|
execAfterInit: 'pnpm run docs:bootstrap',
|
|
713
|
+
// Coarse download gate widened to any signed-in user; the per-file rule lives in
|
|
714
|
+
// `FileService.checkRights()`. Full rationale in the `ci` block above. Deliberately the
|
|
715
|
+
// SAME value as every other environment: a per-file rule that only runs outside
|
|
716
|
+
// production is a rule nobody has actually tested where it matters.
|
|
717
|
+
file: { downloadRoles: [RoleEnum.S_USER] },
|
|
673
718
|
filter: {
|
|
674
719
|
maxLimit: null,
|
|
675
720
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Content-Disposition` rendering for a client-supplied filename.
|
|
3
|
+
*
|
|
4
|
+
* An import-free LEAF on purpose. Two call sites in different layers need the identical value —
|
|
5
|
+
* the file controller's streaming branch (`src/core/modules/file/`) and the S3 presigned-URL
|
|
6
|
+
* branch (`src/core/common/services/`) — and `src/core/common/**` must not depend on
|
|
7
|
+
* `src/core/modules/**`. A leaf serves both without an import edge in the wrong direction, and
|
|
8
|
+
* without a cycle to reason about (see `.claude/rules/architecture.md` → "DI Token Placement
|
|
9
|
+
* (SWC-Safe)").
|
|
10
|
+
*
|
|
11
|
+
* Sharing it is the point, not just tidiness: while the logic was duplicated, the SAME file
|
|
12
|
+
* downloaded under a DIFFERENT name depending on whether `s3.presignedDownloads` was enabled.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Characters an RFC 8187 ext-value may carry unescaped (`attr-char`).
|
|
17
|
+
*
|
|
18
|
+
* `encodeURIComponent` is close but not equal: it leaves `'`, `(`, `)` and `*`
|
|
19
|
+
* unescaped, and none of those is an `attr-char`. A filename like
|
|
20
|
+
* `O'Brien (final)*.pdf` therefore produced a MALFORMED `filename*` parameter,
|
|
21
|
+
* which a strict parser is entitled to drop — falling back to the ASCII
|
|
22
|
+
* `filename` and losing the very name the ext-value exists to preserve.
|
|
23
|
+
* Over-escaping is always allowed (`value-chars = *( pct-encoded / attr-char )`),
|
|
24
|
+
* so escaping these four extra is the whole fix.
|
|
25
|
+
*/
|
|
26
|
+
const NON_ATTR_CHAR = /['()*]/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Everything that may NOT stand in the quoted `filename` fallback.
|
|
30
|
+
*
|
|
31
|
+
* The complement is printable US-ASCII (0x20–0x7E) minus `"` (0x22) and `\`
|
|
32
|
+
* (0x5C) — the two characters that would end the quoted-string early or start a
|
|
33
|
+
* quoted-pair. So the allowed ranges are 0x20–0x21, 0x23–0x5B and 0x5D–0x7E, and
|
|
34
|
+
* everything else (non-ASCII, CR/LF, other control characters, DEL) is replaced
|
|
35
|
+
* with `_`.
|
|
36
|
+
*
|
|
37
|
+
* Replacing rather than deleting is deliberate: it keeps the name's shape
|
|
38
|
+
* recognisable (`Übersicht.pdf` → `_bersicht.pdf`) instead of silently welding
|
|
39
|
+
* neighbouring words together, and it can never produce an empty parameter.
|
|
40
|
+
*/
|
|
41
|
+
const NON_QUOTABLE_ASCII = /[^\x20-\x21\x23-\x5B\x5D-\x7E]/g;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Percent-encode a filename as an RFC 8187 ext-value (the `filename*` parameter).
|
|
45
|
+
*/
|
|
46
|
+
function toExtValue(filename: string): string {
|
|
47
|
+
return encodeURIComponent(filename).replace(
|
|
48
|
+
NON_ATTR_CHAR,
|
|
49
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the `Content-Disposition` value for a client-supplied filename.
|
|
55
|
+
*
|
|
56
|
+
* Used for BOTH file delivery paths — the streamed response and the S3 presigned URL's
|
|
57
|
+
* `response-content-disposition` — so a file keeps its name whichever one serves it.
|
|
58
|
+
*
|
|
59
|
+
* Exported so the rendering can be unit-tested without a Nest context, and so a
|
|
60
|
+
* project overriding `CoreFileController.setFileHeaders` can reuse it.
|
|
61
|
+
*
|
|
62
|
+
* Filenames are client-supplied on both the multer path (`multerFileToUpload()`
|
|
63
|
+
* keeps `originalname`, where the old diskStorage path generated the name
|
|
64
|
+
* server-side) and the tus path (`Upload-Metadata`), so this value has to hold
|
|
65
|
+
* up against a hostile name as well as merely an awkward one.
|
|
66
|
+
*
|
|
67
|
+
* Two parameters, because they answer to two different specs and one client set
|
|
68
|
+
* each:
|
|
69
|
+
*
|
|
70
|
+
* - `filename` is an RFC 6266 quoted-string, and NOTHING percent-decodes it.
|
|
71
|
+
* Percent-encoding it protects nothing the quotes do not already handle — it
|
|
72
|
+
* only makes a client without `filename*` support save
|
|
73
|
+
* `Jahresbericht%202024.pdf`. So it is sanitised to quotable ASCII and left
|
|
74
|
+
* literal.
|
|
75
|
+
* - `filename*` is the RFC 8187 ext-value that carries the real, non-ASCII name
|
|
76
|
+
* for every client that understands it, and it wins over `filename` per RFC
|
|
77
|
+
* 6266 §4.3 when both are present.
|
|
78
|
+
*
|
|
79
|
+
* Injection is closed by the quoting plus the sanitiser, not by the encoding.
|
|
80
|
+
* Before 11.33.0 this header was emitted bare (`filename=${file.filename}`), so a
|
|
81
|
+
* `"` or a `; ` let the caller append or replace parameters outright — and an
|
|
82
|
+
* appended `filename*` WINS over the one meant here. A CR/LF is worse than that:
|
|
83
|
+
* it makes Node's `setHeader` throw `ERR_INVALID_CHAR`, turning that one file's
|
|
84
|
+
* download into a permanent 500.
|
|
85
|
+
*/
|
|
86
|
+
export function buildContentDisposition(filename?: string): string {
|
|
87
|
+
const name = filename || 'download';
|
|
88
|
+
const quotable = name.replace(NON_QUOTABLE_ASCII, '_');
|
|
89
|
+
return `attachment; filename="${quotable}"; filename*=UTF-8''${toExtValue(name)}`;
|
|
90
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
|
2
|
-
import {
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { diskStorage, memoryStorage } from 'multer';
|
|
3
4
|
import { extname } from 'path';
|
|
5
|
+
import { Readable } from 'stream';
|
|
6
|
+
|
|
7
|
+
import type { FileUploadSource } from '../../modules/file/interfaces/file-upload.interface';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* What an upload endpoint accepts: exact mimetypes and exact file extensions.
|
|
@@ -191,6 +195,7 @@ export function multerOptionsForImageUpload(options: {
|
|
|
191
195
|
destination?: string;
|
|
192
196
|
fileSize?: number;
|
|
193
197
|
fileTypeRegex?: RegExp;
|
|
198
|
+
memory?: boolean;
|
|
194
199
|
}): MulterOptions {
|
|
195
200
|
// Set config
|
|
196
201
|
const config = {
|
|
@@ -214,15 +219,53 @@ export function multerOptionsForImageUpload(options: {
|
|
|
214
219
|
|
|
215
220
|
// Automatic storage handling
|
|
216
221
|
// For configuration see https://github.com/expressjs/multer#storage
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
//
|
|
223
|
+
// `memory: true` keeps the upload in RAM so the handler can hand it straight
|
|
224
|
+
// to a central store (GridFS/S3) via `multerFileToUpload()`. Disk storage
|
|
225
|
+
// writes to the POD's filesystem, which another replica cannot read and a
|
|
226
|
+
// restart discards — see `memoryStorage()` below.
|
|
227
|
+
storage: config.memory
|
|
228
|
+
? memoryStorage()
|
|
229
|
+
: diskStorage({
|
|
230
|
+
// Destination for uploaded file
|
|
231
|
+
// If destination is not set file will be buffered and can be processed
|
|
232
|
+
// in the method
|
|
233
|
+
destination: config.destination ? config.destination : undefined,
|
|
234
|
+
|
|
235
|
+
// Generated random file name
|
|
236
|
+
filename: multerRandomFileName(),
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Adapt a multer upload to the shape `CoreFileService` consumes.
|
|
243
|
+
*
|
|
244
|
+
* Lets a REST/multer endpoint write to the same central storage (GridFS or S3)
|
|
245
|
+
* as the GraphQL upload path, instead of the pod-local disk that multer's
|
|
246
|
+
* `diskStorage` produces. Requires `memory: true` on the multer options — a
|
|
247
|
+
* disk-stored file has no `buffer`, and this throws rather than silently
|
|
248
|
+
* storing an empty file.
|
|
249
|
+
*
|
|
250
|
+
* The returned `createReadStream` is callable more than once: it builds a fresh
|
|
251
|
+
* stream over the same buffer each time, matching graphql-upload's contract.
|
|
252
|
+
*/
|
|
253
|
+
export function multerFileToUpload(file: {
|
|
254
|
+
buffer?: Buffer;
|
|
255
|
+
mimetype?: string;
|
|
256
|
+
originalname?: string;
|
|
257
|
+
}): FileUploadSource {
|
|
258
|
+
if (!file?.buffer) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'multerFileToUpload() needs an in-memory upload: pass `memory: true` to the multer options ' +
|
|
261
|
+
'(a disk-stored file has no buffer).',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
createReadStream: () => Readable.from(file.buffer),
|
|
266
|
+
encoding: '7bit',
|
|
267
|
+
filename: file.originalname || randomBytes(16).toString('hex'),
|
|
268
|
+
mimetype: file.mimetype || 'application/octet-stream',
|
|
226
269
|
};
|
|
227
270
|
}
|
|
228
271
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { INestApplication, Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ConfigService } from '../services/config.service';
|
|
4
|
+
|
|
5
|
+
/** Signals an orchestrator uses to ask a container to stop */
|
|
6
|
+
const SHUTDOWN_SIGNALS: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A delay this long is almost certainly a typo (seconds written as milliseconds, or the other way
|
|
10
|
+
* round). Orchestrators SIGKILL after their grace period anyway — typically 10-30s — so a longer
|
|
11
|
+
* wait cannot be honored, it only guarantees the ungraceful ending it was meant to avoid.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_SHUTDOWN_DELAY_MS = 60_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Above this, the delay is very likely longer than what actually gets to run.
|
|
17
|
+
*
|
|
18
|
+
* The cap above catches typos; it does NOT describe the real ceiling, and the gap between the two
|
|
19
|
+
* is where this silently breaks. Three separate limits sit BELOW the 60s cap:
|
|
20
|
+
* Docker/Compose `stop_grace_period` defaults to 10s, Kubernetes `terminationGracePeriodSeconds`
|
|
21
|
+
* to 30s, and this framework's own `installProcessDiagnostics()` arms a 30s watchdog that calls
|
|
22
|
+
* `exit(1)`. A delay of, say, 45s passes the cap without a word and is then force-killed mid-wait:
|
|
23
|
+
* no drain, and not one `onModuleDestroy` / `onApplicationShutdown` hook runs — precisely the
|
|
24
|
+
* ending the delay exists to prevent. Warning here means the dangerous band is no longer silent.
|
|
25
|
+
*
|
|
26
|
+
* The delay is only part of the budget: whatever `app.close()` needs to drain comes after it.
|
|
27
|
+
*/
|
|
28
|
+
const SHUTDOWN_DELAY_ADVISORY_MS = 10_000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Install signal handling that keeps the app healthy for `IServerOptions.shutdownDelayMs`
|
|
32
|
+
* before shutting it down, then closes it cleanly.
|
|
33
|
+
*
|
|
34
|
+
* **Why this is not a NestJS lifecycle hook.** The obvious place looks like
|
|
35
|
+
* `beforeApplicationShutdown`, but Nest's `close()` runs
|
|
36
|
+
* `onModuleDestroy` → `beforeApplicationShutdown` → dispose (close the HTTP server) →
|
|
37
|
+
* `onApplicationShutdown`. A delay in that hook therefore waits AFTER every module has been
|
|
38
|
+
* destroyed while the socket is still accepting — so the instance keeps taking traffic with its
|
|
39
|
+
* services already torn down. That is worse than not waiting at all, which is why the wait has to
|
|
40
|
+
* happen before `close()` is entered, i.e. in the signal handler.
|
|
41
|
+
*
|
|
42
|
+
* **What the delay is for.** An orchestrator sends SIGTERM and deregisters the instance from the
|
|
43
|
+
* load balancer at the same moment, but deregistration propagates asynchronously — for a beat,
|
|
44
|
+
* requests still arrive. Staying fully healthy through that beat is what lets them be answered
|
|
45
|
+
* instead of dropped.
|
|
46
|
+
*
|
|
47
|
+
* Without a configured delay this is exactly `app.enableShutdownHooks()`, which is what the
|
|
48
|
+
* framework did before.
|
|
49
|
+
*
|
|
50
|
+
* @param app the Nest application to shut down
|
|
51
|
+
* @returns the same app, so it can be chained
|
|
52
|
+
*/
|
|
53
|
+
export function installGracefulShutdown<T extends INestApplication>(app: T): T {
|
|
54
|
+
const logger = new Logger('GracefulShutdown');
|
|
55
|
+
const configured = ConfigService.getFastButReadOnly<number | undefined>('shutdownDelayMs');
|
|
56
|
+
const delayMs = typeof configured === 'number' && configured > 0 ? configured : 0;
|
|
57
|
+
|
|
58
|
+
if (!delayMs) {
|
|
59
|
+
// Nest's own handling: close immediately on a signal, running every lifecycle hook.
|
|
60
|
+
app.enableShutdownHooks();
|
|
61
|
+
return app;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (delayMs > MAX_SHUTDOWN_DELAY_MS) {
|
|
65
|
+
logger.warn(
|
|
66
|
+
`shutdownDelayMs is ${delayMs}ms, which exceeds the ${MAX_SHUTDOWN_DELAY_MS}ms cap and is longer than a typical ` +
|
|
67
|
+
`orchestrator grace period — capping it, since a longer wait only ends in SIGKILL.`,
|
|
68
|
+
);
|
|
69
|
+
} else if (delayMs > SHUTDOWN_DELAY_ADVISORY_MS) {
|
|
70
|
+
logger.warn(
|
|
71
|
+
`shutdownDelayMs is ${delayMs}ms. Keep it well below your orchestrator's grace period AND leave room for the ` +
|
|
72
|
+
'drain that follows it — Docker/Compose stop_grace_period defaults to 10s, Kubernetes ' +
|
|
73
|
+
'terminationGracePeriodSeconds to 30s, and installProcessDiagnostics() force-exits after 30s. Exceeding any ' +
|
|
74
|
+
'of them means the process is killed mid-wait and no shutdown hook runs at all.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const effectiveDelayMs = Math.min(delayMs, MAX_SHUTDOWN_DELAY_MS);
|
|
78
|
+
|
|
79
|
+
// Deliberately NOT enableShutdownHooks(): Nest would install its own listener for the same
|
|
80
|
+
// signals and close the app immediately, in parallel with the wait below.
|
|
81
|
+
let shuttingDown = false;
|
|
82
|
+
// Hoisted out of the handler so a second signal can CANCEL the pending wait. Left inside, the
|
|
83
|
+
// timer stayed armed and fired after the impatient close had already run, calling app.close()
|
|
84
|
+
// a second time and re-running every onApplicationShutdown hook against a closed app.
|
|
85
|
+
let timer: NodeJS.Timeout | undefined;
|
|
86
|
+
|
|
87
|
+
const handle = (signal: NodeJS.Signals): void => {
|
|
88
|
+
if (shuttingDown) {
|
|
89
|
+
// A second signal means someone is impatient. Stop waiting and close now.
|
|
90
|
+
logger.warn(`${signal} received again — closing immediately`);
|
|
91
|
+
if (timer) {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
timer = undefined;
|
|
94
|
+
}
|
|
95
|
+
void app.close();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
shuttingDown = true;
|
|
99
|
+
logger.log(`${signal} received — staying healthy for ${effectiveDelayMs}ms so the load balancer can deregister`);
|
|
100
|
+
|
|
101
|
+
timer = setTimeout(() => {
|
|
102
|
+
app
|
|
103
|
+
.close()
|
|
104
|
+
.then(() => logger.log('Shutdown complete'))
|
|
105
|
+
.catch((error) => logger.error(`Shutdown failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
106
|
+
}, effectiveDelayMs);
|
|
107
|
+
// The listening server keeps the loop alive on its own; this timer must not be what holds it.
|
|
108
|
+
timer.unref?.();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
112
|
+
process.on(signal, handle);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return app;
|
|
116
|
+
}
|