@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 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 every request by resolved client IP (IPv6 grouped by /64), answering `429` with a
17
- * `Retry-After` header past the limit. Health routes are never limited.
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 every request by resolved client IP (IPv6 grouped by /64), answering `429` with a
12
- * `Retry-After` header past the limit. Health routes are never limited.
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)=>clientKey(ctx.clientIP(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.2.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.0",
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.5",
27
+ "@types/node": "^26.6.6",
27
28
  "del-cli": "^8.0.1",
28
- "hono": "^4.13.13",
29
- "vitest": "5.0.5"
29
+ "hono": "^4.13.18",
30
+ "vitest": "5.1.0"
30
31
  },
31
32
  "peerDependencies": {
32
- "hono": "^4.13.13"
33
+ "hono": "^4.13.18"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=24"