@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.33.0",
|
|
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
|
},
|
|
@@ -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,85 @@
|
|
|
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';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What an upload endpoint accepts: exact mimetypes and exact file extensions.
|
|
11
|
+
*
|
|
12
|
+
* Prefer this over the legacy `RegExp` form. A single expression `.test()`ed
|
|
13
|
+
* against BOTH the mimetype and the extension searches for a SUBSTRING, so every
|
|
14
|
+
* alternative matches anywhere inside either value. An allow-list of `text` /
|
|
15
|
+
* `txt` therefore also accepts `text/html` and `text/xml`, and one containing
|
|
16
|
+
* `md` accepts every mimetype with "md" in it. Consumers hit this in practice:
|
|
17
|
+
* a filter documented as "no html" happily accepted a file named `x.txt` that
|
|
18
|
+
* was sent as `text/html`.
|
|
19
|
+
*
|
|
20
|
+
* Two separate sets also remove the reason the expression could not simply be
|
|
21
|
+
* anchored: it had to carry mimetype FRAGMENTS (`wordprocessingml`, `ms-excel`)
|
|
22
|
+
* next to bare extensions, and no anchoring satisfies both at once.
|
|
23
|
+
*/
|
|
24
|
+
export interface UploadAllowList {
|
|
25
|
+
/** Allowed file extensions, lowercase and WITH the leading dot. */
|
|
26
|
+
extensions: readonly string[];
|
|
27
|
+
/** Allowed mimetypes, lowercase and without parameters. */
|
|
28
|
+
mimeTypes: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default for image uploads — the exact-matching equivalent of the legacy
|
|
33
|
+
* `/jpeg|jpg|png/`.
|
|
34
|
+
*/
|
|
35
|
+
export const IMAGE_UPLOAD_ALLOW_LIST: UploadAllowList = {
|
|
36
|
+
extensions: ['.jpeg', '.jpg', '.png'],
|
|
37
|
+
mimeTypes: ['image/jpeg', 'image/png'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Types a browser may execute as script when it renders the stored file.
|
|
42
|
+
*
|
|
43
|
+
* These are rejected by {@link multerFileFilter} REGARDLESS of the allow-list,
|
|
44
|
+
* because the danger does not depend on what a given endpoint meant to accept:
|
|
45
|
+
* an upload served back from the API origin with one of these content types
|
|
46
|
+
* runs in that origin, with the victim's session. The check is what makes the
|
|
47
|
+
* legacy `RegExp` path safe for existing consumers without breaking their
|
|
48
|
+
* expressions — see `allowScriptableTypes` to opt out.
|
|
49
|
+
*/
|
|
50
|
+
export const SCRIPTABLE_UPLOAD_MIME_TYPES: readonly string[] = [
|
|
51
|
+
'application/javascript',
|
|
52
|
+
'application/xhtml+xml',
|
|
53
|
+
'application/xml',
|
|
54
|
+
'image/svg+xml',
|
|
55
|
+
'text/html',
|
|
56
|
+
'text/javascript',
|
|
57
|
+
'text/xml',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Extensions matching {@link SCRIPTABLE_UPLOAD_MIME_TYPES}. */
|
|
61
|
+
export const SCRIPTABLE_UPLOAD_EXTENSIONS: readonly string[] = [
|
|
62
|
+
'.htm',
|
|
63
|
+
'.html',
|
|
64
|
+
'.js',
|
|
65
|
+
'.mjs',
|
|
66
|
+
'.svg',
|
|
67
|
+
'.xhtml',
|
|
68
|
+
'.xml',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** Options for {@link multerFileFilter}. */
|
|
72
|
+
export interface MulterFileFilterOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Accept markup and script types too (`text/html`, `image/svg+xml`, …).
|
|
75
|
+
*
|
|
76
|
+
* Only set this when the stored file is never served from an origin that
|
|
77
|
+
* carries a session — e.g. a separate download host, or a route that always
|
|
78
|
+
* answers with `Content-Disposition: attachment` AND
|
|
79
|
+
* `X-Content-Type-Options: nosniff`.
|
|
80
|
+
*/
|
|
81
|
+
allowScriptableTypes?: boolean;
|
|
82
|
+
}
|
|
4
83
|
|
|
5
84
|
/**
|
|
6
85
|
* Helper class for inputs
|
|
@@ -18,14 +97,18 @@ export default class FileHelper {
|
|
|
18
97
|
/**
|
|
19
98
|
* Get function to filter files for multer with a certain mimetype & extname
|
|
20
99
|
*/
|
|
21
|
-
public static multerFileFilter(
|
|
22
|
-
|
|
100
|
+
public static multerFileFilter(
|
|
101
|
+
accept: RegExp | UploadAllowList = IMAGE_UPLOAD_ALLOW_LIST,
|
|
102
|
+
options?: MulterFileFilterOptions,
|
|
103
|
+
) {
|
|
104
|
+
return multerFileFilter(accept, options);
|
|
23
105
|
}
|
|
24
106
|
|
|
25
107
|
/**
|
|
26
108
|
* Get multer options for image upload
|
|
27
109
|
*/
|
|
28
110
|
public static multerOptionsForImageUpload(options: {
|
|
111
|
+
allowList?: UploadAllowList;
|
|
29
112
|
destination?: string;
|
|
30
113
|
fileSize?: number;
|
|
31
114
|
fileTypeRegex?: RegExp;
|
|
@@ -35,38 +118,98 @@ export default class FileHelper {
|
|
|
35
118
|
}
|
|
36
119
|
|
|
37
120
|
/**
|
|
38
|
-
*
|
|
121
|
+
* Reduce a reported mimetype to the bare type: lowercase, trimmed, without the
|
|
122
|
+
* `; charset=…` parameters a user agent may append.
|
|
123
|
+
*/
|
|
124
|
+
function normalizeMimeType(value: string): string {
|
|
125
|
+
return String(value || '')
|
|
126
|
+
.split(';')[0]
|
|
127
|
+
.trim()
|
|
128
|
+
.toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get a multer `fileFilter` that accepts only the given mimetypes / extensions.
|
|
133
|
+
*
|
|
134
|
+
* Pass an {@link UploadAllowList} — both the mimetype and the extension must
|
|
135
|
+
* appear in it, each compared as a WHOLE value. The two conditions are
|
|
136
|
+
* independent: either one alone rejects the file, while a pair that is odd yet
|
|
137
|
+
* individually allowed (`report.txt` announced as `application/pdf`) passes.
|
|
138
|
+
* An extension→mimetype MAPPING is deliberately not enforced: user agents
|
|
139
|
+
* genuinely disagree about office and audio types (macOS reports `.csv` as
|
|
140
|
+
* `text/plain`), so a mapping rejects legitimate uploads.
|
|
141
|
+
*
|
|
142
|
+
* A `RegExp` is still accepted for backwards compatibility but is
|
|
143
|
+
* **deprecated**: it is `.test()`ed against both values and therefore matches
|
|
144
|
+
* SUBSTRINGS, which is how `te?xt` ends up accepting `text/html`. Whichever form
|
|
145
|
+
* is used, the types in {@link SCRIPTABLE_UPLOAD_MIME_TYPES} /
|
|
146
|
+
* {@link SCRIPTABLE_UPLOAD_EXTENSIONS} are rejected first unless
|
|
147
|
+
* `options.allowScriptableTypes` is set — that is what closes the hole for
|
|
148
|
+
* expressions that already exist in consumer projects.
|
|
149
|
+
*
|
|
150
|
+
* Rejections are reported as a real `Error`. Passing a bare string (as this
|
|
151
|
+
* helper did before) leaves multer with an "error" that has no `message`, which
|
|
152
|
+
* NestJS's `transformException` cannot map to a 4xx.
|
|
39
153
|
*/
|
|
40
|
-
export function multerFileFilter(
|
|
154
|
+
export function multerFileFilter(
|
|
155
|
+
accept: RegExp | UploadAllowList = IMAGE_UPLOAD_ALLOW_LIST,
|
|
156
|
+
options?: MulterFileFilterOptions,
|
|
157
|
+
) {
|
|
41
158
|
return (req, file, cb) => {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
159
|
+
const mimeType = normalizeMimeType(file?.mimetype);
|
|
160
|
+
const extension = extname(String(file?.originalname || '')).toLowerCase();
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
!options?.allowScriptableTypes &&
|
|
164
|
+
(SCRIPTABLE_UPLOAD_MIME_TYPES.includes(mimeType) || SCRIPTABLE_UPLOAD_EXTENSIONS.includes(extension))
|
|
165
|
+
) {
|
|
166
|
+
return cb(new Error(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
|
|
167
|
+
}
|
|
44
168
|
|
|
45
|
-
|
|
169
|
+
const accepted =
|
|
170
|
+
accept instanceof RegExp
|
|
171
|
+
? accept.test(mimeType) && accept.test(extension)
|
|
172
|
+
: accept.mimeTypes.includes(mimeType) && accept.extensions.includes(extension);
|
|
173
|
+
|
|
174
|
+
if (accepted) {
|
|
46
175
|
return cb(null, true);
|
|
47
176
|
}
|
|
48
|
-
cb(`
|
|
177
|
+
cb(new Error(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
|
|
49
178
|
};
|
|
50
179
|
}
|
|
51
180
|
|
|
181
|
+
/** Render the accepted types for the rejection message. */
|
|
182
|
+
function describeAccept(accept: RegExp | UploadAllowList): string {
|
|
183
|
+
return accept instanceof RegExp ? String(accept) : accept.extensions.join(', ');
|
|
184
|
+
}
|
|
185
|
+
|
|
52
186
|
/**
|
|
53
187
|
* Get multer options for image upload
|
|
188
|
+
*
|
|
189
|
+
* Pass `allowList` for exact matching; `fileTypeRegex` is deprecated (see
|
|
190
|
+
* {@link multerFileFilter}). When neither is set, {@link IMAGE_UPLOAD_ALLOW_LIST}
|
|
191
|
+
* applies.
|
|
54
192
|
*/
|
|
55
193
|
export function multerOptionsForImageUpload(options: {
|
|
194
|
+
allowList?: UploadAllowList;
|
|
56
195
|
destination?: string;
|
|
57
196
|
fileSize?: number;
|
|
58
197
|
fileTypeRegex?: RegExp;
|
|
198
|
+
memory?: boolean;
|
|
59
199
|
}): MulterOptions {
|
|
60
200
|
// Set config
|
|
61
201
|
const config = {
|
|
62
202
|
fileSize: 1024 * 1024, // 1MB
|
|
63
|
-
fileTypeRegex: /jpeg|jpg|png/, // Images only
|
|
64
203
|
...options,
|
|
65
204
|
};
|
|
66
205
|
|
|
206
|
+
// An explicit regex keeps precedence so existing callers behave as before
|
|
207
|
+
// (minus the scriptable types); otherwise the exact-matching default applies.
|
|
208
|
+
const accept: RegExp | UploadAllowList = config.fileTypeRegex ?? config.allowList ?? IMAGE_UPLOAD_ALLOW_LIST;
|
|
209
|
+
|
|
67
210
|
return {
|
|
68
211
|
// File filter
|
|
69
|
-
fileFilter:
|
|
212
|
+
fileFilter: multerFileFilter(accept),
|
|
70
213
|
|
|
71
214
|
// Limits
|
|
72
215
|
limits: {
|
|
@@ -76,15 +219,53 @@ export function multerOptionsForImageUpload(options: {
|
|
|
76
219
|
|
|
77
220
|
// Automatic storage handling
|
|
78
221
|
// For configuration see https://github.com/expressjs/multer#storage
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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',
|
|
88
269
|
};
|
|
89
270
|
}
|
|
90
271
|
|