@cqrs-ddd/pipeline-rate-limit 0.5.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.
Files changed (48) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +36 -0
  4. package/dist/errors/missing-partition.error.d.ts +13 -0
  5. package/dist/errors/missing-partition.error.d.ts.map +1 -0
  6. package/dist/errors/missing-partition.error.js +14 -0
  7. package/dist/errors/missing-partition.error.js.map +1 -0
  8. package/dist/errors/rate-limit-exceeded.error.d.ts +35 -0
  9. package/dist/errors/rate-limit-exceeded.error.d.ts.map +1 -0
  10. package/dist/errors/rate-limit-exceeded.error.js +41 -0
  11. package/dist/errors/rate-limit-exceeded.error.js.map +1 -0
  12. package/dist/helpers/build-attributes.d.ts +47 -0
  13. package/dist/helpers/build-attributes.d.ts.map +1 -0
  14. package/dist/helpers/build-attributes.js +54 -0
  15. package/dist/helpers/build-attributes.js.map +1 -0
  16. package/dist/helpers/build-key.d.ts +23 -0
  17. package/dist/helpers/build-key.d.ts.map +1 -0
  18. package/dist/helpers/build-key.js +38 -0
  19. package/dist/helpers/build-key.js.map +1 -0
  20. package/dist/helpers/partitioned-key.d.ts +65 -0
  21. package/dist/helpers/partitioned-key.d.ts.map +1 -0
  22. package/dist/helpers/partitioned-key.js +61 -0
  23. package/dist/helpers/partitioned-key.js.map +1 -0
  24. package/dist/helpers/rate-limit.intent.d.ts +30 -0
  25. package/dist/helpers/rate-limit.intent.d.ts.map +1 -0
  26. package/dist/helpers/rate-limit.intent.js +25 -0
  27. package/dist/helpers/rate-limit.intent.js.map +1 -0
  28. package/dist/http.d.ts +22 -0
  29. package/dist/http.d.ts.map +1 -0
  30. package/dist/http.js +27 -0
  31. package/dist/http.js.map +1 -0
  32. package/dist/index.d.ts +15 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +14 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces/rate-limit-options.interface.d.ts +64 -0
  37. package/dist/interfaces/rate-limit-options.interface.d.ts.map +1 -0
  38. package/dist/interfaces/rate-limit-options.interface.js +3 -0
  39. package/dist/interfaces/rate-limit-options.interface.js.map +1 -0
  40. package/dist/interfaces/rate-limiter.interface.d.ts +35 -0
  41. package/dist/interfaces/rate-limiter.interface.d.ts.map +1 -0
  42. package/dist/interfaces/rate-limiter.interface.js +3 -0
  43. package/dist/interfaces/rate-limiter.interface.js.map +1 -0
  44. package/dist/rate-limit.behavior.d.ts +74 -0
  45. package/dist/rate-limit.behavior.d.ts.map +1 -0
  46. package/dist/rate-limit.behavior.js +188 -0
  47. package/dist/rate-limit.behavior.js.map +1 -0
  48. package/package.json +73 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;GAIG;AAEH,OAAO,EACL,8BAA8B,GAE/B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,oCAAoC,GAGrC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,SAAS,GACV,MAAM,gCAAgC,CAAC;AAUxC,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,64 @@
