@alxia/rate-limit 0.2.0 → 0.3.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 +1 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +3 -3
- package/dist/rate-limit.d.ts +2 -2
- package/dist/rate-limit.d.ts.map +1 -1
- package/docs/README.md +6 -0
- package/docs/roadmap.md +3 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -80,3 +80,4 @@ nothing.
|
|
|
80
80
|
- [Guide](https://github.com/softistx/alxia/tree/develop/packages/rate-limit/docs): the options and their defaults, which requests are counted, the headers, the 429 on the wire, stores, and testing.
|
|
81
81
|
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/rate-limit/docs/troubleshooting.md): an error message, and what to do about it.
|
|
82
82
|
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/rate-limit/docs/roadmap.md): what is coming, and what is not planned.
|
|
83
|
+
- [Recipes](https://github.com/softistx/alxia/blob/develop/docs/recipes/README.md): [Caching and rate limiting with Redis](https://github.com/softistx/alxia/blob/develop/docs/recipes/caching-and-rate-limiting.md).
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/rate-limit.ts
|
|
2
2
|
import {
|
|
3
|
-
defineMiddleware
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
markFactory
|
|
4
5
|
} from "@alxia/core";
|
|
5
6
|
|
|
6
7
|
// src/store.ts
|
|
@@ -67,7 +68,7 @@ function rateLimit(options) {
|
|
|
67
68
|
const store = options.store ?? new MemoryStore;
|
|
68
69
|
const key = options.key ?? ((ctx) => ctx.ip);
|
|
69
70
|
const style = options.headers ?? "draft";
|
|
70
|
-
return defineMiddleware()(async (ctx, next)
|
|
71
|
+
return defineMiddleware()(async function rateLimit(ctx, next) {
|
|
71
72
|
const counted = options.skip?.(ctx) ? undefined : await key(ctx);
|
|
72
73
|
if (counted === undefined) {
|
|
73
74
|
const rateLimit = undefined;
|
|
@@ -105,10 +106,11 @@ function said(headers, style, options, decision) {
|
|
|
105
106
|
headers.set("x-ratelimit-reset", String(Math.ceil((Date.now() + decision.resetAfter) / 1000)));
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
markFactory(rateLimit);
|
|
108
110
|
export {
|
|
109
111
|
MemoryStore,
|
|
110
112
|
rateLimit
|
|
111
113
|
};
|
|
112
114
|
|
|
113
|
-
//# debugId=
|
|
115
|
+
//# debugId=8237EF6642B5B78864756E2164756E21
|
|
114
116
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/rate-limit.ts", "../src/store.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\
|
|
5
|
+
"import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\ttype Reply,\n} from '@alxia/core';\nimport { type Decision, MemoryStore, type RateLimitStore } from './store';\n\n/**\n * `Requires` is what `key` and `skip` read from the context beyond\n * `BaseContext` — a `user` an earlier middleware adds — and what the app that\n * uses the limit must then give.\n */\nexport interface RateLimitOptions<Requires extends object = Empty> {\n\t/** How many requests a key may make in a window: a whole number, 1 or more. */\n\treadonly limit: number;\n\t/** The window, in milliseconds: a whole number, 1 or more. */\n\treadonly windowMs: number;\n\t/** What is counted: the client's address by default. `undefined` is not counted. */\n\treadonly key?: (\n\t\tctx: BaseContext & Requires,\n\t) => string | undefined | Promise<string | undefined>;\n\t/** Where it is counted: one process's memory by default. */\n\treadonly store?: RateLimitStore;\n\t/** Requests not counted at all. */\n\treadonly skip?: (ctx: BaseContext & Requires) => boolean;\n\t/**\n\t * The `RateLimit` headers of the IETF draft on every counted response,\n\t * `X-RateLimit-*` with `legacy`, or none. `draft` by default.\n\t */\n\treadonly headers?: 'draft' | 'legacy' | false;\n}\n\n/** The body of the 429. */\nexport interface RateLimitedBody {\n\treadonly error: 'rate_limited';\n\t/** Seconds until the window ends. */\n\treadonly retryAfter: number;\n}\n\n/** What the routes behind the limit read: where the key stands. */\nexport interface RateLimitInfo {\n\treadonly limit: number;\n\treadonly remaining: number;\n\t/** Milliseconds until the allowance is whole again. */\n\treadonly resetAfter: number;\n}\n\n/**\n * What `rateLimit()` makes: a middleware that requires `Requires` of the\n * app, and gives `rateLimit` or answers the 429.\n */\nexport type RateLimit<Requires extends object = Empty> = Middleware<\n\tRequires,\n\tPromise<\n\t\tReply<429, RateLimitedBody> | Next<{ rateLimit: RateLimitInfo | undefined }>\n\t>\n>;\n\n/**\n * A rate limit, as a middleware: every request it runs on is counted —\n * the routes declared after it, and, given to `app.use`, a request no\n * route matches too — and answered a 429 past the limit.\n *\n * ```ts\n * app.use(rateLimit({ limit: 100, windowMs: 60_000 })).get(...);\n * ```\n *\n * A `key` that reads what an earlier middleware added names it, and the\n * app must then give it: `rateLimit<{ user: User }>({ key: ({ user }) => user.id, … })`.\n */\nexport function rateLimit<Requires extends object = Empty>(\n\toptions: RateLimitOptions<Requires>,\n): NoInfer<RateLimit<Requires>> {\n\tfor (const name of ['limit', 'windowMs'] as const) {\n\t\tconst value = options[name];\n\t\tif (!Number.isSafeInteger(value) || value < 1) {\n\t\t\t// 0 refuses every request; a window of 0 or less never ends one.\n\t\t\tthrow new TypeError(\n\t\t\t\t`rateLimit: ${name} must be a whole number of 1 or more, not ${String(value)}`,\n\t\t\t);\n\t\t}\n\t}\n\tconst store = options.store ?? new MemoryStore();\n\tconst key = options.key ?? ((ctx: BaseContext & Requires) => ctx.ip);\n\tconst style = options.headers ?? 'draft';\n\treturn defineMiddleware<Requires>()(async function rateLimit(ctx, next) {\n\t\tconst counted = options.skip?.(ctx) ? undefined : await key(ctx);\n\t\tif (counted === undefined) {\n\t\t\tconst rateLimit: RateLimitInfo | undefined = undefined;\n\t\t\treturn next({ rateLimit });\n\t\t}\n\t\tconst decision = await store.consume(counted, {\n\t\t\tlimit: options.limit,\n\t\t\twindowMs: options.windowMs,\n\t\t});\n\t\tsaid(ctx.set.headers, style, options, decision);\n\t\tif (!decision.allowed) {\n\t\t\tconst retryAfter = Math.max(1, Math.ceil(decision.retryAfter / 1000));\n\t\t\tconst body: RateLimitedBody = { error: 'rate_limited', retryAfter };\n\t\t\treturn ctx.reply(429, body, {\n\t\t\t\theaders: { 'retry-after': String(retryAfter) },\n\t\t\t});\n\t\t}\n\t\tconst rateLimit: RateLimitInfo | undefined = {\n\t\t\tlimit: options.limit,\n\t\t\tremaining: decision.remaining,\n\t\t\tresetAfter: decision.resetAfter,\n\t\t};\n\t\treturn next({ rateLimit });\n\t});\n}\n\n/** The headers that say where the key stands, in the `style` asked for. */\nfunction said(\n\theaders: Headers,\n\tstyle: 'draft' | 'legacy' | false,\n\toptions: { readonly limit: number; readonly windowMs: number },\n\tdecision: Decision,\n): void {\n\tif (style === 'draft') {\n\t\theaders.set('ratelimit-limit', String(options.limit));\n\t\theaders.set('ratelimit-remaining', String(decision.remaining));\n\t\theaders.set(\n\t\t\t'ratelimit-reset',\n\t\t\tString(Math.ceil(decision.resetAfter / 1000)),\n\t\t);\n\t\theaders.set(\n\t\t\t'ratelimit-policy',\n\t\t\t`${options.limit};w=${Math.ceil(options.windowMs / 1000)}`,\n\t\t);\n\t} else if (style === 'legacy') {\n\t\theaders.set('x-ratelimit-limit', String(options.limit));\n\t\theaders.set('x-ratelimit-remaining', String(decision.remaining));\n\t\theaders.set(\n\t\t\t'x-ratelimit-reset',\n\t\t\tString(Math.ceil((Date.now() + decision.resetAfter) / 1000)),\n\t\t);\n\t}\n}\n\nmarkFactory(rateLimit);\n",
|
|
6
6
|
"/** What a store decides for one request. Every duration is a delay, in milliseconds. */\nexport interface Decision {\n\treadonly allowed: boolean;\n\t/** Requests the key may still make now, after this one. */\n\treadonly remaining: number;\n\t/** Until the key's allowance is whole again. */\n\treadonly resetAfter: number;\n\t/** Until a refused request would be allowed; 0 when this one is. */\n\treadonly retryAfter: number;\n}\n\n/** The policy a store applies: `limit` requests per `windowMs`. */\nexport interface Policy {\n\treadonly limit: number;\n\treadonly windowMs: number;\n}\n\n/**\n * Where requests are counted, and what decides. The memory store counts in\n * one process with a fixed window; `@alxia/redis`'s counts across every\n * process sharing a Redis, with GCRA.\n */\nexport interface RateLimitStore {\n\t/** Counts one request for `key` under `policy`; a refused one counts nothing. */\n\tconsume(key: string, policy: Policy): Decision | Promise<Decision>;\n\t/** Forgets `key`: a user who just logged in. */\n\treset(key: string): void | Promise<void>;\n}\n\n/** A fixed-window counter in memory, swept as windows end. */\nexport class MemoryStore implements RateLimitStore {\n\treadonly #hits = new Map<string, { count: number; resetAt: number }>();\n\t#sweeper: ReturnType<typeof setInterval> | undefined;\n\n\tconsume(key: string, policy: Policy): Decision {\n\t\tconst now = Date.now();\n\t\tlet entry = this.#hits.get(key);\n\t\tif (entry === undefined || entry.resetAt <= now) {\n\t\t\tentry = { count: 0, resetAt: now + policy.windowMs };\n\t\t\tthis.#hits.set(key, entry);\n\t\t}\n\t\tthis.#sweep(policy.windowMs);\n\t\tconst resetAfter = entry.resetAt - now;\n\t\tif (entry.count >= policy.limit) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tremaining: 0,\n\t\t\t\tresetAfter,\n\t\t\t\tretryAfter: resetAfter,\n\t\t\t};\n\t\t}\n\t\tentry.count++;\n\t\treturn {\n\t\t\tallowed: true,\n\t\t\tremaining: policy.limit - entry.count,\n\t\t\tresetAfter,\n\t\t\tretryAfter: 0,\n\t\t};\n\t}\n\n\treset(key: string): void {\n\t\tthis.#hits.delete(key);\n\t}\n\n\t/** How many keys are counted. */\n\tget size(): number {\n\t\treturn this.#hits.size;\n\t}\n\n\t#sweep(windowMs: number): void {\n\t\tif (this.#sweeper !== undefined) return;\n\t\tthis.#sweeper = setInterval(() => {\n\t\t\tconst now = Date.now();\n\t\t\tfor (const [key, entry] of this.#hits) {\n\t\t\t\tif (entry.resetAt <= now) this.#hits.delete(key);\n\t\t\t}\n\t\t\tif (this.#hits.size === 0) {\n\t\t\t\tclearInterval(this.#sweeper);\n\t\t\t\tthis.#sweeper = undefined;\n\t\t\t}\n\t\t}, windowMs);\n\t\tthis.#sweeper.unref?.();\n\t}\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAAA;AAAA;AAAA;;;AC8BO,MAAM,YAAsC;AAAA,EACzC,QAAQ,IAAI;AAAA,EACrB;AAAA,EAEA,OAAO,CAAC,KAAa,QAA0B;AAAA,IAC9C,MAAM,MAAM,KAAK,IAAI;AAAA,IACrB,IAAI,QAAQ,KAAK,MAAM,IAAI,GAAG;AAAA,IAC9B,IAAI,UAAU,aAAa,MAAM,WAAW,KAAK;AAAA,MAChD,QAAQ,EAAE,OAAO,GAAG,SAAS,MAAM,OAAO,SAAS;AAAA,MACnD,KAAK,MAAM,IAAI,KAAK,KAAK;AAAA,IAC1B;AAAA,IACA,KAAK,OAAO,OAAO,QAAQ;AAAA,IAC3B,MAAM,aAAa,MAAM,UAAU;AAAA,IACnC,IAAI,MAAM,SAAS,OAAO,OAAO;AAAA,MAChC,OAAO;AAAA,QACN,SAAS;AAAA,QACT,WAAW;AAAA,QACX;AAAA,QACA,YAAY;AAAA,MACb;AAAA,IACD;AAAA,IACA,MAAM;AAAA,IACN,OAAO;AAAA,MACN,SAAS;AAAA,MACT,WAAW,OAAO,QAAQ,MAAM;AAAA,MAChC;AAAA,MACA,YAAY;AAAA,IACb;AAAA;AAAA,EAGD,KAAK,CAAC,KAAmB;AAAA,IACxB,KAAK,MAAM,OAAO,GAAG;AAAA;AAAA,MAIlB,IAAI,GAAW;AAAA,IAClB,OAAO,KAAK,MAAM;AAAA;AAAA,EAGnB,MAAM,CAAC,UAAwB;AAAA,IAC9B,IAAI,KAAK,aAAa;AAAA,MAAW;AAAA,IACjC,KAAK,WAAW,YAAY,MAAM;AAAA,MACjC,MAAM,MAAM,KAAK,IAAI;AAAA,MACrB,YAAY,KAAK,UAAU,KAAK,OAAO;AAAA,QACtC,IAAI,MAAM,WAAW;AAAA,UAAK,KAAK,MAAM,OAAO,GAAG;AAAA,MAChD;AAAA,MACA,IAAI,KAAK,MAAM,SAAS,GAAG;AAAA,QAC1B,cAAc,KAAK,QAAQ;AAAA,QAC3B,KAAK,WAAW;AAAA,MACjB;AAAA,OACE,QAAQ;AAAA,IACX,KAAK,SAAS,QAAQ;AAAA;AAExB;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "8237EF6642B5B78864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BaseContext, type Empty, type Middleware, type
|
|
1
|
+
import { type BaseContext, type Empty, type Middleware, type Next, type Reply } from '@alxia/core';
|
|
2
2
|
import { type RateLimitStore } from './store';
|
|
3
3
|
/**
|
|
4
4
|
* `Requires` is what `key` and `skip` read from the context beyond
|
|
@@ -41,7 +41,7 @@ export interface RateLimitInfo {
|
|
|
41
41
|
*/
|
|
42
42
|
export type RateLimit<Requires extends object = Empty> = Middleware<Requires, Promise<Reply<429, RateLimitedBody> | Next<{
|
|
43
43
|
rateLimit: RateLimitInfo | undefined;
|
|
44
|
-
}
|
|
44
|
+
}>>>;
|
|
45
45
|
/**
|
|
46
46
|
* A rate limit, as a middleware: every request it runs on is counted —
|
|
47
47
|
* the routes declared after it, and, given to `app.use`, a request no
|
package/dist/rate-limit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,KAAK,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E;;;;GAIG;AACH,MAAM,WAAW,gBAAgB,CAAC,QAAQ,SAAS,MAAM,GAAG,KAAK;IAChE,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,GAAG,CAAC,EAAE,CACd,GAAG,EAAE,WAAW,GAAG,QAAQ,KACvB,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;IAChC,mCAAmC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,QAAQ,KAAK,OAAO,CAAC;IACzD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;CAC9C;AAED,2BAA2B;AAC3B,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,QAAQ,SAAS,MAAM,GAAG,KAAK,IAAI,UAAU,CAClE,QAAQ,EACR,OAAO,CACN,KAAK,CAAC,GAAG,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;IAAE,SAAS,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAC,CAC5E,CACD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,QAAQ,SAAS,MAAM,GAAG,KAAK,EACxD,OAAO,EAAE,gBAAgB,CAAC,QAAQ,CAAC,GACjC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAsC9B"}
|
package/docs/README.md
CHANGED
|
@@ -9,3 +9,9 @@ the errors you may meet, and what is coming.
|
|
|
9
9
|
| [Guide](guide.md) | choosing what to count and where, reading the 429 on the wire, keeping counts across processes, or testing a limit |
|
|
10
10
|
| [Troubleshooting](troubleshooting.md) | something went wrong and you have the message, or a limit does not count as you expected |
|
|
11
11
|
| [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
|
|
12
|
+
|
|
13
|
+
## Recipes
|
|
14
|
+
|
|
15
|
+
A task that crosses packages, in [the repository's recipes](https://github.com/softistx/alxia/blob/develop/docs/recipes/README.md), each with a complete example:
|
|
16
|
+
|
|
17
|
+
- [Caching and rate limiting with Redis](https://github.com/softistx/alxia/blob/develop/docs/recipes/caching-and-rate-limiting.md): a limit and a response cache shared by every process
|
package/docs/roadmap.md
CHANGED
|
@@ -8,7 +8,8 @@ number on it. Every release, with each change it made, is in
|
|
|
8
8
|
## Now
|
|
9
9
|
|
|
10
10
|
- **A middleware, not a plugin.** `app.use(rateLimit({ limit, windowMs }))` is
|
|
11
|
-
the form; `app.plugin(rateLimit(…))
|
|
11
|
+
the form; `app.plugin(rateLimit(…))`, deprecated in 0.4, was removed in 0.5.
|
|
12
|
+
Given to the
|
|
12
13
|
app, the limit also counts a request no route matches, and `RateLimit<Requires>`
|
|
13
14
|
names what `rateLimit()` returns.
|
|
14
15
|
|
|
@@ -33,7 +34,7 @@ Nothing scheduled yet.
|
|
|
33
34
|
|
|
34
35
|
### 0.1.0
|
|
35
36
|
|
|
36
|
-
- **A rate limit as a plugin
|
|
37
|
+
- **A rate limit as a plugin**, the 0.1 form. `rateLimit({ limit, windowMs })` counts
|
|
37
38
|
the requests of every route declared after it, per client address by
|
|
38
39
|
default, and answers a 429 with `Retry-After` and
|
|
39
40
|
`{ error: 'rate_limited', retryAfter }` past the limit.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/rate-limit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Rate limiting for alxia: a 429 with Retry-After past the limit, the remaining allowance typed in the context, pluggable stores",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
44
|
+
"@alxia/core": "^0.5.0",
|
|
45
45
|
"@types/bun": "^1.4.2",
|
|
46
46
|
"zod": "^4.2.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@alxia/core": "^0.
|
|
49
|
+
"@alxia/core": "^0.5.0",
|
|
50
50
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|