@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.
- package/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +36 -0
- package/dist/errors/missing-partition.error.d.ts +13 -0
- package/dist/errors/missing-partition.error.d.ts.map +1 -0
- package/dist/errors/missing-partition.error.js +14 -0
- package/dist/errors/missing-partition.error.js.map +1 -0
- package/dist/errors/rate-limit-exceeded.error.d.ts +35 -0
- package/dist/errors/rate-limit-exceeded.error.d.ts.map +1 -0
- package/dist/errors/rate-limit-exceeded.error.js +41 -0
- package/dist/errors/rate-limit-exceeded.error.js.map +1 -0
- package/dist/helpers/build-attributes.d.ts +47 -0
- package/dist/helpers/build-attributes.d.ts.map +1 -0
- package/dist/helpers/build-attributes.js +54 -0
- package/dist/helpers/build-attributes.js.map +1 -0
- package/dist/helpers/build-key.d.ts +23 -0
- package/dist/helpers/build-key.d.ts.map +1 -0
- package/dist/helpers/build-key.js +38 -0
- package/dist/helpers/build-key.js.map +1 -0
- package/dist/helpers/partitioned-key.d.ts +65 -0
- package/dist/helpers/partitioned-key.d.ts.map +1 -0
- package/dist/helpers/partitioned-key.js +61 -0
- package/dist/helpers/partitioned-key.js.map +1 -0
- package/dist/helpers/rate-limit.intent.d.ts +30 -0
- package/dist/helpers/rate-limit.intent.d.ts.map +1 -0
- package/dist/helpers/rate-limit.intent.js +25 -0
- package/dist/helpers/rate-limit.intent.js.map +1 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +27 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/rate-limit-options.interface.d.ts +64 -0
- package/dist/interfaces/rate-limit-options.interface.d.ts.map +1 -0
- package/dist/interfaces/rate-limit-options.interface.js +3 -0
- package/dist/interfaces/rate-limit-options.interface.js.map +1 -0
- package/dist/interfaces/rate-limiter.interface.d.ts +35 -0
- package/dist/interfaces/rate-limiter.interface.d.ts.map +1 -0
- package/dist/interfaces/rate-limiter.interface.js +3 -0
- package/dist/interfaces/rate-limiter.interface.js.map +1 -0
- package/dist/rate-limit.behavior.d.ts +74 -0
- package/dist/rate-limit.behavior.d.ts.map +1 -0
- package/dist/rate-limit.behavior.js +188 -0
- package/dist/rate-limit.behavior.js.map +1 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @cqrs-ddd/pipeline-rate-limit
|
|
2
|
+
|
|
3
|
+
Rate limiting for `@cqrs-ddd/pipeline`, on any limiter with a `consume()` method such as
|
|
4
|
+
those of [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible)
|
|
5
|
+
(memory, Redis, PostgreSQL). `createPartitionedRateLimitKeyFactory()` builds keys
|
|
6
|
+
partitioned by tenant, principal and operation; `toHttpResponse()` from
|
|
7
|
+
`@cqrs-ddd/pipeline-rate-limit/http` turns a refusal into a 429 answer with `Retry-After`.
|
|
8
|
+
|
|
9
|
+
**Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-rate-limit/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-rate-limit/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @cqrs-ddd/pipeline-rate-limit @cqrs-ddd/pipeline rate-limiter-flexible
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
|
|
18
|
+
it with `require()`.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const pipeline = createPipeline({
|
|
24
|
+
behaviors: [new RateLimitBehavior(new RateLimiterMemory({ points: 10, duration: 60 }))],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const search = pipeline.wrap(
|
|
28
|
+
{ name: 'search', kind: 'query' },
|
|
29
|
+
rateLimit({ keyFactory: createPartitionedRateLimitKeyFactory(currentUserId) }),
|
|
30
|
+
)(async (term: string) => catalog.search(term));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
|
|
36
|
+
`COMMERCIAL_LICENSE.txt`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MissingPartitionError } from '@cqrs-ddd/pipeline';
|
|
2
|
+
/** Which dimension of the bucket key could not be resolved. */
|
|
3
|
+
export type RateLimitPartitionDimension = 'tenant' | 'caller';
|
|
4
|
+
/**
|
|
5
|
+
* Raised before the limiter is consumed when a dimension required by the
|
|
6
|
+
* configured key factory cannot be resolved; omitting the segment instead would
|
|
7
|
+
* put every unresolved caller into one shared bucket.
|
|
8
|
+
*/
|
|
9
|
+
export declare class MissingRateLimitPartitionError extends MissingPartitionError<RateLimitPartitionDimension> {
|
|
10
|
+
readonly name = "MissingRateLimitPartitionError";
|
|
11
|
+
constructor(requestName: string, dimension: RateLimitPartitionDimension, remedy: string);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=missing-partition.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-partition.error.d.ts","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,+DAA+D;AAC/D,MAAM,MAAM,2BAA2B,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9D;;;;GAIG;AACH,qBAAa,8BAA+B,SAAQ,qBAAqB,CAAC,2BAA2B,CAAC;IACpG,SAAkB,IAAI,oCAAoC;IAE1D,YACE,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,2BAA2B,EACtC,MAAM,EAAE,MAAM,EAGf;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { MissingPartitionError } from '@cqrs-ddd/pipeline';
|
|
3
|
+
/**
|
|
4
|
+
* Raised before the limiter is consumed when a dimension required by the
|
|
5
|
+
* configured key factory cannot be resolved; omitting the segment instead would
|
|
6
|
+
* put every unresolved caller into one shared bucket.
|
|
7
|
+
*/
|
|
8
|
+
export class MissingRateLimitPartitionError extends MissingPartitionError {
|
|
9
|
+
name = 'MissingRateLimitPartitionError';
|
|
10
|
+
constructor(requestName, dimension, remedy) {
|
|
11
|
+
super('Rate-limit', requestName, dimension, remedy);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=missing-partition.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-partition.error.js","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAK3D;;;;GAIG;AACH,MAAM,OAAO,8BAA+B,SAAQ,qBAAkD;IAClF,IAAI,GAAG,gCAAgC,CAAC;IAE1D,YACE,WAAmB,EACnB,SAAsC,EACtC,MAAc;QAEd,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link RateLimitBehavior} when a request exceeds its allotted
|
|
3
|
+
* points for the current window.
|
|
4
|
+
*
|
|
5
|
+
* Map it to HTTP `429 Too Many Requests` with {@link RateLimitExceededFilter},
|
|
6
|
+
* which also emits a `Retry-After` header from {@link retryAfterSeconds}.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RateLimitExceededError extends Error {
|
|
9
|
+
/** The rate-limit bucket key that was exhausted. */
|
|
10
|
+
readonly key: string;
|
|
11
|
+
/** The request that was throttled, e.g. `CreateUserCommand`. */
|
|
12
|
+
readonly requestName: string;
|
|
13
|
+
/** Milliseconds until the caller may retry. */
|
|
14
|
+
readonly msBeforeNext: number;
|
|
15
|
+
/** Seconds until the caller may retry (rounded up), for `Retry-After`. */
|
|
16
|
+
readonly retryAfterSeconds: number;
|
|
17
|
+
/** Points remaining in the window (typically `0` here). */
|
|
18
|
+
readonly remainingPoints: number;
|
|
19
|
+
/** Configured point capacity per window, when known. */
|
|
20
|
+
readonly limit?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Points the rejected request asked for, when known. A request costing more
|
|
23
|
+
* than {@link limit} can never pass, whatever the wait.
|
|
24
|
+
*/
|
|
25
|
+
readonly points?: number;
|
|
26
|
+
constructor(params: {
|
|
27
|
+
key: string;
|
|
28
|
+
requestName: string;
|
|
29
|
+
msBeforeNext: number;
|
|
30
|
+
remainingPoints: number;
|
|
31
|
+
limit?: number;
|
|
32
|
+
points?: number;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=rate-limit-exceeded.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-exceeded.error.d.ts","sourceRoot":"","sources":["../../src/errors/rate-limit-exceeded.error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,YAAY,MAAM,EAAE;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,EAiBA;CACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link RateLimitBehavior} when a request exceeds its allotted
|
|
4
|
+
* points for the current window.
|
|
5
|
+
*
|
|
6
|
+
* Map it to HTTP `429 Too Many Requests` with {@link RateLimitExceededFilter},
|
|
7
|
+
* which also emits a `Retry-After` header from {@link retryAfterSeconds}.
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimitExceededError extends Error {
|
|
10
|
+
/** The rate-limit bucket key that was exhausted. */
|
|
11
|
+
key;
|
|
12
|
+
/** The request that was throttled, e.g. `CreateUserCommand`. */
|
|
13
|
+
requestName;
|
|
14
|
+
/** Milliseconds until the caller may retry. */
|
|
15
|
+
msBeforeNext;
|
|
16
|
+
/** Seconds until the caller may retry (rounded up), for `Retry-After`. */
|
|
17
|
+
retryAfterSeconds;
|
|
18
|
+
/** Points remaining in the window (typically `0` here). */
|
|
19
|
+
remainingPoints;
|
|
20
|
+
/** Configured point capacity per window, when known. */
|
|
21
|
+
limit;
|
|
22
|
+
/**
|
|
23
|
+
* Points the rejected request asked for, when known. A request costing more
|
|
24
|
+
* than {@link limit} can never pass, whatever the wait.
|
|
25
|
+
*/
|
|
26
|
+
points;
|
|
27
|
+
constructor(params) {
|
|
28
|
+
const retryAfterSeconds = Math.max(1, Math.ceil(params.msBeforeNext / 1000));
|
|
29
|
+
super(`Rate limit exceeded for ${params.requestName} ` +
|
|
30
|
+
`(key: ${params.key}); retry after ${retryAfterSeconds}s`);
|
|
31
|
+
this.name = 'RateLimitExceededError';
|
|
32
|
+
this.key = params.key;
|
|
33
|
+
this.requestName = params.requestName;
|
|
34
|
+
this.msBeforeNext = params.msBeforeNext;
|
|
35
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
36
|
+
this.remainingPoints = params.remainingPoints;
|
|
37
|
+
this.limit = params.limit;
|
|
38
|
+
this.points = params.points;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=rate-limit-exceeded.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-exceeded.error.js","sourceRoot":"","sources":["../../src/errors/rate-limit-exceeded.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,oDAAoD;IAC3C,GAAG,CAAS;IACrB,gEAAgE;IACvD,WAAW,CAAS;IAC7B,+CAA+C;IACtC,YAAY,CAAS;IAC9B,0EAA0E;IACjE,iBAAiB,CAAS;IACnC,2DAA2D;IAClD,eAAe,CAAS;IACjC,wDAAwD;IAC/C,KAAK,CAAU;IACxB;;;OAGG;IACM,MAAM,CAAU;IAEzB,YAAY,MAOX;QACC,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAChC,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CACtC,CAAC;QACF,KAAK,CACH,2BAA2B,MAAM,CAAC,WAAW,GAAG;YAC9C,SAAS,MAAM,CAAC,GAAG,kBAAkB,iBAAiB,GAAG,CAC5D,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;QACxC,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,CAAC;QAC9C,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;CACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
/**
|
|
3
|
+
* The rate-limit decision of one execution as flat attributes:
|
|
4
|
+
* `rate_limit.remaining_points` once `RateLimitBehavior` consumed points and the
|
|
5
|
+
* limiter reported what remains, `{}` otherwise. The key is never included: it
|
|
6
|
+
* carries the tenant and the caller.
|
|
7
|
+
*
|
|
8
|
+
* The result is plain data and needs no telemetry package: put it on a span, a
|
|
9
|
+
* log line, an audit record or a metric label (bounded values only). The remaining points take as many values as the limiter has
|
|
10
|
+
* points; keep them out of metric labels unless that is bounded enough.
|
|
11
|
+
*
|
|
12
|
+
* @param context - The pipeline execution, read after `RateLimitBehavior` ran.
|
|
13
|
+
* @returns The attributes; empty when the behavior did not run.
|
|
14
|
+
*
|
|
15
|
+
* @example Span attributes
|
|
16
|
+
* With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
|
|
17
|
+
* ```ts
|
|
18
|
+
* createPipeline({
|
|
19
|
+
* globalBehaviors: {
|
|
20
|
+
* before: [TraceBehavior, [AttributesBehavior, { factories: [buildRateLimitAttributes] }]],
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Audit metadata
|
|
26
|
+
* `audit()` is declared outside `rateLimit()`, so the record is built after its decision:
|
|
27
|
+
* ```ts
|
|
28
|
+
* @pipeline.wrap(
|
|
29
|
+
* { kind: 'command' },
|
|
30
|
+
* audit({ action: 'order.create', metadata: buildRateLimitAttributes }),
|
|
31
|
+
* rateLimit({ keyFactory, points: 1 }),
|
|
32
|
+
* )
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example A log line from a custom behavior, after the chain ran
|
|
36
|
+
* ```ts
|
|
37
|
+
* async handle(context: IPipelineContext, next: NextDelegate) {
|
|
38
|
+
* try {
|
|
39
|
+
* return await next();
|
|
40
|
+
* } finally {
|
|
41
|
+
* this.logger.log({ request: context.requestName, ...buildRateLimitAttributes(context) });
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildRateLimitAttributes(context: IPipelineContext): Record<string, number>;
|
|
47
|
+
//# sourceMappingURL=build-attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKxB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { getPipelineItem } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { RATE_LIMIT_ITEM_TOKEN } from '../rate-limit.behavior.js';
|
|
4
|
+
/**
|
|
5
|
+
* The rate-limit decision of one execution as flat attributes:
|
|
6
|
+
* `rate_limit.remaining_points` once `RateLimitBehavior` consumed points and the
|
|
7
|
+
* limiter reported what remains, `{}` otherwise. The key is never included: it
|
|
8
|
+
* carries the tenant and the caller.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain data and needs no telemetry package: put it on a span, a
|
|
11
|
+
* log line, an audit record or a metric label (bounded values only). The remaining points take as many values as the limiter has
|
|
12
|
+
* points; keep them out of metric labels unless that is bounded enough.
|
|
13
|
+
*
|
|
14
|
+
* @param context - The pipeline execution, read after `RateLimitBehavior` ran.
|
|
15
|
+
* @returns The attributes; empty when the behavior did not run.
|
|
16
|
+
*
|
|
17
|
+
* @example Span attributes
|
|
18
|
+
* With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
|
|
19
|
+
* ```ts
|
|
20
|
+
* createPipeline({
|
|
21
|
+
* globalBehaviors: {
|
|
22
|
+
* before: [TraceBehavior, [AttributesBehavior, { factories: [buildRateLimitAttributes] }]],
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example Audit metadata
|
|
28
|
+
* `audit()` is declared outside `rateLimit()`, so the record is built after its decision:
|
|
29
|
+
* ```ts
|
|
30
|
+
* @pipeline.wrap(
|
|
31
|
+
* { kind: 'command' },
|
|
32
|
+
* audit({ action: 'order.create', metadata: buildRateLimitAttributes }),
|
|
33
|
+
* rateLimit({ keyFactory, points: 1 }),
|
|
34
|
+
* )
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example A log line from a custom behavior, after the chain ran
|
|
38
|
+
* ```ts
|
|
39
|
+
* async handle(context: IPipelineContext, next: NextDelegate) {
|
|
40
|
+
* try {
|
|
41
|
+
* return await next();
|
|
42
|
+
* } finally {
|
|
43
|
+
* this.logger.log({ request: context.requestName, ...buildRateLimitAttributes(context) });
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function buildRateLimitAttributes(context) {
|
|
49
|
+
const result = getPipelineItem(context, RATE_LIMIT_ITEM_TOKEN);
|
|
50
|
+
return typeof result?.remainingPoints === 'number'
|
|
51
|
+
? { 'rate_limit.remaining_points': result.remainingPoints }
|
|
52
|
+
: {};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=build-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-attributes.js","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,eAAe,EAAyB,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAyB;IAEzB,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IAC/D,OAAO,OAAO,MAAM,EAAE,eAAe,KAAK,QAAQ;QAChD,CAAC,CAAC,EAAE,6BAA6B,EAAE,MAAM,CAAC,eAAe,EAAE;QAC3D,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { RateLimitBehaviorOptions } from '../interfaces/rate-limit-options.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the rate-limit bucket key for a request.
|
|
5
|
+
*
|
|
6
|
+
* A `keyFactory` is required; a request-name-only bucket is shared by every
|
|
7
|
+
* caller in every tenant.
|
|
8
|
+
*
|
|
9
|
+
* Callers that genuinely want an application-wide operation bucket say so:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* { keyFactory: (ctx) => ctx.requestName }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* `options.keyPrefix` is prepended as `"<prefix>:<key>"` when provided, so
|
|
16
|
+
* environment and service prefixes stay orthogonal to the partitioning choice.
|
|
17
|
+
*
|
|
18
|
+
* @param context - The pipeline context of the current request.
|
|
19
|
+
* @param options - Effective behavior options.
|
|
20
|
+
* @throws {TypeError} When no `keyFactory` is configured.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildRateLimitKey(context: IPipelineContext, options?: RateLimitBehaviorOptions): string;
|
|
23
|
+
//# sourceMappingURL=build-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-key.d.ts","sourceRoot":"","sources":["../../src/helpers/build-key.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAE9F;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,wBAA6B,GACrC,MAAM,CAkBR"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { escapeKeySegment } from '@cqrs-ddd/safe-stringify';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the rate-limit bucket key for a request.
|
|
5
|
+
*
|
|
6
|
+
* A `keyFactory` is required; a request-name-only bucket is shared by every
|
|
7
|
+
* caller in every tenant.
|
|
8
|
+
*
|
|
9
|
+
* Callers that genuinely want an application-wide operation bucket say so:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* { keyFactory: (ctx) => ctx.requestName }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* `options.keyPrefix` is prepended as `"<prefix>:<key>"` when provided, so
|
|
16
|
+
* environment and service prefixes stay orthogonal to the partitioning choice.
|
|
17
|
+
*
|
|
18
|
+
* @param context - The pipeline context of the current request.
|
|
19
|
+
* @param options - Effective behavior options.
|
|
20
|
+
* @throws {TypeError} When no `keyFactory` is configured.
|
|
21
|
+
*/
|
|
22
|
+
export function buildRateLimitKey(context, options = {}) {
|
|
23
|
+
if (!options.keyFactory) {
|
|
24
|
+
throw new TypeError(`RateLimitBehavior on ${context.handlerName} requires an explicit keyFactory. ` +
|
|
25
|
+
'A request-name-only bucket is shared by every caller in every tenant, so a ' +
|
|
26
|
+
'single client can exhaust it for all of them. Use ' +
|
|
27
|
+
'createPartitionedRateLimitKeyFactory(...) for per-caller limits, or pass ' +
|
|
28
|
+
'keyFactory: (ctx) => ctx.requestName to accept a global bucket deliberately.');
|
|
29
|
+
}
|
|
30
|
+
const base = options.keyFactory(context);
|
|
31
|
+
// Only the prefix is escaped. `base` is already a complete key — a partitioned
|
|
32
|
+
// factory has escaped its own segments — so re-escaping it here would mangle
|
|
33
|
+
// the documented "<prefix>:<key>" shape.
|
|
34
|
+
return options.keyPrefix
|
|
35
|
+
? `${escapeKeySegment(options.keyPrefix)}:${base}`
|
|
36
|
+
: base;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=build-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-key.js","sourceRoot":"","sources":["../../src/helpers/build-key.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG5D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAyB,EACzB,OAAO,GAA6B,EAAE;IAEtC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,SAAS,CACjB,wBAAwB,OAAO,CAAC,WAAW,oCAAoC;YAC7E,6EAA6E;YAC7E,oDAAoD;YACpD,2EAA2E;YAC3E,8EAA8E,CACjF,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACzC,+EAA+E;IAC/E,6EAA6E;IAC7E,yCAAyC;IACzC,OAAO,OAAO,CAAC,SAAS;QACtB,CAAC,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE;QAClD,CAAC,CAAC,IAAI,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type IPipelineContext, type TenantPartitionOptions } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { RateLimitKeyFactory } from '../interfaces/rate-limit-options.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the stable caller/partition identifier used by
|
|
5
|
+
* {@link createPartitionedRateLimitKeyFactory}.
|
|
6
|
+
*
|
|
7
|
+
* Typical values are an authenticated user ID, account ID, API-client ID, or a
|
|
8
|
+
* trusted transport identity copied into `context.items` by an earlier behavior.
|
|
9
|
+
*/
|
|
10
|
+
export type RateLimitPartitionFactory = (context: IPipelineContext) => string | undefined;
|
|
11
|
+
/** Options for {@link createPartitionedRateLimitKeyFactory}. */
|
|
12
|
+
export interface PartitionedRateLimitKeyOptions extends TenantPartitionOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Behavior when the partition factory cannot resolve a non-empty identity.
|
|
15
|
+
*
|
|
16
|
+
* - `throw` — fail before consuming the limiter (recommended for per-caller
|
|
17
|
+
* security limits because silently falling back could merge callers).
|
|
18
|
+
* - `request` — fall back to a bucket shared by every caller of this request
|
|
19
|
+
* within the same tenant partition.
|
|
20
|
+
*
|
|
21
|
+
* @default 'throw'
|
|
22
|
+
*/
|
|
23
|
+
onMissingPartition?: 'throw' | 'request';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a {@link RateLimitKeyFactory} for per-caller limits.
|
|
27
|
+
*
|
|
28
|
+
* Segments are escaped and joined through the core key helper so tenant,
|
|
29
|
+
* caller/account identity, and request name remain distinct even when values
|
|
30
|
+
* contain separator characters.
|
|
31
|
+
*
|
|
32
|
+
* @param partitionFactory - Resolves the stable caller/account identity.
|
|
33
|
+
* @param options - Tenant and missing-partition policy.
|
|
34
|
+
* @returns A `RateLimitKeyFactory` suitable for `RateLimitBehaviorOptions.keyFactory`.
|
|
35
|
+
* @throws {MissingRateLimitPartitionError} When a required tenant or caller partition is absent.
|
|
36
|
+
*
|
|
37
|
+
* The produced key is `<tenantId>:<partition>:<requestName>` when tenant
|
|
38
|
+
* inclusion is enabled, `<partition>:<requestName>` otherwise.
|
|
39
|
+
* `RateLimitBehaviorOptions.keyPrefix` is still applied afterwards by
|
|
40
|
+
* `buildRateLimitKey()`, so environment/service prefixes remain orthogonal.
|
|
41
|
+
*
|
|
42
|
+
* @example Per authenticated user, tenant-aware and fail-closed by default
|
|
43
|
+
* ```ts
|
|
44
|
+
* const perUserKey = createPartitionedRateLimitKeyFactory((ctx) =>
|
|
45
|
+
* ctx.items.get('currentUserId') as string | undefined,
|
|
46
|
+
* );
|
|
47
|
+
*
|
|
48
|
+
* class CreateOrderHandler {
|
|
49
|
+
* @pipeline.wrap({ kind: 'command' }, [RateLimitBehavior, { keyFactory: perUserKey, keyPrefix: 'write-api' }])
|
|
50
|
+
* async handle(command: CreateOrderCommand) {}
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example Single-tenant deployment — say so explicitly
|
|
55
|
+
* ```ts
|
|
56
|
+
* createPartitionedRateLimitKeyFactory(readUserId, { includeTenant: false });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example Account-wide bucket shared by every user in the account
|
|
60
|
+
* ```ts
|
|
61
|
+
* createPartitionedRateLimitKeyFactory(readAccountId, { includeTenant: true });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function createPartitionedRateLimitKeyFactory(partitionFactory: RateLimitPartitionFactory, options?: PartitionedRateLimitKeyOptions): RateLimitKeyFactory;
|
|
65
|
+
//# sourceMappingURL=partitioned-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"partitioned-key.d.ts","sourceRoot":"","sources":["../../src/helpers/partitioned-key.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAEzF;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,gBAAgB,KACtB,MAAM,GAAG,SAAS,CAAC;AAExB,gEAAgE;AAChE,MAAM,WAAW,8BAA+B,SAAQ,sBAAsB;IAC5E;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,oCAAoC,CAClD,gBAAgB,EAAE,yBAAyB,EAC3C,OAAO,GAAE,8BAAmC,GAC3C,mBAAmB,CA4BrB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { tenantSegments, } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { joinKeySegments } from '@cqrs-ddd/safe-stringify';
|
|
4
|
+
import { MissingRateLimitPartitionError } from '../errors/missing-partition.error.js';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a {@link RateLimitKeyFactory} for per-caller limits.
|
|
7
|
+
*
|
|
8
|
+
* Segments are escaped and joined through the core key helper so tenant,
|
|
9
|
+
* caller/account identity, and request name remain distinct even when values
|
|
10
|
+
* contain separator characters.
|
|
11
|
+
*
|
|
12
|
+
* @param partitionFactory - Resolves the stable caller/account identity.
|
|
13
|
+
* @param options - Tenant and missing-partition policy.
|
|
14
|
+
* @returns A `RateLimitKeyFactory` suitable for `RateLimitBehaviorOptions.keyFactory`.
|
|
15
|
+
* @throws {MissingRateLimitPartitionError} When a required tenant or caller partition is absent.
|
|
16
|
+
*
|
|
17
|
+
* The produced key is `<tenantId>:<partition>:<requestName>` when tenant
|
|
18
|
+
* inclusion is enabled, `<partition>:<requestName>` otherwise.
|
|
19
|
+
* `RateLimitBehaviorOptions.keyPrefix` is still applied afterwards by
|
|
20
|
+
* `buildRateLimitKey()`, so environment/service prefixes remain orthogonal.
|
|
21
|
+
*
|
|
22
|
+
* @example Per authenticated user, tenant-aware and fail-closed by default
|
|
23
|
+
* ```ts
|
|
24
|
+
* const perUserKey = createPartitionedRateLimitKeyFactory((ctx) =>
|
|
25
|
+
* ctx.items.get('currentUserId') as string | undefined,
|
|
26
|
+
* );
|
|
27
|
+
*
|
|
28
|
+
* class CreateOrderHandler {
|
|
29
|
+
* @pipeline.wrap({ kind: 'command' }, [RateLimitBehavior, { keyFactory: perUserKey, keyPrefix: 'write-api' }])
|
|
30
|
+
* async handle(command: CreateOrderCommand) {}
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Single-tenant deployment — say so explicitly
|
|
35
|
+
* ```ts
|
|
36
|
+
* createPartitionedRateLimitKeyFactory(readUserId, { includeTenant: false });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example Account-wide bucket shared by every user in the account
|
|
40
|
+
* ```ts
|
|
41
|
+
* createPartitionedRateLimitKeyFactory(readAccountId, { includeTenant: true });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function createPartitionedRateLimitKeyFactory(partitionFactory, options = {}) {
|
|
45
|
+
const onMissingPartition = options.onMissingPartition ?? 'throw';
|
|
46
|
+
return (context) => {
|
|
47
|
+
const tenant = tenantSegments(context, options, MissingRateLimitPartitionError);
|
|
48
|
+
const resolved = partitionFactory(context);
|
|
49
|
+
const partition = typeof resolved === 'string' ? resolved.trim() : '';
|
|
50
|
+
if (!partition) {
|
|
51
|
+
if (onMissingPartition === 'throw') {
|
|
52
|
+
throw new MissingRateLimitPartitionError(context.requestName, 'caller', "Return a stable caller/account identifier, or configure onMissingPartition: 'request'.");
|
|
53
|
+
}
|
|
54
|
+
// Shared bucket, but still inside the tenant partition when one is used,
|
|
55
|
+
// so an anonymous caller in one tenant cannot exhaust another tenant's quota.
|
|
56
|
+
return joinKeySegments([...tenant, context.requestName]);
|
|
57
|
+
}
|
|
58
|
+
return joinKeySegments([...tenant, partition, context.requestName]);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=partitioned-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"partitioned-key.js","sourceRoot":"","sources":["../../src/helpers/partitioned-key.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAGL,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AA6BtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,oCAAoC,CAClD,gBAA2C,EAC3C,OAAO,GAAmC,EAAE;IAE5C,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,OAAO,CAAC;IAEjE,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,MAAM,GAAG,cAAc,CAC3B,OAAO,EACP,OAAO,EACP,8BAA8B,CAC/B,CAAC;QAEF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAEtE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,IAAI,kBAAkB,KAAK,OAAO,EAAE,CAAC;gBACnC,MAAM,IAAI,8BAA8B,CACtC,OAAO,CAAC,WAAW,EACnB,QAAQ,EACR,wFAAwF,CACzF,CAAC;YACJ,CAAC;YACD,yEAAyE;YACzE,8EAA8E;YAC9E,OAAO,eAAe,CAAC,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QAC3D,CAAC;QAED,OAAO,eAAe,CAAC,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PipelineBehaviorTuple } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { RateLimitBehaviorOptions } from '../interfaces/rate-limit-options.interface.js';
|
|
3
|
+
import { RateLimitBehavior } from '../rate-limit.behavior.js';
|
|
4
|
+
export type RateLimitIntentOptions = Omit<RateLimitBehaviorOptions, 'keyFactory'> & ({
|
|
5
|
+
keyFactory: NonNullable<RateLimitBehaviorOptions['keyFactory']>;
|
|
6
|
+
inheritModuleKey?: never;
|
|
7
|
+
} | {
|
|
8
|
+
inheritModuleKey: true;
|
|
9
|
+
keyFactory?: never;
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Returns a rate-limit behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
|
|
13
|
+
* @param options A key factory and rate-limit options, or `inheritModuleKey: true`
|
|
14
|
+
* when the behavior's constructor defaults supply the key factory. The inheritance marker is not
|
|
15
|
+
* forwarded to the behavior and does not validate module configuration.
|
|
16
|
+
* @returns The behavior class and options tuple.
|
|
17
|
+
* Use `createPartitionedRateLimitKeyFactory` for tenant/caller-specific limits.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* export const placeOrder = pipeline.wrap(
|
|
21
|
+
* { name: 'placeOrder', kind: 'command' },
|
|
22
|
+
* rateLimit({ keyFactory: perUserKey, points: 1 }),
|
|
23
|
+
* )(orders.place);
|
|
24
|
+
*
|
|
25
|
+
* // The key factory comes from the RateLimitBehavior constructor defaults.
|
|
26
|
+
* const entry = rateLimit({ inheritModuleKey: true, points: 2 });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function rateLimit(options: RateLimitIntentOptions): PipelineBehaviorTuple<RateLimitBehavior, RateLimitBehaviorOptions>;
|
|
30
|
+
//# sourceMappingURL=rate-limit.intent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.intent.d.ts","sourceRoot":"","sources":["../../src/helpers/rate-limit.intent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,wBAAwB,EACxB,YAAY,CACb,GACC,CACI;IACE,UAAU,EAAE,WAAW,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAC,CAAC;IAChE,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,GACD;IAAE,gBAAgB,EAAE,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,KAAK,CAAA;CAAE,CACjD,CAAC;AAEJ;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAAC,iBAAiB,EAAE,wBAAwB,CAAC,CAGpE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { RateLimitBehavior } from '../rate-limit.behavior.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a rate-limit behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
|
|
5
|
+
* @param options A key factory and rate-limit options, or `inheritModuleKey: true`
|
|
6
|
+
* when the behavior's constructor defaults supply the key factory. The inheritance marker is not
|
|
7
|
+
* forwarded to the behavior and does not validate module configuration.
|
|
8
|
+
* @returns The behavior class and options tuple.
|
|
9
|
+
* Use `createPartitionedRateLimitKeyFactory` for tenant/caller-specific limits.
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const placeOrder = pipeline.wrap(
|
|
13
|
+
* { name: 'placeOrder', kind: 'command' },
|
|
14
|
+
* rateLimit({ keyFactory: perUserKey, points: 1 }),
|
|
15
|
+
* )(orders.place);
|
|
16
|
+
*
|
|
17
|
+
* // The key factory comes from the RateLimitBehavior constructor defaults.
|
|
18
|
+
* const entry = rateLimit({ inheritModuleKey: true, points: 2 });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function rateLimit(options) {
|
|
22
|
+
const { inheritModuleKey: _inheritModuleKey, ...behaviorOptions } = options;
|
|
23
|
+
return [RateLimitBehavior, behaviorOptions];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=rate-limit.intent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.intent.js","sourceRoot":"","sources":["../../src/helpers/rate-limit.intent.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAItE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAc9D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS,CACvB,OAA+B;IAE/B,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IAC5E,OAAO,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RateLimitExceededError } from './errors/rate-limit-exceeded.error.js';
|
|
2
|
+
/** An HTTP answer for an error: status, JSON body and headers. */
|
|
3
|
+
export interface HttpResponse {
|
|
4
|
+
status: number;
|
|
5
|
+
body: Record<string, unknown>;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The HTTP answer for a {@link RateLimitExceededError}: `429 Too Many Requests` with a
|
|
10
|
+
* `Retry-After` header. Framework filters and Express handlers reply with it.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* app.use((error, _req, res, next) => {
|
|
15
|
+
* if (!(error instanceof RateLimitExceededError)) return next(error);
|
|
16
|
+
* const { status, body, headers } = toHttpResponse(error);
|
|
17
|
+
* res.status(status).set(headers).json(body);
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function toHttpResponse(error: RateLimitExceededError): HttpResponse;
|
|
22
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAE/E,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,sBAAsB,GAAG,YAAY,CAW1E"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* The HTTP answer for a {@link RateLimitExceededError}: `429 Too Many Requests` with a
|
|
4
|
+
* `Retry-After` header. Framework filters and Express handlers reply with it.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* app.use((error, _req, res, next) => {
|
|
9
|
+
* if (!(error instanceof RateLimitExceededError)) return next(error);
|
|
10
|
+
* const { status, body, headers } = toHttpResponse(error);
|
|
11
|
+
* res.status(status).set(headers).json(body);
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function toHttpResponse(error) {
|
|
16
|
+
return {
|
|
17
|
+
status: 429,
|
|
18
|
+
body: {
|
|
19
|
+
statusCode: 429,
|
|
20
|
+
error: 'Too Many Requests',
|
|
21
|
+
message: error.message,
|
|
22
|
+
retryAfter: error.retryAfterSeconds,
|
|
23
|
+
},
|
|
24
|
+
headers: { 'Retry-After': String(error.retryAfterSeconds) },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAWtE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,KAA6B;IAC1D,OAAO;QACL,MAAM,EAAE,GAAG;QACX,IAAI,EAAE;YACJ,UAAU,EAAE,GAAG;YACf,KAAK,EAAE,mBAAmB;YAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,UAAU,EAAE,KAAK,CAAC,iBAAiB;SACpC;QACD,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE;KAC5D,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limits for wrapped functions and methods: the behavior, key factories and `rateLimit()`. The HTTP mapping of its errors is the `@cqrs-ddd/pipeline-rate-limit/http` entry point.
|
|
3
|
+
*
|
|
4
|
+
* @module main
|
|
5
|
+
*/
|
|
6
|
+
export { MissingRateLimitPartitionError, type RateLimitPartitionDimension, } from './errors/missing-partition.error.js';
|
|
7
|
+
export { RateLimitExceededError } from './errors/rate-limit-exceeded.error.js';
|
|
8
|
+
export { buildRateLimitAttributes } from './helpers/build-attributes.js';
|
|
9
|
+
export { buildRateLimitKey } from './helpers/build-key.js';
|
|
10
|
+
export { createPartitionedRateLimitKeyFactory, type PartitionedRateLimitKeyOptions, type RateLimitPartitionFactory, } from './helpers/partitioned-key.js';
|
|
11
|
+
export { type RateLimitIntentOptions, rateLimit, } from './helpers/rate-limit.intent.js';
|
|
12
|
+
export type { RateLimitBehaviorOptions, RateLimitCostFactory, RateLimitKeyFactory, } from './interfaces/rate-limit-options.interface.js';
|
|
13
|
+
export type { RateLimiterLike, RateLimiterResLike, } from './interfaces/rate-limiter.interface.js';
|
|
14
|
+
export { RATE_LIMIT_ITEM, RATE_LIMIT_ITEM_TOKEN, RATE_LIMIT_KEY_ITEM, RATE_LIMIT_KEY_ITEM_TOKEN, RateLimitBehavior, } from './rate-limit.behavior.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EACL,8BAA8B,EAC9B,KAAK,2BAA2B,GACjC,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,EACpC,KAAK,8BAA8B,EACnC,KAAK,yBAAyB,GAC/B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,KAAK,sBAAsB,EAC3B,SAAS,GACV,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,8CAA8C,CAAC;AACtD,YAAY,EACV,eAAe,EACf,kBAAkB,GACnB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* Rate limits for wrapped functions and methods: the behavior, key factories and `rateLimit()`. The HTTP mapping of its errors is the `@cqrs-ddd/pipeline-rate-limit/http` entry point.
|
|
4
|
+
*
|
|
5
|
+
* @module main
|
|
6
|
+
*/
|
|
7
|
+
export { MissingRateLimitPartitionError, } from './errors/missing-partition.error.js';
|
|
8
|
+
export { RateLimitExceededError } from './errors/rate-limit-exceeded.error.js';
|
|
9
|
+
export { buildRateLimitAttributes } from './helpers/build-attributes.js';
|
|
10
|
+
export { buildRateLimitKey } from './helpers/build-key.js';
|
|
11
|
+
export { createPartitionedRateLimitKeyFactory, } from './helpers/partitioned-key.js';
|
|
12
|
+
export { rateLimit, } from './helpers/rate-limit.intent.js';
|
|
13
|
+
export { RATE_LIMIT_ITEM, RATE_LIMIT_ITEM_TOKEN, RATE_LIMIT_KEY_ITEM, RATE_LIMIT_KEY_ITEM_TOKEN, RateLimitBehavior, } from './rate-limit.behavior.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|