1
+ import type { IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ import type { RateLimiterLike } from './rate-limiter.interface.js';
3
+ /**
4
+ * Derives the rate-limit bucket key from the pipeline context — e.g. combine
5
+ * `context.requestName` with a tenant or user id to limit per caller.
6
+ */
7
+ export type RateLimitKeyFactory = (context: IPipelineContext) => string;
8
+ /**
9
+ * Computes the points a request costs from the pipeline context — e.g. one
10
+ * point per item of a bulk command. Must return a non-negative safe integer.
11
+ */
12
+ export type RateLimitCostFactory = (context: IPipelineContext) => number;
13
+ /**
14
+ * Per-handler (and constructor-default) options for {@link RateLimitBehavior}.
15
+ *
16
+ * @example Tenant + caller scoped limit
17
+ * ```ts
18
+ * const perUser = createPartitionedRateLimitKeyFactory(
19
+ * (ctx) => ctx.items.get('userId') as string | undefined,
20
+ * );
21
+ *
22
+ * class CreateOrderHandler {
23
+ * @pipeline.wrap({ kind: 'command' }, [RateLimitBehavior, {
24
+ * keyFactory: perUser,
25
+ * points: 1,
26
+ * }])
27
+ * async handle(command: CreateOrderCommand) {}
28
+ * }
29
+ * ```
30
+ */
31
+ export interface RateLimitBehaviorOptions {
32
+ /**
33
+ * Points this request costs: a non-negative safe integer, or a
34
+ * {@link RateLimitCostFactory} computing it per request. `0` charges
35
+ * nothing: the limiter is not called and no key is built. Default `1`.
36
+ *
37
+ * @example One point per imported row
38
+ * ```ts
39
+ * points: (ctx) => (ctx.request as ImportUsersCommand).rows.length
40
+ * ```
41
+ */
42
+ points?: number | RateLimitCostFactory;
43
+ /**
44
+ * Builds the bucket key. Required whenever the behavior executes; there is
45
+ * no implicit shared bucket. Prefer `createPartitionedRateLimitKeyFactory`
46
+ * for tenant/caller-aware limits.
47
+ */
48
+ keyFactory?: RateLimitKeyFactory;
49
+ /** Optional prefix prepended to the key as `"<prefix>:<key>"`. */
50
+ keyPrefix?: string;
51
+ /**
52
+ * Per-handler limiter override — use a stricter/looser limiter for specific
53
+ * handlers without changing the global one. Defaults to the injected limiter.
54
+ */
55
+ limiter?: RateLimiterLike;
56
+ /**
57
+ * When the backing store itself errors (e.g. Redis is unreachable — distinct
58
+ * from a normal limit hit), allow the request through (`true`, default) or
59
+ * reject it (`false`). Fail-open favors availability; fail-closed favors
60
+ * strict protection.
61
+ */
62
+ failOpen?: boolean;
63
+ }
64
+ //# sourceMappingURL=rate-limit-options.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-options.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/rate-limit-options.interface.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;AAEzE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAAC;IACvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB"}
@@ -0,0 +1,3 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export {};
3
+ //# sourceMappingURL=rate-limit-options.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/rate-limit-options.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal structural shape of a `rate-limiter-flexible` result
3
+ * (`RateLimiterRes`). Declared locally so this package does not hard-depend on
4
+ * `rate-limiter-flexible` — a real result satisfies it.
5
+ */
6
+ export interface RateLimiterResLike {
7
+ /** Milliseconds until the next point(s) become available. */
8
+ msBeforeNext: number;
9
+ /** Points remaining in the current window after this consumption. */
10
+ remainingPoints: number;
11
+ /** Points consumed so far in the current window. */
12
+ consumedPoints: number;
13
+ /** Whether this consumption started a fresh window. */
14
+ isFirstInDuration: boolean;
15
+ }
16
+ /**
17
+ * Minimal structural shape of a `rate-limiter-flexible` limiter
18
+ * (`RateLimiterAbstract`). Any of its backends —
19
+ * `RateLimiterMemory`, `RateLimiterRedis`, `RateLimiterMongo`,
20
+ * `RateLimiterPostgres`, `RateLimiterMySQL`, … — satisfies this interface, so
21
+ * they are genuine drop-in replacements. Add it in your app:
22
+ * `pnpm add rate-limiter-flexible`.
23
+ */
24
+ export interface RateLimiterLike {
25
+ /**
26
+ * Consume `points` (default `1`) for `key`. Resolves with the updated
27
+ * {@link RateLimiterResLike} when allowed, or **rejects** with a
28
+ * {@link RateLimiterResLike} when the limit is exceeded (and with a plain
29
+ * `Error` when the backing store itself fails).
30
+ */
31
+ consume(key: string, points?: number): Promise<RateLimiterResLike>;
32
+ /** The configured point capacity per window, when the limiter exposes it. */
33
+ readonly points?: number;
34
+ }
35
+ //# sourceMappingURL=rate-limiter.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/rate-limiter.interface.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnE,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B"}
@@ -0,0 +1,3 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export {};
3
+ //# sourceMappingURL=rate-limiter.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.interface.js","sourceRoot":"","sources":["../../src/interfaces/rate-limiter.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
@@ -0,0 +1,74 @@
1
+ import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import type { RateLimitBehaviorOptions } from './interfaces/rate-limit-options.interface.js';
3
+ import type { RateLimiterLike, RateLimiterResLike } from './interfaces/rate-limiter.interface.js';
4
+ /**
5
+ * Unique symbol key set on `context.items` containing the result of a rate-limit check (or rejection payload).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const result = context.items.get(RATE_LIMIT_ITEM);
10
+ * ```
11
+ */
12
+ export declare const RATE_LIMIT_ITEM: unique symbol;
13
+ /**
14
+ * Typed token for {@link RATE_LIMIT_ITEM}: the rate-limit result. Reads and writes the same
15
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
16
+ */
17
+ export declare const RATE_LIMIT_ITEM_TOKEN: PipelineItemToken<RateLimiterResLike>;
18
+ /**
19
+ * Unique symbol key set on `context.items` containing the resolved rate limit bucket key string.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const key = context.items.get(RATE_LIMIT_KEY_ITEM) as string | undefined;
24
+ * ```
25
+ */
26
+ export declare const RATE_LIMIT_KEY_ITEM: unique symbol;
27
+ /**
28
+ * Typed token for {@link RATE_LIMIT_KEY_ITEM}: the resolved rate-limit key. Reads and writes the same
29
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
30
+ */
31
+ export declare const RATE_LIMIT_KEY_ITEM_TOKEN: PipelineItemToken<string>;
32
+ /**
33
+ * Pipeline behavior that enforces rate limits before a handler runs.
34
+ *
35
+ * For each request it consumes `points` (default `1`, or computed per request
36
+ * when `points` is a function) from a bucket keyed by
37
+ * {@link buildRateLimitKey}. A cost of `0` charges nothing and skips the
38
+ * limiter. If the bucket is exhausted it throws
39
+ * {@link RateLimitExceededError} (map to HTTP 429 with
40
+ * `toHttpResponse` of `@cqrs-ddd/pipeline-rate-limit/http`); otherwise the handler proceeds.
41
+ *
42
+ * `context.request` is the CQRS command/query/event, not an Express/Fastify
43
+ * request. Transport metadata needed for keying should be copied into the CQRS
44
+ * request or written to `context.items` by an earlier behavior.
45
+ *
46
+ * Backend-agnostic: it depends only on {@link RateLimiterLike}, so any
47
+ * `rate-limiter-flexible` backend (memory, Redis/Valkey, Mongo, SQL) is a
48
+ * one-line swap in the constructor.
49
+ *
50
+ * @example Per-handler limit, partitioned by tenant and authenticated caller
51
+ * ```ts
52
+ * const perUser = createPartitionedRateLimitKeyFactory(
53
+ * (ctx) => ctx.items.get('userId') as string | undefined,
54
+ * );
55
+ *
56
+ * class CreateUserHandler {
57
+ * @pipeline.wrap({ kind: 'command' }, [RateLimitBehavior, { points: 1, keyFactory: perUser }])
58
+ * async handle(command: CreateUserCommand) {}
59
+ * }
60
+ * ```
61
+ */
62
+ export declare class RateLimitBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<RateLimitBehaviorOptions> {
63
+ private readonly limiter;
64
+ static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
65
+ private readonly logger;
66
+ private readonly defaults;
67
+ constructor(limiter: RateLimiterLike, defaults?: RateLimitBehaviorOptions, logger?: PipelineLogger);
68
+ handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
69
+ /** Fail-open (default) or fail-closed when the backing store itself errors. */
70
+ private handleStoreError;
71
+ /** Shallow-merges pipeline-level options over the constructor defaults. */
72
+ resolveEffectiveOptions(options?: RateLimitBehaviorOptions): RateLimitBehaviorOptions;
73
+ }
74
+ //# sourceMappingURL=rate-limit.behavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.behavior.d.ts","sourceRoot":"","sources":["../src/rate-limit.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAG1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AAC7F,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,wCAAwC,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,iBAAiB,CAAC,kBAAkB,CACI,CAAC;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,iBAAiB,CAAC,MAAM,CACQ,CAAC;AAYzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,iBACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,wBAAwB,CAAC;IA2D1D,OAAO,CAAC,QAAQ,CAAC,OAAO;IAzD1B,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAmDrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IAEpD,YACmB,OAAO,EAAE,eAAe,EACzC,QAAQ,CAAC,EAAE,wBAAwB,EACnC,MAAM,CAAC,EAAE,cAAc,EAUxB;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CAgClB;IAED,+EAA+E;IAC/E,OAAO,CAAC,gBAAgB;IA0BxB,2EAA2E;IAC3E,uBAAuB,CACrB,OAAO,CAAC,EAAE,wBAAwB,GACjC,wBAAwB,CAG1B;CACF"}
@@ -0,0 +1,188 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { RateLimitExceededError } from './errors/rate-limit-exceeded.error.js';
4
+ import { buildRateLimitKey } from './helpers/build-key.js';
5
+ /**
6
+ * Unique symbol key set on `context.items` containing the result of a rate-limit check (or rejection payload).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const result = context.items.get(RATE_LIMIT_ITEM);
11
+ * ```
12
+ */
13
+ export const RATE_LIMIT_ITEM = Symbol('RATE_LIMIT_ITEM');
14
+ /**
15
+ * Typed token for {@link RATE_LIMIT_ITEM}: the rate-limit result. Reads and writes the same
16
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
17
+ */
18
+ export const RATE_LIMIT_ITEM_TOKEN = createPipelineItem('RATE_LIMIT_ITEM', RATE_LIMIT_ITEM);
19
+ /**
20
+ * Unique symbol key set on `context.items` containing the resolved rate limit bucket key string.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const key = context.items.get(RATE_LIMIT_KEY_ITEM) as string | undefined;
25
+ * ```
26
+ */
27
+ export const RATE_LIMIT_KEY_ITEM = Symbol('RATE_LIMIT_KEY_ITEM');
28
+ /**
29
+ * Typed token for {@link RATE_LIMIT_KEY_ITEM}: the resolved rate-limit key. Reads and writes the same
30
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
31
+ */
32
+ export const RATE_LIMIT_KEY_ITEM_TOKEN = createPipelineItem('RATE_LIMIT_KEY_ITEM', RATE_LIMIT_KEY_ITEM);
33
+ /** Whether a rejection value is a `rate-limiter-flexible` result (a limit hit). */
34
+ function isRateLimiterRes(value) {
35
+ return (typeof value === 'object' &&
36
+ value !== null &&
37
+ typeof value.msBeforeNext === 'number' &&
38
+ typeof value.remainingPoints === 'number');
39
+ }
40
+ /**
41
+ * Pipeline behavior that enforces rate limits before a handler runs.
42
+ *
43
+ * For each request it consumes `points` (default `1`, or computed per request
44
+ * when `points` is a function) from a bucket keyed by
45
+ * {@link buildRateLimitKey}. A cost of `0` charges nothing and skips the
46
+ * limiter. If the bucket is exhausted it throws
47
+ * {@link RateLimitExceededError} (map to HTTP 429 with
48
+ * `toHttpResponse` of `@cqrs-ddd/pipeline-rate-limit/http`); otherwise the handler proceeds.
49
+ *
50
+ * `context.request` is the CQRS command/query/event, not an Express/Fastify
51
+ * request. Transport metadata needed for keying should be copied into the CQRS
52
+ * request or written to `context.items` by an earlier behavior.
53
+ *
54
+ * Backend-agnostic: it depends only on {@link RateLimiterLike}, so any
55
+ * `rate-limiter-flexible` backend (memory, Redis/Valkey, Mongo, SQL) is a
56
+ * one-line swap in the constructor.
57
+ *
58
+ * @example Per-handler limit, partitioned by tenant and authenticated caller
59
+ * ```ts
60
+ * const perUser = createPartitionedRateLimitKeyFactory(
61
+ * (ctx) => ctx.items.get('userId') as string | undefined,
62
+ * );
63
+ *
64
+ * class CreateUserHandler {
65
+ * @pipeline.wrap({ kind: 'command' }, [RateLimitBehavior, { points: 1, keyFactory: perUser }])
66
+ * async handle(command: CreateUserCommand) {}
67
+ * }
68
+ * ```
69
+ */
70
+ export class RateLimitBehavior {
71
+ limiter;
72
+ static [PIPELINE_BEHAVIOR_CONTRACT] = {
73
+ validate: (context) => {
74
+ const options = context.effectiveOptions;
75
+ if (!options?.keyFactory) {
76
+ return [
77
+ {
78
+ handlerName: context.handlerName,
79
+ behaviorName: RateLimitBehavior.name,
80
+ message: 'Active RateLimitBehavior requires an explicit `keyFactory`',
81
+ fix: 'Provide keyFactory in the [RateLimitBehavior, { keyFactory: ... }] entry, ' +
82
+ 'the constructor defaults, or use createPartitionedRateLimitKeyFactory(...).',
83
+ },
84
+ ];
85
+ }
86
+ if (typeof options.keyFactory !== 'function') {
87
+ return [
88
+ {
89
+ handlerName: context.handlerName,
90
+ behaviorName: RateLimitBehavior.name,
91
+ message: `RateLimitBehavior keyFactory must be a callable function, received ${typeof options.keyFactory}`,
92
+ fix: 'Pass a valid function (ctx) => string to the keyFactory option of the [RateLimitBehavior, { keyFactory: ... }] entry.',
93
+ },
94
+ ];
95
+ }
96
+ const { points } = options;
97
+ if (points !== undefined &&
98
+ typeof points !== 'function' &&
99
+ !isValidPoints(points)) {
100
+ return [
101
+ {
102
+ handlerName: context.handlerName,
103
+ behaviorName: RateLimitBehavior.name,
104
+ message: `RateLimitBehavior points must be a non-negative safe integer or a function, received ${String(points)}`,
105
+ fix: 'Pass an integer from 0 up (0 charges nothing) or a function (ctx) => number as the points option.',
106
+ },
107
+ ];
108
+ }
109
+ return undefined;
110
+ },
111
+ };
112
+ logger;
113
+ defaults;
114
+ constructor(limiter, defaults, logger) {
115
+ this.limiter = limiter;
116
+ if (typeof limiter?.consume !== 'function') {
117
+ throw new TypeError('RateLimitBehavior requires a rate limiter with a consume() method, such as RateLimiterMemory of rate-limiter-flexible.');
118
+ }
119
+ this.defaults = defaults ?? {};
120
+ this.logger = logger ?? console;
121
+ }
122
+ async handle(context, next) {
123
+ const options = this.resolveEffectiveOptions(context.getBehaviorOptions(RateLimitBehavior));
124
+ const points = resolvePoints(context, options);
125
+ if (points === 0)
126
+ return next();
127
+ const limiter = options.limiter ?? this.limiter;
128
+ const key = buildRateLimitKey(context, options);
129
+ setPipelineItem(context, RATE_LIMIT_KEY_ITEM_TOKEN, key);
130
+ let result;
131
+ try {
132
+ result = await limiter.consume(key, points);
133
+ }
134
+ catch (error) {
135
+ if (isRateLimiterRes(error)) {
136
+ setPipelineItem(context, RATE_LIMIT_ITEM_TOKEN, error);
137
+ throw new RateLimitExceededError({
138
+ key,
139
+ requestName: context.requestName,
140
+ msBeforeNext: error.msBeforeNext,
141
+ remainingPoints: error.remainingPoints,
142
+ limit: limiter.points,
143
+ points,
144
+ });
145
+ }
146
+ return this.handleStoreError(context, options, key, error, next);
147
+ }
148
+ setPipelineItem(context, RATE_LIMIT_ITEM_TOKEN, result);
149
+ return next();
150
+ }
151
+ /** Fail-open (default) or fail-closed when the backing store itself errors. */
152
+ handleStoreError(context, options, key, error, next) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ if (options.failOpen ?? true) {
155
+ this.logger.warn?.(`Rate limiter store error for ${context.requestName} ` +
156
+ `(key: ${key}); failing open: ${message}`, RateLimitBehavior.name);
157
+ return next();
158
+ }
159
+ this.logger.error?.(`Rate limiter store error for ${context.requestName} ` +
160
+ `(key: ${key}); failing closed: ${message}`, RateLimitBehavior.name);
161
+ throw error;
162
+ }
163
+ /** Shallow-merges pipeline-level options over the constructor defaults. */
164
+ resolveEffectiveOptions(options) {
165
+ if (!options)
166
+ return this.defaults;
167
+ return { ...this.defaults, ...options };
168
+ }
169
+ }
170
+ function isValidPoints(points) {
171
+ return Number.isSafeInteger(points) && points >= 0;
172
+ }
173
+ /**
174
+ * Resolves the request's cost: the `points` option, computed when it is a
175
+ * function, `1` when omitted.
176
+ *
177
+ * @throws {TypeError} When the cost is not a non-negative safe integer.
178
+ */
179
+ function resolvePoints(context, options) {
180
+ const points = typeof options.points === 'function'
181
+ ? options.points(context)
182
+ : (options.points ?? 1);
183
+ if (!isValidPoints(points)) {
184
+ throw new TypeError(`Rate-limit points for ${context.handlerName} must be a non-negative safe integer, received ${String(points)}.`);
185
+ }
186
+ return points;
187
+ }
188
+ //# sourceMappingURL=rate-limit.behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.behavior.js","sourceRoot":"","sources":["../src/rate-limit.behavior.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@cqrs-ddd/pipeline-rate-limit",
3
+ "version": "0.5.0",
4
+ "description": "Rate limiting for @cqrs-ddd/pipeline, with keys partitioned by tenant, principal and operation",
5
+ "author": "Aristotelis",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=22.12.0"
10
+ },
11
+ "keywords": [
12
+ "pipeline",
13
+ "cqrs",
14
+ "rate-limit",
15
+ "rate-limiter",
16
+ "throttle",
17
+ "rate-limiter-flexible",
18
+ "redis",
19
+ "ddos",
20
+ "behavior"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/aristoteliss/ddd-cqrs.git",
25
+ "directory": "packages/pipeline-rate-limit"
26
+ },
27
+ "homepage": "https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-rate-limit/",
28
+ "bugs": {
29
+ "url": "https://github.com/aristoteliss/ddd-cqrs/issues"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./http": {
37
+ "types": "./dist/http.d.ts",
38
+ "default": "./dist/http.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "main": "dist/index.js",
43
+ "types": "dist/index.d.ts",
44
+ "files": [
45
+ "dist",
46
+ "LICENSE",
47
+ "COMMERCIAL_LICENSE.txt"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "peerDependencies": {
53
+ "@cqrs-ddd/pipeline": "^0.5.0"
54
+ },
55
+ "devDependencies": {
56
+ "@vitest/coverage-v8": "^5.0.3",
57
+ "rate-limiter-flexible": "^11.2.1",
58
+ "vitest": "^5.0.3",
59
+ "@cqrs-ddd/pipeline": "0.5.0"
60
+ },
61
+ "dependencies": {
62
+ "@cqrs-ddd/safe-stringify": "^0.5.0"
63
+ },
64
+ "scripts": {
65
+ "build": "tsc -p tsconfig.build.json",
66
+ "build:watch": "tsc -p tsconfig.build.json --watch",
67
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
68
+ "rebuild": "pnpm run clean && pnpm run build",
69
+ "lint": "tsc --noEmit",
70
+ "test": "vitest run",
71
+ "test:watch": "vitest"
72
+ }
73
+ }