feat(portal-bff): rate limiting + structured error filter
closes the phase-2 hardening list main.ts has been advertising since #122 (helmet + cors + csrf). two new middlewares + an alignment pass so every bff error follows a single response contract. structured error filter: global ExceptionFilter (registered via app.useGlobalFilters at the top of bootstrap()) normalises every 4xx/5xx response to: { error: { code, message, traceId } } - code: stable token the spa can switch on. either explicit on the HttpException's response object (UnauthorizedException({ code: 'unauthenticated', message })) or derived from status. 500s always 'internal'. - message: safe text. 500s NEVER leak the underlying exception (full message + stack go to pino's err: field, never to the response body). - traceId: otel trace id for cross-correlation. exported errorResponse(code, message) helper for raw-middleware callers (csrf, rate-limit) so the envelope is identical whether the response came through nest's filter or a short-circuit middleware. rate limiting: express-rate-limit mounted after the session middleware. - dynamic max per request: 10/min on /api/auth/login + /api/auth/ callback (RATE_LIMIT_AUTH_PER_MINUTE), 120/min elsewhere (RATE_LIMIT_PER_MINUTE). - bucket key = session id when auth, remote ip otherwise. rotating sessions doesn't dodge the limit; auth'd users get per-account fairness regardless of source ip. - /api/health skipped. orchestrator polls don't burn user quota. - 429 uses the shared errorResponse envelope. - in-memory store. single-instance v1 per adr-0015. redis-backed store is one config arg the day we scale out horizontally. alignment pass on existing error responses: - csrf middleware used to return { error: 'csrf' }. now uses errorResponse('csrf', 'CSRF token missing or invalid'). - /auth/me used to res.status(401).json({ error: 'unauthenticated' }) directly. now throws UnauthorizedException({ code: ..., message: ... }) so the filter formats it. identical shape on the wire. type-resolution fix (transitive): @types/express@4.17.25 was being pulled in by http-proxy-middleware (nx's webpack-dev-server). express-rate-limit's .d.ts files import 'express', and tsc was matching the v4 copy — causing Request type mismatches with our v5-based code. added "@types/express": "^5.0.6" to pnpm.overrides so the workspace pins one version everywhere. specs: - 199/199 portal-bff (was 174 before; +25 specs across StructuredErrorFilter, rate-limit middleware, csrf alignment, /me alignment). - feature-auth 28/28, portal-shell 34/34 unchanged. - clean-env repro: env -u every config var → 261/261 pass. out of scope: - redis-backed rate-limit store (single-instance v1). - per-user rate-limit overrides (admin / service accounts). - csp fine-tuning when caddy serves the static spas.
This commit is contained in:
Generated
+12
-18
@@ -13,6 +13,7 @@ overrides:
|
||||
protobufjs@<8.0.2: '>=8.0.2'
|
||||
tmp@<0.2.4: '>=0.2.4'
|
||||
yaml@<2.8.3: '>=2.8.3'
|
||||
'@types/express': ^5.0.6
|
||||
|
||||
importers:
|
||||
|
||||
@@ -123,6 +124,9 @@ importers:
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
express-rate-limit:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.1(express@5.2.1)
|
||||
express-session:
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0
|
||||
@@ -4307,7 +4311,7 @@ packages:
|
||||
'@types/cookie-parser@1.4.10':
|
||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
'@types/express': ^5.0.6
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
@@ -4336,9 +4340,6 @@ packages:
|
||||
'@types/express-session@1.19.0':
|
||||
resolution: {integrity: sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==}
|
||||
|
||||
'@types/express@4.17.25':
|
||||
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
||||
|
||||
'@types/express@5.0.6':
|
||||
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
|
||||
|
||||
@@ -6578,7 +6579,7 @@ packages:
|
||||
resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/express': ^4.17.13
|
||||
'@types/express': ^5.0.6
|
||||
peerDependenciesMeta:
|
||||
'@types/express':
|
||||
optional: true
|
||||
@@ -14537,7 +14538,7 @@ snapshots:
|
||||
'@rspack/core': 1.6.8(@swc/helpers@0.5.21)
|
||||
'@types/bonjour': 3.5.13
|
||||
'@types/connect-history-api-fallback': 1.5.4
|
||||
'@types/express': 4.17.25
|
||||
'@types/express': 5.0.6
|
||||
'@types/express-serve-static-core': 4.19.8
|
||||
'@types/serve-index': 1.9.4
|
||||
'@types/serve-static': 1.15.10
|
||||
@@ -14551,7 +14552,7 @@ snapshots:
|
||||
connect-history-api-fallback: 2.0.0
|
||||
express: 4.22.1
|
||||
graceful-fs: 4.2.11
|
||||
http-proxy-middleware: 2.0.9(@types/express@4.17.25)
|
||||
http-proxy-middleware: 2.0.9(@types/express@5.0.6)
|
||||
ipaddr.js: 2.4.0
|
||||
launch-editor: 2.13.2
|
||||
open: 10.2.0
|
||||
@@ -14965,13 +14966,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/express': 5.0.6
|
||||
|
||||
'@types/express@4.17.25':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
'@types/express-serve-static-core': 4.19.8
|
||||
'@types/qs': 6.15.0
|
||||
'@types/serve-static': 1.15.10
|
||||
|
||||
'@types/express@5.0.6':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
@@ -17417,7 +17411,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-middleware@2.0.9(@types/express@4.17.25):
|
||||
http-proxy-middleware@2.0.9(@types/express@5.0.6):
|
||||
dependencies:
|
||||
'@types/http-proxy': 1.17.17
|
||||
http-proxy: 1.18.1(debug@4.4.3)
|
||||
@@ -17425,7 +17419,7 @@ snapshots:
|
||||
is-plain-obj: 3.0.0
|
||||
micromatch: 4.0.8
|
||||
optionalDependencies:
|
||||
'@types/express': 4.17.25
|
||||
'@types/express': 5.0.6
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
@@ -21427,7 +21421,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/bonjour': 3.5.13
|
||||
'@types/connect-history-api-fallback': 1.5.4
|
||||
'@types/express': 4.17.25
|
||||
'@types/express': 5.0.6
|
||||
'@types/express-serve-static-core': 4.19.8
|
||||
'@types/serve-index': 1.9.4
|
||||
'@types/serve-static': 1.15.10
|
||||
@@ -21441,7 +21435,7 @@ snapshots:
|
||||
connect-history-api-fallback: 2.0.0
|
||||
express: 4.22.1
|
||||
graceful-fs: 4.2.11
|
||||
http-proxy-middleware: 2.0.9(@types/express@4.17.25)
|
||||
http-proxy-middleware: 2.0.9(@types/express@5.0.6)
|
||||
ipaddr.js: 2.4.0
|
||||
launch-editor: 2.13.2
|
||||
open: 10.2.0
|
||||
|
||||
Reference in New Issue
Block a user