@ayepi/rate 0.2.0 → 0.4.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.md +13 -0
- package/ayepi-rate-stores-doer.md +139 -22
- package/ayepi-rate.md +63 -31
- package/dist/index.cjs +122 -25
- package/dist/index.d.cts +90 -8
- package/dist/index.d.ts +90 -8
- package/dist/index.js +121 -26
- package/dist/server.cjs +16 -4
- package/dist/server.d.cts +35 -10
- package/dist/server.d.ts +35 -10
- package/dist/server.js +17 -5
- package/package.json +3 -3
package/dist/server.d.ts
CHANGED
|
@@ -5,22 +5,47 @@ import { AnyMiddleware, BoundMiddleware, Json, StackCtx } from "@ayepi/core";
|
|
|
5
5
|
|
|
6
6
|
/** The `requires` chain of a middleware def. */
|
|
7
7
|
type ReqOf<M extends AnyMiddleware> = M['__req'];
|
|
8
|
+
/** The `io` (request + accumulated, typed context) passed to every per-request option of a bound {@link rateLimit}. */
|
|
9
|
+
type KeyIO<M extends AnyMiddleware> = RateKeyIO<StackCtx<ReqOf<M>>>;
|
|
10
|
+
/**
|
|
11
|
+
* A per-request option: derive `T` from the rate-limit key {@link RateKeyIO} — the same
|
|
12
|
+
* `(io) => T` shape `key` and `skip` use. `io.ctx` is typed from the def's `requires`.
|
|
13
|
+
*/
|
|
14
|
+
type RateKeyFn<M extends AnyMiddleware, T> = (io: KeyIO<M>) => T;
|
|
15
|
+
/**
|
|
16
|
+
* A {@link RateLimitServerOptions} field that is either a fixed `T` or resolved **per request**
|
|
17
|
+
* from the key {@link RateKeyIO}. Use a function to vary the policy by caller — e.g. a larger
|
|
18
|
+
* `limit` for a premium plan, a longer `window` for a trusted client.
|
|
19
|
+
*/
|
|
20
|
+
type Dynamic<M extends AnyMiddleware, T> = T | RateKeyFn<M, T>;
|
|
8
21
|
/**
|
|
9
22
|
* Server-side options for binding a {@link rateLimit} def — the limiting policy and
|
|
10
23
|
* response customization, with `key`/`skip`/`message` typed against the def's
|
|
11
24
|
* `requires` context.
|
|
12
25
|
*
|
|
26
|
+
* The rule itself — `limit`, `window`, `algorithm`, `countRejected` — can be a fixed value or
|
|
27
|
+
* a `(io) => T` resolved per request, so limits can differ by caller (plan, tenant, API key).
|
|
28
|
+
* **Resolve these from a stable property of the identity** (the same key should always map to
|
|
29
|
+
* the same `window`/`algorithm`): the store buckets its per-key state by them, so varying
|
|
30
|
+
* `window`/`algorithm` for one key across requests gives inconsistent accounting. Varying only
|
|
31
|
+
* `limit` is always safe. See {@link Dynamic}.
|
|
32
|
+
*
|
|
13
33
|
* @typeParam M - the rate-limit def being bound.
|
|
14
34
|
*/
|
|
15
35
|
interface RateLimitServerOptions<M extends AnyMiddleware> {
|
|
16
36
|
/** Derive the rate-limit key from the request context (e.g. `io.ctx.user.id`). */
|
|
17
|
-
readonly key:
|
|
18
|
-
/** Max requests (or token-bucket capacity) per window. */
|
|
19
|
-
readonly limit: number
|
|
20
|
-
/** Window length in milliseconds (also the token refill period). */
|
|
21
|
-
readonly window: number
|
|
22
|
-
/** Algorithm (default `'fixed-window'`). */
|
|
23
|
-
readonly algorithm?: Algorithm
|
|
37
|
+
readonly key: RateKeyFn<M, string>;
|
|
38
|
+
/** Max requests (or token-bucket capacity) per window — fixed, or per request. */
|
|
39
|
+
readonly limit: Dynamic<M, number>;
|
|
40
|
+
/** Window length in milliseconds (also the token refill period) — fixed, or per request. */
|
|
41
|
+
readonly window: Dynamic<M, number>;
|
|
42
|
+
/** Algorithm (default `'fixed-window'`) — fixed, or per request. */
|
|
43
|
+
readonly algorithm?: Dynamic<M, Algorithm>;
|
|
44
|
+
/**
|
|
45
|
+
* Count requests that are themselves rejected (over-limit) against the limit — fixed, or per
|
|
46
|
+
* request. Default `false`; see {@link RateLimitRule.countRejected}.
|
|
47
|
+
*/
|
|
48
|
+
readonly countRejected?: Dynamic<M, boolean>;
|
|
24
49
|
/** Backend store (default an in-process memory store). */
|
|
25
50
|
readonly store?: RateLimitStore;
|
|
26
51
|
/** Key prefix/namespace (default `'rl:'`). */
|
|
@@ -28,7 +53,7 @@ interface RateLimitServerOptions<M extends AnyMiddleware> {
|
|
|
28
53
|
/** Over-limit status code (default `429`). */
|
|
29
54
|
readonly status?: number;
|
|
30
55
|
/** Over-limit body — a string, a JSON value, or a function of the limiter info and request. */
|
|
31
|
-
readonly message?: string | Json | ((info: RateLimitInfo, io:
|
|
56
|
+
readonly message?: string | Json | ((info: RateLimitInfo, io: KeyIO<M>) => string | Json);
|
|
32
57
|
/** Response headers (draft `RateLimit-*` + `Retry-After` by default; `false` for none; a function for your own). */
|
|
33
58
|
readonly headers?: boolean | ((info: RateLimitInfo) => Record<string, string>);
|
|
34
59
|
/**
|
|
@@ -39,7 +64,7 @@ interface RateLimitServerOptions<M extends AnyMiddleware> {
|
|
|
39
64
|
*/
|
|
40
65
|
readonly alwaysHeaders?: boolean;
|
|
41
66
|
/** Bypass the limiter for some requests (e.g. an allow-list). */
|
|
42
|
-
readonly skip?:
|
|
67
|
+
readonly skip?: RateKeyFn<M, boolean>;
|
|
43
68
|
/**
|
|
44
69
|
* What to do when the **store itself errors** (e.g. Redis is down) — `true` serves the
|
|
45
70
|
* request through (as if allowed, full budget); `false` (default) is fail-**closed**: the
|
|
@@ -64,4 +89,4 @@ declare const rateLimit: typeof rateLimit$1 & {
|
|
|
64
89
|
server: typeof rateLimitServer;
|
|
65
90
|
};
|
|
66
91
|
//#endregion
|
|
67
|
-
export { RateLimitServerOptions, rateLimit };
|
|
92
|
+
export { Dynamic, RateKeyFn, RateLimitServerOptions, rateLimit };
|
package/dist/server.js
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { memoryStore, rateLimit as rateLimit$1, rateLimitHeaders, rateLimitResponse } from "./index.js";
|
|
2
2
|
//#region src/server.ts
|
|
3
|
+
/** Resolve a {@link Dynamic} option for a request (calls it with `io` when it's a function). */
|
|
4
|
+
function resolve(value, io) {
|
|
5
|
+
return typeof value === "function" ? value(io) : value;
|
|
6
|
+
}
|
|
3
7
|
/** Bind a {@link rateLimit} def to its runtime policy. */
|
|
4
8
|
function rateLimitServer(def, opts) {
|
|
5
|
-
const
|
|
9
|
+
const store = opts.store ?? memoryStore();
|
|
10
|
+
const prefix = opts.prefix ?? "rl:";
|
|
6
11
|
const message = opts.message;
|
|
12
|
+
const ruleFor = (io) => ({
|
|
13
|
+
limit: resolve(opts.limit, io),
|
|
14
|
+
window: resolve(opts.window, io),
|
|
15
|
+
algorithm: (opts.algorithm !== void 0 ? resolve(opts.algorithm, io) : void 0) ?? "fixed-window",
|
|
16
|
+
countRejected: (opts.countRejected !== void 0 ? resolve(opts.countRejected, io) : void 0) ?? false
|
|
17
|
+
});
|
|
7
18
|
const run = async (io) => {
|
|
8
19
|
const kio = {
|
|
9
20
|
req: io.req,
|
|
10
21
|
ctx: io.ctx
|
|
11
22
|
};
|
|
23
|
+
const rule = ruleFor(kio);
|
|
12
24
|
const advertise = (info) => {
|
|
13
25
|
if (!opts.alwaysHeaders) return;
|
|
14
26
|
for (const [name, value] of Object.entries(rateLimitHeaders(info, opts.headers))) io.setHeader(name, value);
|
|
15
27
|
};
|
|
16
28
|
const fullBudget = () => ({
|
|
17
|
-
limit:
|
|
18
|
-
remaining:
|
|
29
|
+
limit: rule.limit,
|
|
30
|
+
remaining: rule.limit,
|
|
19
31
|
reset: 0,
|
|
20
32
|
retryAfter: 0
|
|
21
33
|
});
|
|
@@ -26,7 +38,7 @@ function rateLimitServer(def, opts) {
|
|
|
26
38
|
}
|
|
27
39
|
let result;
|
|
28
40
|
try {
|
|
29
|
-
result = await
|
|
41
|
+
result = await store.consume(prefix + opts.key(kio), rule, Date.now());
|
|
30
42
|
} catch (err) {
|
|
31
43
|
try {
|
|
32
44
|
opts.onError?.(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayepi/rate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Rate-limiting middleware for @ayepi/core — pluggable stores, multiple algorithms, customizable 429 responses",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"ioredis": "^5",
|
|
62
|
-
"@ayepi/core": "^0.
|
|
62
|
+
"@ayepi/core": "^0.4.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"ioredis": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"tsdown": "^0.12.0",
|
|
75
75
|
"vitest": "^2.1.8",
|
|
76
76
|
"zod": "^4.4.3",
|
|
77
|
-
"@ayepi/core": "0.
|
|
77
|
+
"@ayepi/core": "0.4.0"
|
|
78
78
|
},
|
|
79
79
|
"keywords": [
|
|
80
80
|
"ayepi",
|