@fluojs/throttler 1.0.5 → 2.0.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/README.ko.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  ## 설치
21
21
 
22
- `@fluojs/throttler`는 배포 package manifest에서 `engines.node >=20.0.0`을 선언합니다.
22
+ `@fluojs/throttler`는 패키지 자체의 지원 계약으로 배포 package manifest에서 `engines.node >=24.0.0 <27`을 선언합니다. Node 20 및 Node 22 host를 Node.js `>=24.0.0 <27`로 올리세요. Node 24 미만과 Node 27 이상은 지원하지 않습니다.
23
23
 
24
24
  ```bash
25
25
  npm install @fluojs/throttler
@@ -95,18 +95,21 @@ ThrottlerModule.forRoot({
95
95
 
96
96
  ### 커스텀 키 생성
97
97
 
98
- 기본적으로 throttler는 raw socket `remoteAddress`만으로 클라이언트 식별자를 해석합니다. 배포가 `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 덮어쓰는 신뢰 가능한 리버스 프록시 뒤에 있다면 `trustProxyHeaders: true`로 명시적으로 opt-in 하세요. 신뢰 가능한 소켓 식별자나 프록시 식별자가 없으면 서로 다른 호출자를 같은 버킷으로 합치지 않도록 예외를 던집니다. API 키나 사용자 ID 등 다른 식별자를 사용하도록 커스터마이징할 수도 있습니다.
98
+ 기본적으로 throttler는 raw socket `remoteAddress`만으로 클라이언트 식별자를 해석합니다. 배포가 `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 덮어쓰는 신뢰 가능한 리버스 프록시 뒤에 있다면, 애플리케이션이 실제로 관찰하는 프록시 주소로 제한한 `trustProxy` 구성을 우선 권장합니다. `trustProxyHeaders: true`는 광범위한 legacy compatibility 전용이며 새 deployment에는 권장하지 않습니다. 신뢰 가능한 소켓 식별자나 프록시 식별자가 없으면 서로 다른 호출자를 같은 버킷으로 합치지 않도록 예외를 던집니다. API 키나 사용자 ID 등 다른 식별자를 사용하도록 커스터마이징할 수도 있습니다.
99
99
 
100
- 카운터는 route identity와 client identity로 구분됩니다. route 부분에는 module, controller, method, path, version, handler identity포함되므로 서로 다른 route-handler 경계가 실수로 같은 버킷을 공유하지 않습니다. 요청이 거부되면 `ThrottlerGuard`는 `429`를 반환하고 `Retry-After`를 설정합니다.
100
+ 카운터는 route identity와 client identity로 구분됩니다. route 부분에는 module, controller, method, path, version HTTP route compiler할당한 deterministic source/method position이 포함됩니다. 따라서 display name과 emitted source가 같은 서로 다른 compiled handler 격리되며, 동일한 artifact layout은 애플리케이션 인스턴스 간에 같은 identity를 생성해 distributed bucket 공유를 유지합니다. 요청이 거부되면 `ThrottlerGuard`는 `429`를 반환하고 `Retry-After`를 설정합니다.
101
101
 
102
102
  ```typescript
103
103
  ThrottlerModule.forRoot({
104
104
  ttl: 60,
105
105
  limit: 100,
106
- trustProxyHeaders: true,
106
+ // 이 알려진 reverse proxy에서 온 forwarding metadata만 허용합니다.
107
+ trustProxy: ['192.0.2.10/32'],
107
108
  });
108
109
  ```
109
110
 
111
+ `trustProxy`는 기본 client key의 명시적인 forwarding boundary입니다. `false`, trusted-hop 수, address/CIDR rule 또는 predicate를 받을 수 있습니다. 신뢰되지 않았거나 malformed인 forwarding data는 direct transport identity를 대체할 수 없습니다. `trustProxyHeaders: true`는 광범위한 legacy full-header compatibility 전용이며 새 deployment에는 `trustProxy`를 사용하세요.
112
+
110
113
  ```typescript
