@teikyo/rate-limit 0.2.0 → 0.3.1
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 +1 -0
- package/lib/index.d.ts +8 -2
- package/lib/index.js +16 -5
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -30,4 +30,5 @@ Clients are keyed on the IP resolved by the server, so set `trustProxy` when run
|
|
|
30
30
|
| --- | --- | --- |
|
|
31
31
|
| `windowMs` | `number` | Length of the window in milliseconds. Must be a positive safe integer. |
|
|
32
32
|
| `limit` | `number` | Maximum requests per client within a window. Must be a positive safe integer. |
|
|
33
|
+
| `path` | `string` (optional) | Limit only this path prefix, matched by segment: `/oauth` covers `/oauth` and `/oauth/...` but not `/oauthx`. Other routes stay unlimited. A server runs one rate limit plugin, so a global limit and a scoped one cannot be combined. |
|
|
33
34
|
| `store` | `Store` (optional) | A `hono-rate-limiter` store, in-memory by default. The plugin calls its `shutdown()` when the server closes, so do not pass a store shared with or owned by something else. |
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HTTPPlugin } from '@sozai/http-server';
|
|
2
|
+
import type { Context } from 'hono';
|
|
2
3
|
import { type Store } from 'hono-rate-limiter';
|
|
3
4
|
export declare const RATE_LIMIT: import("@sozai/http-server").PluginName<"http:rate-limit", void>;
|
|
4
5
|
export type RateLimitPluginParams = {
|
|
@@ -6,6 +7,10 @@ export type RateLimitPluginParams = {
|
|
|
6
7
|
windowMs: number;
|
|
7
8
|
/** Maximum number of requests per client within a window. */
|
|
8
9
|
limit: number;
|
|
10
|
+
/** Limit only this path prefix, segment-aware: `/oauth` covers `/oauth/...` but not `/oauthx`. */
|
|
11
|
+
path?: string;
|
|
12
|
+
/** Bucket by IP (default), principal attribution, or a custom key with nullish IP fallback. */
|
|
13
|
+
key?: 'ip' | 'principal' | ((c: Context) => string | null | undefined | Promise<string | null | undefined>);
|
|
9
14
|
/**
|
|
10
15
|
* Custom store, defaults to an in-memory store. The plugin calls `shutdown()` on it when
|
|
11
16
|
* the server closes, so do not pass a store shared with or owned by something else.
|
|
@@ -13,7 +18,8 @@ export type RateLimitPluginParams = {
|
|
|
13
18
|
store?: Store;
|
|
14
19
|
};
|
|
15
20
|
/**
|
|
16
|
-
* Rate limit
|
|
17
|
-
* `Retry-After` header past the limit.
|
|
21
|
+
* Rate limit requests by IP, principal attribution, or a custom key, answering `429` with a
|
|
22
|
+
* `Retry-After` header past the limit. With `path`, only that prefix is limited. Health routes are
|
|
23
|
+
* never limited.
|
|
18
24
|
*/
|
|
19
25
|
export declare function rateLimitPlugin(params: RateLimitPluginParams): HTTPPlugin<'http:rate-limit', void, []>;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pluginName } from '@sozai/http-server';
|
|
2
|
+
import { attributionOf, getPrincipal } from '@teikyo/principal';
|
|
2
3
|
import { MemoryStore, rateLimiter } from 'hono-rate-limiter';
|
|
3
4
|
import { clientKey } from './key.js';
|
|
4
5
|
export const RATE_LIMIT = pluginName()('http:rate-limit');
|
|
@@ -8,12 +9,13 @@ function assertPositiveInteger(name, value) {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
|
-
* Rate limit
|
|
12
|
-
* `Retry-After` header past the limit.
|
|
12
|
+
* Rate limit requests by IP, principal attribution, or a custom key, answering `429` with a
|
|
13
|
+
* `Retry-After` header past the limit. With `path`, only that prefix is limited. Health routes are
|
|
14
|
+
* never limited.
|
|
13
15
|
*/ export function rateLimitPlugin(params) {
|
|
14
16
|
assertPositiveInteger('windowMs', params.windowMs);
|
|
15
17
|
assertPositiveInteger('limit', params.limit);
|
|
16
|
-
const { windowMs, limit } = params;
|
|
18
|
+
const { windowMs, limit, key = 'ip', path } = params;
|
|
17
19
|
return {
|
|
18
20
|
name: RATE_LIMIT,
|
|
19
21
|
dependsOn: [],
|
|
@@ -25,9 +27,18 @@ function assertPositiveInteger(name, value) {
|
|
|
25
27
|
standardHeaders: 'draft-7',
|
|
26
28
|
// IPv6 clients share one bucket per /64. A request with no resolvable client IP falls
|
|
27
29
|
// into one shared bucket.
|
|
28
|
-
keyGenerator: (c)=>
|
|
30
|
+
keyGenerator: async (c)=>{
|
|
31
|
+
if (typeof key === 'function') {
|
|
32
|
+
const customKey = await key(c);
|
|
33
|
+
if (customKey != null) return customKey;
|
|
34
|
+
} else if (key === 'principal') {
|
|
35
|
+
const principal = getPrincipal(c);
|
|
36
|
+
if (principal != null) return `p:${attributionOf(principal).onBehalfOf}`;
|
|
37
|
+
}
|
|
38
|
+
return `ip:${clientKey(ctx.clientIP(c))}`;
|
|
39
|
+
},
|
|
29
40
|
store
|
|
30
|
-
}));
|
|
41
|
+
}), path);
|
|
31
42
|
ctx.onClose(()=>store.shutdown?.());
|
|
32
43
|
}
|
|
33
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teikyo/rate-limit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Rate limiting plugin for Sozai HTTP servers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,17 +19,18 @@
|
|
|
19
19
|
"lib/*"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@sozai/http-server": "^0.2.
|
|
22
|
+
"@sozai/http-server": "^0.2.2",
|
|
23
|
+
"@teikyo/principal": "^0.1.0",
|
|
23
24
|
"hono-rate-limiter": "^0.5.4"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^26.6.
|
|
27
|
+
"@types/node": "^26.6.6",
|
|
27
28
|
"del-cli": "^8.0.1",
|
|
28
|
-
"hono": "^4.13.
|
|
29
|
-
"vitest": "5.0
|
|
29
|
+
"hono": "^4.13.18",
|
|
30
|
+
"vitest": "5.1.0"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
|
-
"hono": "^4.13.
|
|
33
|
+
"hono": "^4.13.18"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=24"
|