@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
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Controller, Delete, Get, Logger, Post, Req, Res } from '@nestjs/common';
|
|
1
|
+
import { Controller, Delete, Get, Logger, OnModuleDestroy, Optional, Post, Req, Res } from '@nestjs/common';
|
|
2
2
|
import { ApiExcludeController } from '@nestjs/swagger';
|
|
3
3
|
import { Request, Response } from 'express';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
4
5
|
|
|
5
6
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
6
7
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
8
|
import { ConfigService } from '../../common/services/config.service';
|
|
9
|
+
import { CoreRedisService } from '../../common/services/core-redis.service';
|
|
8
10
|
import { CoreBetterAuthModule } from '../better-auth/core-better-auth.module';
|
|
9
11
|
import { ErrorCode } from '../error-code/error-codes';
|
|
10
12
|
import { CoreAiMcpOAuthService } from './services/core-ai-mcp-oauth.service';
|
|
@@ -26,22 +28,65 @@ import { CoreAiMcpService } from './services/core-ai-mcp.service';
|
|
|
26
28
|
*
|
|
27
29
|
* When `ai.mcp.oauth` is enabled, the handler additionally accepts OAuth 2.1 access
|
|
28
30
|
* tokens (see `mountAiMcpOAuth`); otherwise it authenticates via Bearer/session token.
|
|
31
|
+
*
|
|
32
|
+
* ## Multi-replica
|
|
33
|
+
*
|
|
34
|
+
* A Streamable-HTTP transport is a live object holding the open response stream — it cannot
|
|
35
|
+
* be serialized, so the session map is inherently process-local and `/ai/mcp` REQUIRES sticky
|
|
36
|
+
* sessions behind a load balancer. When Redis is configured, session ids are additionally
|
|
37
|
+
* registered in a shared registry (replica + owning user), which turns a mis-routed request
|
|
38
|
+
* into an explicit 409 naming the owning replica instead of a misleading "unknown session" 404.
|
|
39
|
+
* Without Redis the behavior is unchanged.
|
|
40
|
+
*
|
|
41
|
+
* The registry entry records the OWNING USER, and only that user is ever told about the 409.
|
|
42
|
+
* Otherwise the cross-replica path would undo the local one: an authenticated caller probing
|
|
43
|
+
* ids would learn both that a session id is valid and the internal hostname/PID holding it,
|
|
44
|
+
* where the same probe against the local map deliberately answers 404.
|
|
29
45
|
*/
|
|
30
46
|
@ApiExcludeController()
|
|
31
47
|
@Controller('ai/mcp')
|
|
32
48
|
@Roles(RoleEnum.S_EVERYONE)
|
|
33
|
-
export class CoreAiMcpController {
|
|
49
|
+
export class CoreAiMcpController implements OnModuleDestroy {
|
|
34
50
|
protected readonly logger = new Logger(CoreAiMcpController.name);
|
|
35
51
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Active transports keyed by MCP session id.
|
|
54
|
+
*
|
|
55
|
+
* `ownerId` is load-bearing, not bookkeeping. The transport is built once via
|
|
56
|
+
* `createServer(user)` and stays bound to THAT user's identity and role-filtered tools for its
|
|
57
|
+
* whole life, while the lookup below is driven by a client-supplied `mcp-session-id` header.
|
|
58
|
+
* Authenticating the caller only proves they are *some* user, so without comparing the owner,
|
|
59
|
+
* anyone holding another user's session id drives that user's server — the id travels in a
|
|
60
|
+
* response header, through proxies and client logs.
|
|
61
|
+
*/
|
|
62
|
+
private readonly transports = new Map<string, { lastUsed: number; ownerId: string; transport: any }>();
|
|
38
63
|
|
|
39
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Cap on concurrent MCP sessions held by ONE user.
|
|
66
|
+
*
|
|
67
|
+
* The per-user cap is the load-bearing one. A single global cap alone made one authenticated
|
|
68
|
+
* user able to evict everybody else's sessions — each eviction closing a live SSE stream —
|
|
69
|
+
* simply by opening sessions in a loop, because the victim was always the globally oldest.
|
|
70
|
+
*/
|
|
71
|
+
private readonly maxSessionsPerUser = 25;
|
|
72
|
+
|
|
73
|
+
/** Cap on concurrent MCP sessions across all users (bounded memory). */
|
|
40
74
|
private readonly maxSessions = 500;
|
|
41
75
|
|
|
76
|
+
/** Owner id written into the shared registry — identifies THIS replica. */
|
|
77
|
+
protected readonly instanceId = `${hostname()}:${process.pid}`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* TTL of a shared registry entry. The local map is bounded by `maxSessions`, not by time,
|
|
81
|
+
* so this TTL exists only to let a crashed replica's entries expire from Redis. It is
|
|
82
|
+
* refreshed on every request of a session.
|
|
83
|
+
*/
|
|
84
|
+
protected readonly sessionTtlSeconds = 3600;
|
|
85
|
+
|
|
42
86
|
constructor(
|
|
43
87
|
private readonly mcpService: CoreAiMcpService,
|
|
44
88
|
private readonly oauthService: CoreAiMcpOAuthService,
|
|
89
|
+
@Optional() protected readonly redisService?: CoreRedisService,
|
|
45
90
|
) {}
|
|
46
91
|
|
|
47
92
|
@Post()
|
|
@@ -55,6 +100,23 @@ export class CoreAiMcpController {
|
|
|
55
100
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
56
101
|
let entry = sessionId ? this.transports.get(sessionId) : undefined;
|
|
57
102
|
|
|
103
|
+
// A session belongs to the user it was created for. Treat someone else's id as unknown
|
|
104
|
+
// rather than as an authorization error — confirming it exists would turn the endpoint into
|
|
105
|
+
// an oracle for valid session ids.
|
|
106
|
+
if (entry && entry.ownerId !== user.id) {
|
|
107
|
+
entry = undefined;
|
|
108
|
+
res.status(404).json({ error: 'Unknown or expired MCP session' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!entry && sessionId) {
|
|
113
|
+
const owner = await this.foreignSessionOwner(sessionId, user.id);
|
|
114
|
+
if (owner) {
|
|
115
|
+
this.foreignSession(res, owner);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
58
120
|
if (!entry) {
|
|
59
121
|
let StreamableHTTPServerTransport: any;
|
|
60
122
|
try {
|
|
@@ -79,18 +141,23 @@ export class CoreAiMcpController {
|
|
|
79
141
|
transport.onclose = () => {
|
|
80
142
|
if (transport.sessionId) {
|
|
81
143
|
this.transports.delete(transport.sessionId);
|
|
144
|
+
this.releaseSession(transport.sessionId);
|
|
82
145
|
}
|
|
83
146
|
};
|
|
84
|
-
entry = { lastUsed: Date.now(), transport };
|
|
147
|
+
entry = { lastUsed: Date.now(), ownerId: user.id, transport };
|
|
85
148
|
}
|
|
86
149
|
|
|
87
150
|
entry.lastUsed = Date.now();
|
|
88
151
|
await entry.transport.handleRequest(req, res, req.body);
|
|
89
152
|
|
|
90
153
|
// The sessionId is assigned during handleRequest (initialize); register after.
|
|
91
|
-
if (entry.transport.sessionId
|
|
92
|
-
this.
|
|
93
|
-
|
|
154
|
+
if (entry.transport.sessionId) {
|
|
155
|
+
if (!this.transports.has(entry.transport.sessionId)) {
|
|
156
|
+
this.evictIfNeeded(user.id);
|
|
157
|
+
this.transports.set(entry.transport.sessionId, entry);
|
|
158
|
+
}
|
|
159
|
+
// Also refreshes the TTL for an already-registered session.
|
|
160
|
+
this.registerSession(entry.transport.sessionId, user.id);
|
|
94
161
|
}
|
|
95
162
|
}
|
|
96
163
|
|
|
@@ -161,15 +228,141 @@ export class CoreAiMcpController {
|
|
|
161
228
|
return;
|
|
162
229
|
}
|
|
163
230
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
164
|
-
const
|
|
231
|
+
const held = sessionId ? this.transports.get(sessionId) : undefined;
|
|
232
|
+
// Same ownership rule as the POST path: another user's session is not ours to serve.
|
|
233
|
+
const entry = held && held.ownerId === user.id ? held : undefined;
|
|
165
234
|
if (!entry) {
|
|
235
|
+
const owner = sessionId ? await this.foreignSessionOwner(sessionId, user.id) : undefined;
|
|
236
|
+
if (owner) {
|
|
237
|
+
this.foreignSession(res, owner);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
166
240
|
res.status(404).json({ error: 'Unknown or expired MCP session' });
|
|
167
241
|
return;
|
|
168
242
|
}
|
|
169
243
|
entry.lastUsed = Date.now();
|
|
244
|
+
this.registerSession(sessionId as string, user.id);
|
|
170
245
|
await entry.transport.handleRequest(req, res, (req as any).body);
|
|
171
246
|
}
|
|
172
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Replica holding a session that this process does not hold — but ONLY when the session belongs
|
|
250
|
+
* to `userId`. Undefined when Redis is disabled, the session is unknown, it belongs to this
|
|
251
|
+
* instance, or it belongs to somebody else.
|
|
252
|
+
*
|
|
253
|
+
* The owner comparison is what keeps this path in step with the local one. Answering 409 for any
|
|
254
|
+
* session id an authenticated caller can name would confirm the id exists AND disclose the
|
|
255
|
+
* internal `<hostname>:<pid>` holding it — an oracle the local map deliberately refuses by
|
|
256
|
+
* returning 404. Only the session's own user benefits from the 409 anyway: it tells THEIR client
|
|
257
|
+
* to retry against the right replica.
|
|
258
|
+
*
|
|
259
|
+
* Never throws: a registry outage must degrade to the plain "unknown session" path, not to a 500.
|
|
260
|
+
*/
|
|
261
|
+
protected async foreignSessionOwner(sessionId: string, userId: string): Promise<string | undefined> {
|
|
262
|
+
if (!this.redisService?.enabled) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const raw = await this.redisService.getClient().get(this.sessionKey(sessionId));
|
|
267
|
+
const entry = this.parseSessionEntry(raw);
|
|
268
|
+
if (!entry || entry.userId !== userId || entry.instanceId === this.instanceId) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return entry.instanceId;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.logger.debug(`MCP session registry lookup failed: ${(err as Error).message}`);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Parse a shared registry value, or undefined when it is absent or not readable.
|
|
280
|
+
*
|
|
281
|
+
* An entry written by an older version carries the bare instance id and no owner. It is treated
|
|
282
|
+
* as unreadable on purpose: without an owner we cannot tell whether the caller may learn about
|
|
283
|
+
* it, and "fall through to 404" is the safe answer during a rolling upgrade.
|
|
284
|
+
*/
|
|
285
|
+
protected parseSessionEntry(raw: null | string): undefined | { instanceId: string; userId: string } {
|
|
286
|
+
if (!raw) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(raw);
|
|
291
|
+
return parsed?.instanceId && parsed?.userId ? parsed : undefined;
|
|
292
|
+
} catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Register the session (or refresh its TTL) in the shared registry. Fire-and-forget. */
|
|
298
|
+
protected registerSession(sessionId: string, userId: string): void {
|
|
299
|
+
if (!this.redisService?.enabled) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.redisService
|
|
303
|
+
.getClient()
|
|
304
|
+
.set(
|
|
305
|
+
this.sessionKey(sessionId),
|
|
306
|
+
JSON.stringify({ instanceId: this.instanceId, userId }),
|
|
307
|
+
'EX',
|
|
308
|
+
this.sessionTtlSeconds,
|
|
309
|
+
)
|
|
310
|
+
.catch((err: Error) => this.logger.debug(`MCP session registry write failed: ${err.message}`));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Drop the session from the shared registry (close/evict). Fire-and-forget. */
|
|
314
|
+
protected releaseSession(sessionId: string): Promise<void> {
|
|
315
|
+
if (!this.redisService?.enabled) {
|
|
316
|
+
return Promise.resolve();
|
|
317
|
+
}
|
|
318
|
+
return this.redisService
|
|
319
|
+
.getClient()
|
|
320
|
+
.del(this.sessionKey(sessionId))
|
|
321
|
+
.then(() => undefined)
|
|
322
|
+
.catch((err: Error) => this.logger.debug(`MCP session registry delete failed: ${err.message}`));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Close every live transport and drop this instance's sessions from the shared registry.
|
|
327
|
+
*
|
|
328
|
+
* Each transport holds an OPEN response stream, and an open stream keeps the HTTP server from
|
|
329
|
+
* closing — so without this, `app.close()` waits on clients that will never disconnect on their
|
|
330
|
+
* own. Releasing the registry keys matters too: a restarted replica's sessions would otherwise
|
|
331
|
+
* keep answering 409 "owned by another replica" — naming a replica that no longer exists —
|
|
332
|
+
* until their TTL expires an hour later.
|
|
333
|
+
*/
|
|
334
|
+
async onModuleDestroy(): Promise<void> {
|
|
335
|
+
const sessions = [...this.transports.keys()];
|
|
336
|
+
for (const [, entry] of this.transports) {
|
|
337
|
+
try {
|
|
338
|
+
await entry.transport?.close?.();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.logger.debug(`MCP transport close failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this.transports.clear();
|
|
344
|
+
await Promise.allSettled(sessions.map((sessionId) => this.releaseSession(sessionId)));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
protected sessionKey(sessionId: string): string {
|
|
348
|
+
return this.redisService!.key('ai-mcp-session', sessionId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 409 Conflict when the session exists — on another replica. The live transport cannot be
|
|
353
|
+
* moved, so the only fix is routing: name the owner and state the sticky-session requirement
|
|
354
|
+
* instead of returning a 404 that reads like "your session expired".
|
|
355
|
+
*/
|
|
356
|
+
protected foreignSession(res: Response, owner: string): void {
|
|
357
|
+
res.status(409).json({
|
|
358
|
+
error:
|
|
359
|
+
`MCP session belongs to another server instance (${owner}); this instance is ${this.instanceId}. ` +
|
|
360
|
+
'The MCP transport is held in process memory and cannot be shared, so /ai/mcp requires sticky ' +
|
|
361
|
+
'sessions — route every request of one MCP session to the same replica.',
|
|
362
|
+
statusCode: 409,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
173
366
|
/**
|
|
174
367
|
* 503 Service Unavailable when `@modelcontextprotocol/sdk` cannot be loaded.
|
|
175
368
|
* The SDK is lazy-imported because not every consumer needs MCP — when
|
|
@@ -212,28 +405,66 @@ export class CoreAiMcpController {
|
|
|
212
405
|
}
|
|
213
406
|
|
|
214
407
|
/**
|
|
215
|
-
*
|
|
408
|
+
* Make room for a new session of `ownerId`, evicting WITHIN the offending user's own set.
|
|
409
|
+
*
|
|
410
|
+
* Eviction is not free — it closes a live SSE stream — so who pays for it matters. The naive
|
|
411
|
+
* "drop the globally oldest" rule let one authenticated user open sessions in a loop and
|
|
412
|
+
* disconnect every other user in turn. Here a user first evicts their own oldest session at
|
|
413
|
+
* their personal cap, and when the global cap is reached the victim is the oldest session of
|
|
414
|
+
* whoever holds the MOST sessions, i.e. the account actually responsible for the pressure.
|
|
216
415
|
*/
|
|
217
|
-
private evictIfNeeded(): void {
|
|
416
|
+
private evictIfNeeded(ownerId: string): void {
|
|
417
|
+
const perUser = new Map<string, string[]>();
|
|
418
|
+
for (const [key, value] of this.transports) {
|
|
419
|
+
const owned = perUser.get(value.ownerId);
|
|
420
|
+
if (owned) {
|
|
421
|
+
owned.push(key);
|
|
422
|
+
} else {
|
|
423
|
+
perUser.set(value.ownerId, [key]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if ((perUser.get(ownerId)?.length ?? 0) >= this.maxSessionsPerUser) {
|
|
428
|
+
this.evictOldestOf(perUser.get(ownerId));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
218
432
|
if (this.transports.size < this.maxSessions) {
|
|
219
433
|
return;
|
|
220
434
|
}
|
|
435
|
+
|
|
436
|
+
let largest: string[] | undefined;
|
|
437
|
+
for (const owned of perUser.values()) {
|
|
438
|
+
if (!largest || owned.length > largest.length) {
|
|
439
|
+
largest = owned;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
this.evictOldestOf(largest);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Close and forget the least recently used session among the given ids
|
|
447
|
+
*/
|
|
448
|
+
private evictOldestOf(sessionIds: string[] | undefined): void {
|
|
221
449
|
let oldestKey: string | undefined;
|
|
222
450
|
let oldest = Infinity;
|
|
223
|
-
for (const
|
|
224
|
-
|
|
451
|
+
for (const key of sessionIds ?? []) {
|
|
452
|
+
const value = this.transports.get(key);
|
|
453
|
+
if (value && value.lastUsed < oldest) {
|
|
225
454
|
oldest = value.lastUsed;
|
|
226
455
|
oldestKey = key;
|
|
227
456
|
}
|
|
228
457
|
}
|
|
229
|
-
if (oldestKey) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
458
|
+
if (!oldestKey) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const evicted = this.transports.get(oldestKey);
|
|
462
|
+
this.transports.delete(oldestKey);
|
|
463
|
+
this.releaseSession(oldestKey);
|
|
464
|
+
try {
|
|
465
|
+
evicted?.transport.close?.();
|
|
466
|
+
} catch {
|
|
467
|
+
// ignore close errors during eviction
|
|
237
468
|
}
|
|
238
469
|
}
|
|
239
470
|
}
|
|
@@ -2,6 +2,8 @@ import { HttpException, HttpStatus, Injectable, Logger, Optional } from '@nestjs
|
|
|
2
2
|
|
|
3
3
|
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
4
4
|
import { ConfigService } from '../../../common/services/config.service';
|
|
5
|
+
import { CoreRedisService } from '../../../common/services/core-redis.service';
|
|
6
|
+
import { InMemoryRateLimitStore, RateLimitStore, RedisRateLimitStore } from '../../../common/services/rate-limit-store';
|
|
5
7
|
import { RequestContext } from '../../../common/services/request-context.service';
|
|
6
8
|
import { ErrorCode } from '../../error-code';
|
|
7
9
|
import { AiToolAuthorization, AiToolContext, AiToolResult, IAiTool } from '../interfaces/ai-tool.interface';
|
|
@@ -92,8 +94,8 @@ export interface AiRunContext {
|
|
|
92
94
|
export class CoreAiService {
|
|
93
95
|
protected readonly logger = new Logger(CoreAiService.name);
|
|
94
96
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
+
/** Rate-limit counters keyed by user id (Redis-backed when configured). */
|
|
98
|
+
protected rateLimitStore?: RateLimitStore;
|
|
97
99
|
|
|
98
100
|
constructor(
|
|
99
101
|
protected readonly connectionService: CoreAiConnectionService,
|
|
@@ -110,6 +112,7 @@ export class CoreAiService {
|
|
|
110
112
|
@Optional() protected readonly toolPolicyService?: CoreAiToolPolicyService,
|
|
111
113
|
@Optional() protected readonly modeService?: CoreAiModeService,
|
|
112
114
|
@Optional() protected readonly placeholderRegistry?: CoreAiPlaceholderRegistry,
|
|
115
|
+
@Optional() protected readonly coreRedisService?: CoreRedisService,
|
|
113
116
|
) {}
|
|
114
117
|
|
|
115
118
|
/**
|
|
@@ -901,8 +904,9 @@ export class CoreAiService {
|
|
|
901
904
|
}
|
|
902
905
|
|
|
903
906
|
/**
|
|
904
|
-
*
|
|
905
|
-
*
|
|
907
|
+
* Fixed-window per-user rate limit. Enabled when `ai.rateLimit` is present
|
|
908
|
+
* (presence implies enabled). Counters are shared across replicas when
|
|
909
|
+
* `ServerOptions.redis` is configured, process-local otherwise.
|
|
906
910
|
*/
|
|
907
911
|
protected async checkRateLimit(userId?: string): Promise<void> {
|
|
908
912
|
const cfg = ConfigService.get<{ enabled?: boolean; max?: number; windowSeconds?: number }>('ai.rateLimit');
|
|
@@ -910,31 +914,36 @@ export class CoreAiService {
|
|
|
910
914
|
return;
|
|
911
915
|
}
|
|
912
916
|
const max = cfg.max ?? 20;
|
|
913
|
-
const
|
|
914
|
-
const key = userId || 'anonymous';
|
|
915
|
-
const now = Date.now();
|
|
916
|
-
|
|
917
|
-
let bucket = this.rateBuckets.get(key);
|
|
918
|
-
if (!bucket || bucket.resetAt <= now) {
|
|
919
|
-
bucket = { count: 0, resetAt: now + windowMs };
|
|
920
|
-
this.rateBuckets.set(key, bucket);
|
|
921
|
-
}
|
|
922
|
-
bucket.count++;
|
|
923
|
-
|
|
924
|
-
// Evict expired buckets when the map grows large (bounded memory).
|
|
925
|
-
if (this.rateBuckets.size > 5000) {
|
|
926
|
-
for (const [k, b] of this.rateBuckets) {
|
|
927
|
-
if (b.resetAt <= now) {
|
|
928
|
-
this.rateBuckets.delete(k);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
917
|
+
const { count } = await this.getRateLimitStore().hit(userId || 'anonymous', cfg.windowSeconds ?? 60);
|
|
932
918
|
|
|
933
|
-
if (
|
|
919
|
+
if (count > max) {
|
|
934
920
|
throw new HttpException(ErrorCode.AI_RATE_LIMITED, HttpStatus.TOO_MANY_REQUESTS);
|
|
935
921
|
}
|
|
936
922
|
}
|
|
937
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Lazily select the rate-limit store: Redis when configured and enabled,
|
|
926
|
+
* process-local otherwise.
|
|
927
|
+
*/
|
|
928
|
+
protected getRateLimitStore(): RateLimitStore {
|
|
929
|
+
if (!this.rateLimitStore) {
|
|
930
|
+
this.rateLimitStore = this.coreRedisService?.enabled
|
|
931
|
+
? new RedisRateLimitStore(this.coreRedisService, 'ai')
|
|
932
|
+
: new InMemoryRateLimitStore(5000);
|
|
933
|
+
}
|
|
934
|
+
return this.rateLimitStore;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Stop the rate-limit store's cleanup interval (for graceful shutdown).
|
|
939
|
+
* `RedisRateLimitStore` owns an in-memory fallback store, so it needs releasing too.
|
|
940
|
+
*/
|
|
941
|
+
onModuleDestroy(): void {
|
|
942
|
+
if (this.rateLimitStore instanceof InMemoryRateLimitStore || this.rateLimitStore instanceof RedisRateLimitStore) {
|
|
943
|
+
this.rateLimitStore.destroy();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
938
947
|
/**
|
|
939
948
|
* Execute a single tool call with the user's permissions.
|
|
940
949
|
*/
|
|
@@ -4,6 +4,8 @@ import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
|
|
4
4
|
import { PassportModule } from '@nestjs/passport';
|
|
5
5
|
import { PubSub } from 'graphql-subscriptions';
|
|
6
6
|
|
|
7
|
+
import { CoreRedisPubSub } from '../../common/services/core-redis-pubsub';
|
|
8
|
+
import { CoreRedisService } from '../../common/services/core-redis.service';
|
|
7
9
|
import { AuthGuardStrategy } from './auth-guard-strategy.enum';
|
|
8
10
|
import { LegacyAuthRateLimitGuard } from './guards/legacy-auth-rate-limit.guard';
|
|
9
11
|
import { RolesGuardRegistry } from './guards/roles-guard-registry';
|
|
@@ -67,8 +69,12 @@ export class CoreAuthModule {
|
|
|
67
69
|
useClass: UserService,
|
|
68
70
|
},
|
|
69
71
|
{
|
|
72
|
+
// Redis-backed when Redis is enabled (subscriptions reach all replicas),
|
|
73
|
+
// process-local in-memory PubSub otherwise
|
|
70
74
|
provide: 'PUB_SUB',
|
|
71
|
-
|
|
75
|
+
useFactory: (redisService?: CoreRedisService) =>
|
|
76
|
+
redisService?.enabled ? new CoreRedisPubSub(redisService) : new PubSub(),
|
|
77
|
+
inject: [{ optional: true, token: CoreRedisService }],
|
|
72
78
|
},
|
|
73
79
|
{
|
|
74
80
|
provide: CoreAuthService,
|
|
@@ -26,14 +26,14 @@ import { LegacyAuthRateLimiter } from '../services/legacy-auth-rate-limiter.serv
|
|
|
26
26
|
export class LegacyAuthRateLimitGuard implements CanActivate {
|
|
27
27
|
constructor(private readonly rateLimiter: LegacyAuthRateLimiter) {}
|
|
28
28
|
|
|
29
|
-
canActivate(context: ExecutionContext): boolean {
|
|
29
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
30
30
|
// If rate limiting is disabled, always allow
|
|
31
31
|
if (!this.rateLimiter.isEnabled()) {
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const { endpoint, ip } = this.extractRequestInfo(context);
|
|
36
|
-
const result = this.rateLimiter.check(ip, endpoint);
|
|
36
|
+
const result = await this.rateLimiter.check(ip, endpoint);
|
|
37
37
|
|
|
38
38
|
if (!result.allowed) {
|
|
39
39
|
throw new HttpException(
|
|
@@ -84,26 +84,17 @@ export class LegacyAuthRateLimitGuard implements CanActivate {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Get client IP
|
|
87
|
+
* Get the client IP that the rate limit counter is keyed on.
|
|
88
|
+
*
|
|
89
|
+
* `request.ip` — NOT `x-forwarded-for` directly. Express derives `req.ip` from the forwarded
|
|
90
|
+
* chain only as far as the app's `trust proxy` setting allows, and falls back to the socket
|
|
91
|
+
* address otherwise. Reading the header ourselves skips that check entirely, so any client
|
|
92
|
+
* simply picks its own bucket by sending a fresh value per request: the counter never reaches
|
|
93
|
+
* the limit, and the brute-force protection this guard exists for is off. A deployment behind
|
|
94
|
+
* a proxy must therefore configure `trust proxy` — that is the one place where "which hop do I
|
|
95
|
+
* believe" belongs.
|
|
88
96
|
*/
|
|
89
97
|
private getClientIp(request: any): string {
|
|
90
|
-
|
|
91
|
-
return 'unknown';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Check common proxy headers
|
|
95
|
-
const forwardedFor = request.headers?.['x-forwarded-for'];
|
|
96
|
-
if (forwardedFor) {
|
|
97
|
-
// Take the first IP in the chain (original client)
|
|
98
|
-
return forwardedFor.split(',')[0].trim();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const realIp = request.headers?.['x-real-ip'];
|
|
102
|
-
if (realIp) {
|
|
103
|
-
return realIp;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Fall back to direct connection IP
|
|
107
|
-
return request.ip || request.connection?.remoteAddress || 'unknown';
|
|
98
|
+
return request?.ip || request?.socket?.remoteAddress || 'unknown';
|
|
108
99
|
}
|
|
109
100
|
}
|