111
114
  ThrottlerModule.forRoot({
112
115
  ttl: 60,
@@ -130,9 +133,16 @@ ThrottlerModule.forRoot({
130
133
 
131
134
  - `ThrottlerModule.forRoot(...)`는 검증된 옵션과 provider를 등록하지만, 모든 route에 throttling을 자동으로 강제하지 않습니다. 보호가 필요한 곳마다 `@UseGuards(ThrottlerGuard)` 같은 Fluo guard metadata로 `ThrottlerGuard`를 활성화하세요.
132
135
  - 공개 정책 shape는 하나의 module default와 class 또는 method 수준 `@Throttle({ ttl, limit })` override입니다. burst와 sustained limit을 함께 두는 named multi-window definition은 HTTP middleware, custom `ThrottlerStore`, 또는 애플리케이션이 소유한 guard wrapper로 명시적으로 조합해야 합니다.
133
- - Forwarded client IP header는 기본적으로 무시됩니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰 가능한 proxy가 덮어쓰는 배포에서만 `trustProxyHeaders: true`를 활성화하세요.
136
+ - NestJS `ttl` 값은 밀리초이고 fluo `ttl` 값은 초입니다. 값을 그대로 복사하지 말고 `ttl: 60_000`을 `ttl: 60`으로 변환하세요.
137
+ - `@SkipThrottle()`에는 named 또는 `false` 형식이 없습니다. Class와 method 수준 skip은 additive하게 결합되므로, 다시 활성화할 method는 skipped controller 밖으로 옮기거나 application-owned guard wrapper를 사용하세요.
138
+ - 비동기 secret, configuration, store 생성은 동기 `ThrottlerModule.forRoot(...)` 등록 전에 해결하세요. fluo는 NestJS `forRootAsync(...)` shape을 제공하지 않습니다.
139
+ - `ThrottlerGuard`와 `keyGenerator`는 HTTP 전용입니다. WebSocket, GraphQL, RPC, queue 정책에는 별도 transport-owned guard 또는 middleware를 적용하세요.
140
+ - Bucket key와 storage call contract가 다르므로 persisted NestJS window는 기본적으로 이어지지 않습니다. 연속성이 필요하면 application-owned compatibility store 또는 bounded cutover를 사용하세요.
141
+ - Forwarded client IP header는 기본적으로 무시됩니다. 신뢰할 proxy 경계를 hop count, CIDR 목록, predicate로 선언하는 `trustProxy`를 우선 사용하세요. `trustProxyHeaders: true`는 direct peer만 신뢰하는 설정이 아니라 전체 forwarding chain을 신뢰하는 광범위한 legacy compatibility mode이며 새 deployment에는 권장하지 않습니다.
134
142
  - 제한 초과 시 보장되는 응답 계약은 HTTP `429`와 `Retry-After`입니다. 추가 rate-limit header나 response body는 exception filter 같은 애플리케이션 경계에서 더하세요.
135
143
 
144
+ 마이그레이션 예제와 전체 호환성 맵은 [NestJS → fluo Migration Map](https://github.com/fluojs/fluo/blob/main/docs/getting-started/migrate-from-nestjs.ko.md)을 참고하세요.
145
+
136
146
  ## 공개 API 개요
137
147
 
138
148
  ### 모듈
@@ -140,7 +150,7 @@ ThrottlerModule.forRoot({
140
150
  - `ThrottlerModuleOptions`: `ThrottlerModule.forRoot(...)`가 받는 공개 options shape입니다.
141
151
  - 패키지 수준 등록은 `ThrottlerModule.forRoot(options)`를 통해 지원합니다. 내부 프로바이더 조합 헬퍼와 DI 토큰은 공개 계약에 포함되지 않습니다.
142
152
 
143
- `ttl`과 `limit`은 양의 finite integer여야 합니다. `global`은 기본값이 `true`입니다. throttler provider를 가져온 모듈 범위에만 유지하려면 `global: false`를 설정하세요. `trustProxyHeaders`와 `keyGenerator`로 client identity를 조정할 있으며, `keyGenerator`를 제공할 때는 함수여야 합니다. 모듈 옵션은 guard가 연결될 때 검증되고 값으로 캡처되므로, 호출자가 나중에 options 객체를 변경해도 실행 중인 throttling 정책은 바뀌지 않습니다. `store` 옵션을 제공하지 않으면 각 `ThrottlerGuard` 인스턴스가 자체 in-memory store를 소유합니다. 저장소를 공유하거나 외부에서 관리해야 한다면 `RedisThrottlerStore` 같은 `ThrottlerStore` 구현을 전달하세요.
153
+ `ttl`과 `limit`은 양의 finite integer여야 합니다. `global`은 기본값이 `true`입니다. throttler provider를 가져온 모듈 범위에만 유지하려면 `global: false`를 설정하세요. `trustProxy`는 `@fluojs/http`의 `TrustProxyPolicy`를 사용하는 권장 명시적 경계이고, `trustProxyHeaders`는 광범위한 legacy compatibility 전용이며 새 deployment에는 권장하지 않습니다. `keyGenerator`를 제공할 때는 함수여야 합니다. 모듈 옵션은 guard가 연결될 때 검증되고 값으로 캡처되므로, 호출자가 나중에 options 객체를 변경해도 실행 중인 throttling 정책은 바뀌지 않습니다. `store` 옵션을 제공하지 않으면 각 `ThrottlerGuard` 인스턴스가 자체 in-memory store를 소유합니다. 저장소를 공유하거나 외부에서 관리해야 한다면 `RedisThrottlerStore` 같은 `ThrottlerStore` 구현을 전달하세요.
144
154
 
145
155
  ### 데코레이터
146
156
  - `@Throttle({ ttl, limit })`: 클래스나 메서드에 특정 속도 제한을 설정합니다.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Decorator-based rate limiting for fluo applications with in-memory and Redis sto
19
19
 
20
20
  ## Installation
21
21
 
22
- `@fluojs/throttler` declares `engines.node >=20.0.0` in its published package manifest.
22
+ `@fluojs/throttler` declares `engines.node >=24.0.0 <27` in its published package manifest as its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js `>=24.0.0 <27`; Node versions below 24 and Node 27+ are unsupported.
23
23
 
24
24
  ```bash
25
25
  npm install @fluojs/throttler
@@ -95,18 +95,21 @@ You can also pass any object that implements the `ThrottlerStore` contract throu
95
95
 
96
96
  ### Custom Key Generation
97
97
 
98
- By default, the throttler resolves client identity from the raw socket `remoteAddress` only. If your deployment sits behind a trusted reverse proxy that rewrites `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, opt in with `trustProxyHeaders: true`. If no trusted socket or proxy identity is available, it throws instead of collapsing unrelated callers into a shared bucket. You can also customize this to use API keys, user IDs, or other identifiers.
98
+ By default, the throttler resolves client identity from the raw socket `remoteAddress` only. For a deployment behind a trusted reverse proxy that rewrites `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, configure `trustProxy` as the primary recommendation and restrict it to the proxy addresses your application actually observes. `trustProxyHeaders: true` is broad legacy compatibility only and is not recommended for new deployments. If no trusted socket or proxy identity is available, it throws instead of collapsing unrelated callers into a shared bucket. You can also customize this to use API keys, user IDs, or other identifiers.
99
99
 
100
- Counters are scoped by route identity and client identity. The route portion includes module, controller, method, path, version, and handler identity so different route-handler boundaries do not share buckets accidentally. When a request is rejected, `ThrottlerGuard` returns `429` and sets `Retry-After`.
100
+ Counters are scoped by route identity and client identity. The route portion includes module, controller, method, path, version, and the deterministic source/method position assigned by the HTTP route compiler. Distinct compiled handlers therefore remain isolated even when their display names and emitted source match, while identical artifact layouts produce the same identity across application instances for distributed bucket sharing. When a request is rejected, `ThrottlerGuard` returns `429` and sets `Retry-After`.
101
101
 
102
102
  ```typescript
103
103
  ThrottlerModule.forRoot({
104
104
  ttl: 60,
105
105
  limit: 100,
106
- trustProxyHeaders: true,
106
+ // Accept forwarding metadata only from this known reverse proxy.
107
+ trustProxy: ['192.0.2.10/32'],
107
108
  });
108
109
  ```
109
110
 
111
+ `trustProxy` is the explicit forwarding boundary for the default client key. It accepts `false`, a trusted-hop count, address/CIDR rules, or a predicate. Untrusted or malformed forwarding data cannot replace the direct transport identity. `trustProxyHeaders: true` preserves broad legacy full-header compatibility only; use `trustProxy` for new deployments.
112
+
110
113
  ```typescript
111
114
  ThrottlerModule.forRoot({
112
115
  ttl: 60,
@@ -130,9 +133,16 @@ When migrating from `@nestjs/throttler`, treat `@fluojs/throttler` as an explici
130
133
 
131
134
  - `ThrottlerModule.forRoot(...)` registers validated options and providers, but it does not automatically enforce throttling on every route. Activate `ThrottlerGuard` with Fluo guard metadata such as `@UseGuards(ThrottlerGuard)` wherever enforcement is required.
132
135
  - The public policy shape is one module default plus class- or method-level `@Throttle({ ttl, limit })` overrides. Named multi-window definitions such as burst plus sustained limits require explicit composition through HTTP middleware, a custom `ThrottlerStore`, or an application-owned guard wrapper.
133
- - Forwarded client IP headers are ignored by default. Enable `trustProxyHeaders: true` only behind a trusted proxy that overwrites `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`.
136
+ - NestJS `ttl` values are milliseconds; fluo `ttl` values are seconds. Convert `ttl: 60_000` to `ttl: 60` rather than copying the value directly.
137
+ - `@SkipThrottle()` has no named or `false` form. Class- and method-level skips combine additively, so move a reactivated method outside a skipped controller or use an application-owned guard wrapper.
138
+ - Resolve async secrets, configuration, and store construction before synchronous `ThrottlerModule.forRoot(...)` registration; fluo does not provide NestJS `forRootAsync(...)` shapes.
139
+ - `ThrottlerGuard` and `keyGenerator` are HTTP-only. Apply separate transport-owned guards or middleware to WebSocket, GraphQL, RPC, and queue policies.
140
+ - Persisted NestJS windows do not continue by default because bucket keys and storage call contracts differ. Use an application-owned compatibility store or a bounded cutover when continuity is required.
141
+ - Forwarded client IP headers are ignored by default. Prefer `trustProxy` to declare a hop count, CIDR list, or predicate for a trusted proxy boundary. `trustProxyHeaders: true` is broad legacy compatibility mode that trusts the complete forwarding chain, not only the direct peer, and is not recommended for new deployments.
134
142
  - The guaranteed limit-exceeded response contract is HTTP `429` with `Retry-After`. Additional rate-limit headers or response bodies should be added at the application boundary, for example with an exception filter.
135
143
 
144
+ For migration examples and the complete compatibility map, see the [NestJS → fluo Migration Map](https://github.com/fluojs/fluo/blob/main/docs/getting-started/migrate-from-nestjs.md).
145
+
136
146
  ## Public API Overview
137
147
 
138
148
  ### Modules
@@ -140,7 +150,7 @@ When migrating from `@nestjs/throttler`, treat `@fluojs/throttler` as an explici
140
150
  - `ThrottlerModuleOptions`: Public options shape accepted by `ThrottlerModule.forRoot(...)`.
141
151
  - Package-level registration is supported through `ThrottlerModule.forRoot(options)`. Internal provider-composition helpers and DI tokens are not part of the public contract.
142
152
 
143
- `ttl` and `limit` must be positive finite integers. `global` defaults to `true`; set `global: false` when the throttler providers should stay scoped to the importing module. `trustProxyHeaders` and `keyGenerator` customize client identity; `keyGenerator`, when provided, must be a function. Module options are validated and captured by value when the guard is wired so later mutation of the caller's options object does not change live throttling policy. If no `store` option is supplied, each `ThrottlerGuard` instance owns its own in-memory store; pass a `ThrottlerStore` implementation such as `RedisThrottlerStore` when storage must be shared or externally managed.
153
+ `ttl` and `limit` must be positive finite integers. `global` defaults to `true`; set `global: false` when the throttler providers should stay scoped to the importing module. `trustProxy` is the recommended explicit boundary and uses `TrustProxyPolicy` from `@fluojs/http`; `trustProxyHeaders` is broad legacy compatibility only and is not recommended for new deployments. `keyGenerator`, when provided, must be a function. Module options are validated and captured by value when the guard is wired so later mutation of the caller's options object does not change live throttling policy. If no `store` option is supplied, each `ThrottlerGuard` instance owns its own in-memory store; pass a `ThrottlerStore` implementation such as `RedisThrottlerStore` when storage must be shared or externally managed.
144
154
 
145
155
  ### Decorators
146
156
  - `@Throttle({ ttl, limit })`: Sets a specific rate limit for a class or method.
@@ -1 +1 @@
1
- {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAoD,MAAM,cAAc,CAAC;AAa/G,OAAO,KAAK,EAAE,sBAAsB,EAAuC,MAAM,YAAY,CAAC;AAgE9F;;GAEG;AACH,qBACa,cAAe,YAAW,KAAK;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+D;IAEhG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,OAAO,EAAE,sBAAsB;IAO3C,OAAO,CAAC,iBAAiB;IA8CzB;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;CAkC3D"}
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,KAAK,EACV,KAAK,YAAY,EAIlB,MAAM,cAAc,CAAC;AAatB,OAAO,KAAK,EAAE,sBAAsB,EAAuC,MAAM,YAAY,CAAC;AAsE9F;;GAEG;AACH,qBACa,cAAe,YAAW,KAAK;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiE;IAElG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,OAAO,EAAE,sBAAsB;IAO3C,OAAO,CAAC,iBAAiB;IA8BzB;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;CAsC3D"}
package/dist/guard.js CHANGED
@@ -7,7 +7,7 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
7
7
  import { Inject } from '@fluojs/core';
8
8
  import { getStandardMetadataBag } from '@fluojs/core/internal';
9
9
  import { TooManyRequestsException } from '@fluojs/http';
10
- import { resolveClientIdentity } from '@fluojs/http/internal';
10
+ import { getCompiledRouteIdentity, resolveClientIdentity } from '@fluojs/http/internal';
11
11
  import { getClassSkipThrottleMetadata, getClassThrottleMetadata, getSkipThrottleMetadata, getThrottleMetadata, throttleRouteMetadataKey } from './decorators.js';
12
12
  import { createMemoryThrottlerStore } from './store.js';
13
13
  import { throttlerRetryAfterMsSymbol } from './store-internals.js';
@@ -24,8 +24,9 @@ function getMethodMetadataBag(controllerToken, methodName) {
24
24
  const routeMap = classBag[throttleRouteMetadataKey];
25
25
  return routeMap?.get(methodName);
26
26
  }
27
- function defaultKeyGenerator(ctx, trustProxyHeaders) {
27
+ function defaultKeyGenerator(ctx, trustProxy, trustProxyHeaders) {
28
28
  return resolveClientIdentity(ctx.request, {
29
+ trustProxy,
29
30
  trustProxyHeaders
30
31
  });
31
32
  }
@@ -37,7 +38,8 @@ function buildHandlerKey(handler) {
37
38
  const version = handler.route.version ?? handler.metadata.effectiveVersion ?? 'unversioned';
38
39
  const moduleName = handler.metadata.moduleType?.name || '<moduleless>';
39
40
  const controllerName = handler.controllerToken.name || '<anonymous-controller>';
40
- return [`module:${encodeURIComponent(moduleName)}`, `controller:${encodeURIComponent(controllerName)}`, `method:${handler.route.method}`, `path:${encodeURIComponent(handler.route.path)}`, `version:${encodeURIComponent(version)}`, `handler:${encodeURIComponent(handler.methodName)}`].join('|');
41
+ const compiledRouteIdentity = getCompiledRouteIdentity(handler) ?? '<uncompiled>';
42
+ return [`module:${encodeURIComponent(moduleName)}`, `controller:${encodeURIComponent(controllerName)}`, `method:${handler.route.method}`, `path:${encodeURIComponent(handler.route.path)}`, `version:${encodeURIComponent(version)}`, `handler:${encodeURIComponent(handler.methodName)}`, `compiled:${encodeURIComponent(compiledRouteIdentity)}`].join('|');
41
43
  }
42
44
  function resolveRetryAfterSeconds(entry, now) {
43
45
  const retryAfterMs = entry.retryAfterMs ?? entry[throttlerRetryAfterMsSymbol];
@@ -64,14 +66,7 @@ class ThrottlerGuard {
64
66
  this.store = validatedOptions.store ?? createMemoryThrottlerStore();
65
67
  }
66
68
  getResolvedPolicy(handler) {
67
- let controllerPolicies = this.resolvedPolicies.get(handler.controllerToken);
68
- if (!controllerPolicies) {
69
- controllerPolicies = new Map();
70
- this.resolvedPolicies.set(handler.controllerToken, controllerPolicies);
71
- }
72
- const version = handler.route.version ?? handler.metadata.effectiveVersion ?? 'unversioned';
73
- const cacheKey = [handler.metadata.moduleType?.name || '<moduleless>', handler.controllerToken.name || '<anonymous-controller>', handler.methodName, handler.route.method, handler.route.path, version].join('\u0000');
74
- const cachedPolicy = controllerPolicies.get(cacheKey);
69
+ const cachedPolicy = this.resolvedPolicies.get(handler);
75
70
  if (cachedPolicy) {
76
71
  return cachedPolicy;
77
72
  }
@@ -90,7 +85,7 @@ class ThrottlerGuard {
90
85
  skip,
91
86
  ttlSeconds: resolvedThrottle.ttl
92
87
  };
93
- controllerPolicies.set(cacheKey, policy);
88
+ this.resolvedPolicies.set(handler, policy);
94
89
  return policy;
95
90
  }
96
91
 
@@ -115,7 +110,7 @@ class ThrottlerGuard {
115
110
  requestContext,
116
111
  response: requestContext.response
117
112
  };
118
- const clientKey = this.options.keyGenerator ? this.options.keyGenerator(middlewareCtx) : defaultKeyGenerator(middlewareCtx, this.options.trustProxyHeaders ?? false);
113
+ const clientKey = this.options.keyGenerator ? this.options.keyGenerator(middlewareCtx) : defaultKeyGenerator(middlewareCtx, this.options.trustProxy ?? (this.options.trustProxyHeaders ? Number.MAX_SAFE_INTEGER : false), this.options.trustProxyHeaders ?? false);
119
114
  const storeKey = buildStoreKey(policy.encodedHandlerKey, clientKey);
120
115
  const now = Date.now();
121
116
  const rawEntry = await this.store.consume(storeKey, {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MiddlewareContext } from '@fluojs/http';
1
+ import type { MiddlewareContext, TrustProxyPolicy } from '@fluojs/http';
2
2
  /**
3
3
  * Snapshot of a client's current rate-limit window state returned by a throttler store.
4
4
  *
@@ -60,10 +60,20 @@ export interface ThrottlerModuleOptions {
60
60
  /** Maximum number of requests allowed within the window (module-wide default). */
61
61
  limit: number;
62
62
  /**
63
- * Trust `Forwarded`, `X-Forwarded-For`, and `X-Real-IP` before the raw socket address.
64
- * Enable this only when the adapter sits behind a trusted proxy that rewrites those headers.
63
+ * Broad legacy compatibility mode for forwarding headers.
64
+ *
65
+ * This trusts the complete `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`
66
+ * chain and is not limited to the direct peer. Use `trustProxy` instead for
67
+ * new deployments with a known proxy boundary.
65
68
  */
66
69
  trustProxyHeaders?: boolean;
70
+ /**
71
+ * Explicit proxy boundary for the default client key generator.
72
+ *
73
+ * @remarks
74
+ * Prefer this policy over the broad legacy `trustProxyHeaders` mode.
75
+ */
76
+ trustProxy?: TrustProxyPolicy;
67
77
  /**
68
78
  * Key generator function. Defaults to conservative client identity resolution.
69
79
  * Receives the raw middleware context so custom headers (e.g. x-api-key) can be used.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,CAAC;IAClD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAExE;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,CAAC;IAClD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA0BvG;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAOjG;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,sBAAsB,GAAG,sBAAsB,CAqBtG;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAkB3F"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA2CvG;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAOjG;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,sBAAsB,GAAG,sBAAsB,CAuBtG;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAkB3F"}
@@ -18,6 +18,19 @@ function assertOptionalBoolean(value, field) {
18
18
  throw new Error(`Invalid throttler ${field}: expected a boolean when provided.`);
19
19
  }
20
20
  }
21
+ function assertOptionalTrustProxy(value) {
22
+ if (value === undefined || value === false || typeof value === 'function') {
23
+ return;
24
+ }
25
+ if (typeof value === 'number') {
26
+ assertNonNegativeFiniteInteger(value, 'trustProxy');
27
+ return;
28
+ }
29
+ if (Array.isArray(value) && value.every(rule => typeof rule === 'string' && rule.trim())) {
30
+ return;
31
+ }
32
+ throw new Error('Invalid throttler trustProxy: expected false, a hop count, address/CIDR list, or predicate.');
33
+ }
21
34
 
22
35
  /**
23
36
  * Validate one per-handler or module-level throttle policy.
@@ -44,6 +57,7 @@ export function validateThrottlerModuleOptions(options) {
44
57
  validateThrottleOptions(options);
45
58
  assertOptionalBoolean(options.global, 'global');
46
59
  assertOptionalBoolean(options.trustProxyHeaders, 'trustProxyHeaders');
60
+ assertOptionalTrustProxy(options.trustProxy);
47
61
  if (options.keyGenerator !== undefined && typeof options.keyGenerator !== 'function') {
48
62
  throw new Error('Invalid throttler keyGenerator: expected a function when provided.');
49
63
  }
@@ -55,6 +69,7 @@ export function validateThrottlerModuleOptions(options) {
55
69
  keyGenerator: options.keyGenerator,
56
70
  limit: options.limit,
57
71
  store: options.store,
72
+ trustProxy: options.trustProxy,
58
73
  trustProxyHeaders: options.trustProxyHeaders,
59
74
  ttl: options.ttl
60
75
  };
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "redis",
10
10
  "decorator"
11
11
  ],
12
- "version": "1.0.5",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/throttler"
19
19
  },
20
20
  "engines": {
21
- "node": ">=20.0.0"
21
+ "node": ">=24.0.0 <27"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
@@ -36,14 +36,14 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.1.0",
40
- "@fluojs/di": "^2.0.0",
41
- "@fluojs/http": "^2.0.1",
42
- "@fluojs/runtime": "^2.0.1"
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/di": "^3.0.0",
41
+ "@fluojs/http": "^3.0.0",
42
+ "@fluojs/runtime": "^3.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "ioredis": "^5.0.0",
46
- "@fluojs/redis": "^1.1.0"
46
+ "@fluojs/redis": "^2.0.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@fluojs/redis": {
@@ -55,8 +55,8 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "ioredis": "^5.10.0",
58
- "vitest": "^3.2.4",
59
- "@fluojs/testing": "^2.0.0"
58
+ "vitest": "^4.1.11",
59
+ "@fluojs/testing": "^3.0.0"
60
60
  },
61
61
  "scripts": {
62
62
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",