hono-rate-limiter 0.1.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 +56 -0
- package/package.json +22 -0
- package/src/core/defaultKeyGenerator.d.ts +2 -0
- package/src/core/defaultKeyGenerator.js +1 -0
- package/src/core/headers.d.ts +28 -0
- package/src/core/headers.js +1 -0
- package/src/core/index.d.ts +13 -0
- package/src/core/index.js +1 -0
- package/src/core/validations.d.ts +2 -0
- package/src/core/validations.js +1 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -0
- package/src/memory-store/index.d.ts +128 -0
- package/src/memory-store/index.js +1 -0
- package/src/redis/index.d.ts +2 -0
- package/src/redis/index.js +1 -0
- package/src/redis/scripts.d.ts +9 -0
- package/src/redis/scripts.js +16 -0
- package/src/redis/store.d.ts +92 -0
- package/src/redis/store.js +1 -0
- package/src/redis/types.d.ts +29 -0
- package/src/redis/types.js +1 -0
- package/src/types/clientRateLimitInfo.d.ts +4 -0
- package/src/types/clientRateLimitInfo.js +1 -0
- package/src/types/config.d.ts +106 -0
- package/src/types/config.js +1 -0
- package/src/types/index.d.ts +5 -0
- package/src/types/index.js +1 -0
- package/src/types/promisify.d.ts +1 -0
- package/src/types/promisify.js +1 -0
- package/src/types/rateLimitExceededEventHandler.d.ts +1 -0
- package/src/types/rateLimitExceededEventHandler.js +1 -0
- package/src/types/rateLimitInfo.d.ts +10 -0
- package/src/types/rateLimitInfo.js +1 -0
- package/src/types/store.d.ts +65 -0
- package/src/types/store.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<h1 align="center"> <code>🔥hono-rate-limiter🔥</code> </h1>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://github.com/rhinobase/hono-rate-limiter/actions/workflows/test.yaml)
|
|
6
|
+
[](https://npmjs.org/package/hono-rate-limiter "View this project on NPM")
|
|
7
|
+
[](https://www.npmjs.com/package/hono-rate-limiter)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
Rate limiting middleware for [Hono](https://hono.dev/). Use to
|
|
13
|
+
limit repeated requests to public APIs and/or endpoints such as password reset.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { rateLimiter } from "hono-rate-limiter";
|
|
19
|
+
|
|
20
|
+
const limiter = rateLimiter({
|
|
21
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
22
|
+
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
|
|
23
|
+
standardHeaders: "draft-7", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
|
|
24
|
+
// store: ... , // Redis, MemoryStore, etc. See below.
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Apply the rate limiting middleware to all requests.
|
|
28
|
+
app.use(limiter);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
# Data Stores
|
|
32
|
+
|
|
33
|
+
Express-rate-limit supports external data stores to sychronize hit counts across multiple processes and servers.
|
|
34
|
+
|
|
35
|
+
By default, `MemoryStore` is used. This one does not synchronize it’s state across instances. It’s simple to deploy, and often sufficient for basic abuse prevention, but will be inconnsistent across reboots or in deployments with multiple process or servers.
|
|
36
|
+
|
|
37
|
+
Deployments requiring more consistently enforced rate limits should use an external store.
|
|
38
|
+
|
|
39
|
+
Here is a list of stores:
|
|
40
|
+
|
|
41
|
+
| Name | Description |
|
|
42
|
+
| ----------- | --------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| MemoryStore | (default) Simple in-memory option. Does not share state when app has multiple processes or servers. |
|
|
44
|
+
| RedisStore | A [Redis](https://redis.io/)-backed store, more suitable for large or demanding deployments. |
|
|
45
|
+
|
|
46
|
+
Take a look at this [guide](https://express-rate-limit.mintlify.app/guides/creating-a-store) if you wish to create your own store.
|
|
47
|
+
|
|
48
|
+
# Contributing
|
|
49
|
+
|
|
50
|
+
We would love to have more contributors involved!
|
|
51
|
+
|
|
52
|
+
To get started, please read our [Contributing Guide](https://github.com/rhinobase/hono-rate-limiter/blob/main/CONTRIBUTING.md).
|
|
53
|
+
|
|
54
|
+
# Credits
|
|
55
|
+
|
|
56
|
+
The `hono-rate-limiter` project is heavily inspired by [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit)
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hono-rate-limiter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@swc/helpers": "~0.5.2"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"hono": "^4.1.1"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./src/index.js",
|
|
14
|
+
"types": "./src/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./redis": {
|
|
17
|
+
"default": "./src/redis/index.js",
|
|
18
|
+
"types": "./src/redis/index.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"main": "./src/index.js"
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"defaultKeyGenerator",{enumerable:true,get:function(){return defaultKeyGenerator}});const _adapter=require("hono/adapter");function defaultKeyGenerator(c){const runtime=(0,_adapter.getRuntimeKey)();let key=null;switch(runtime){case"workerd":key=c.req.raw.headers.get("CF-Connecting-IP");break;case"fastly":key=c.req.raw.headers.get("Fastly-Client-IP");break;case"other":key=c.req.raw.headers.get("x-real-ip");break;default:break}return key!=null?key:""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { RateLimitInfo } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Sets `RateLimit-*`` headers based on the sixth draft of the IETF specification.
|
|
5
|
+
* See https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-06.
|
|
6
|
+
*
|
|
7
|
+
* @param context {Context} - The hono context object to set headers on.
|
|
8
|
+
* @param info {RateLimitInfo} - The rate limit info, used to set the headers.
|
|
9
|
+
* @param windowMs {number} - The window length.
|
|
10
|
+
*/
|
|
11
|
+
export declare const setDraft6Headers: (context: Context, info: RateLimitInfo, windowMs: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Sets `RateLimit` & `RateLimit-Policy` headers based on the seventh draft of the spec.
|
|
14
|
+
* See https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07.
|
|
15
|
+
*
|
|
16
|
+
* @param context {Context} - The hono context object to set headers on.
|
|
17
|
+
* @param info {RateLimitInfo} - The rate limit info, used to set the headers.
|
|
18
|
+
* @param windowMs {number} - The window length.
|
|
19
|
+
*/
|
|
20
|
+
export declare const setDraft7Headers: (context: Context, info: RateLimitInfo, windowMs: number) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Sets the `Retry-After` header.
|
|
23
|
+
*
|
|
24
|
+
* @param context {Context} - The hono context object to set headers on.
|
|
25
|
+
* @param info {RateLimitInfo} - The rate limit info, used to set the headers.
|
|
26
|
+
* @param windowMs {number} - The window length.
|
|
27
|
+
*/
|
|
28
|
+
export declare const setRetryAfterHeader: (context: Context, info: RateLimitInfo, windowMs: number) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:all[name]})}_export(exports,{setDraft6Headers:function(){return setDraft6Headers},setDraft7Headers:function(){return setDraft7Headers},setRetryAfterHeader:function(){return setRetryAfterHeader}});const getResetSeconds=(resetTime,windowMs)=>{let resetSeconds;if(resetTime){const deltaSeconds=Math.ceil((resetTime.getTime()-Date.now())/1e3);resetSeconds=Math.max(0,deltaSeconds)}else if(windowMs){resetSeconds=Math.ceil(windowMs/1e3)}return resetSeconds};const setDraft6Headers=(context,info,windowMs)=>{if(context.finalized)return;const windowSeconds=Math.ceil(windowMs/1e3);const resetSeconds=getResetSeconds(info.resetTime);context.header("RateLimit-Policy",`${info.limit};w=${windowSeconds}`);context.header("RateLimit-Limit",info.limit.toString());context.header("RateLimit-Remaining",info.remaining.toString());if(resetSeconds)context.header("RateLimit-Reset",resetSeconds.toString())};const setDraft7Headers=(context,info,windowMs)=>{if(context.finalized)return;const windowSeconds=Math.ceil(windowMs/1e3);const resetSeconds=getResetSeconds(info.resetTime,windowMs);context.header("RateLimit-Policy",`${info.limit};w=${windowSeconds}`);context.header("RateLimit",`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`)};const setRetryAfterHeader=(context,info,windowMs)=>{if(context.finalized)return;const resetSeconds=getResetSeconds(info.resetTime,windowMs);context.header("Retry-After",resetSeconds==null?void 0:resetSeconds.toString())};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Env, Input } from "hono";
|
|
2
|
+
import type { ConfigType } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* Create an instance of IP rate-limiting middleware for Hono.
|
|
6
|
+
*
|
|
7
|
+
* @param config {ConfigType} - Options to configure the rate limiter.
|
|
8
|
+
*
|
|
9
|
+
* @returns - The middleware that rate-limits clients based on your configuration.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export declare function rateLimiter<E extends Env, P extends string, I extends Input>(config?: Partial<ConfigType<E, P, I>>): import("hono").MiddlewareHandler<E, P, I>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"rateLimiter",{enumerable:true,get:function(){return rateLimiter}});const _interop_require_default=require("@swc/helpers/_/_interop_require_default");const _factory=require("hono/factory");const _memorystore=_interop_require_default._(require("../memory-store"));const _defaultKeyGenerator=require("./defaultKeyGenerator");const _headers=require("./headers");const _validations=require("./validations");function rateLimiter(config){const{windowMs=6e4,limit=5,message="Too many requests, please try again later.",statusCode=429,standardHeaders="draft-6",requestPropertyName="rateLimit",skipFailedRequests=false,skipSuccessfulRequests=false,keyGenerator=_defaultKeyGenerator.defaultKeyGenerator,skip=()=>false,requestWasSuccessful=c=>c.res.status<400,handler=async(c,_next,options)=>{c.status(options.statusCode);const responseMessage=typeof options.message==="function"?await options.message(c):options.message;if(typeof responseMessage==="string")return c.text(responseMessage);return c.json(responseMessage)},store=new _memorystore.default}=config!=null?config:{};const options={windowMs,limit,message,statusCode,standardHeaders,requestPropertyName,skipFailedRequests,skipSuccessfulRequests,keyGenerator,skip,requestWasSuccessful,handler,store};if(!(0,_validations.isValidStore)(store))throw new Error("The store is not correctly implmented!");if(typeof store.init==="function")store.init(options);return(0,_factory.createMiddleware)(async(c,next)=>{const isSkippable=await skip(c);if(isSkippable){await next();return}const key=await keyGenerator(c);const{totalHits,resetTime}=await store.increment(key);const retrieveLimit=typeof limit==="function"?limit(c):limit;const _limit=await retrieveLimit;const info={limit:_limit,used:totalHits,remaining:Math.max(_limit-totalHits,0),resetTime};c.set(requestPropertyName,info);if(standardHeaders&&!c.finalized){if(standardHeaders==="draft-7"){(0,_headers.setDraft7Headers)(c,info,windowMs)}else{(0,_headers.setDraft6Headers)(c,info,windowMs)}}let decremented=false;const decrementKey=async()=>{if(!decremented){await store.decrement(key);decremented=true}};const shouldSkipRequest=async()=>{if(skipFailedRequests||skipSuccessfulRequests){const wasRequestSuccessful=await requestWasSuccessful(c);if(skipFailedRequests&&!wasRequestSuccessful||skipSuccessfulRequests&&wasRequestSuccessful)await decrementKey()}};if(totalHits>_limit){if(standardHeaders){(0,_headers.setRetryAfterHeader)(c,info,windowMs)}await shouldSkipRequest();return handler(c,next,options)}try{await next();await shouldSkipRequest()}catch(error){if(skipFailedRequests)await decrementKey()}finally{if(!c.finalized)await decrementKey()}})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"isValidStore",{enumerable:true,get:function(){return isValidStore}});const isValidStore=value=>!!(value==null?void 0:value.increment);
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});const _export_star=require("@swc/helpers/_/_export_star");_export_star._(require("./core"),exports);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { ClientRateLimitInfo, ConfigType, Store } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* The record that stores information about a client - namely, how many times
|
|
5
|
+
* they have hit the endpoint, and when their hit count resets.
|
|
6
|
+
*
|
|
7
|
+
* Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field.
|
|
8
|
+
*/
|
|
9
|
+
type Client = {
|
|
10
|
+
totalHits: number;
|
|
11
|
+
resetTime: Date;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A `Store` that stores the hit count for each client in memory.
|
|
15
|
+
*
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export declare class MemoryStore implements Store {
|
|
19
|
+
/**
|
|
20
|
+
* The duration of time before which all hit counts are reset (in milliseconds).
|
|
21
|
+
*/
|
|
22
|
+
windowMs: number;
|
|
23
|
+
/**
|
|
24
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
25
|
+
* addresses or API keys).
|
|
26
|
+
*
|
|
27
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
28
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
29
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
30
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
31
|
+
* known to be expired and can be deleted in bulk.
|
|
32
|
+
*/
|
|
33
|
+
previous: Map<string, Client>;
|
|
34
|
+
current: Map<string, Client>;
|
|
35
|
+
/**
|
|
36
|
+
* A reference to the active timer.
|
|
37
|
+
*/
|
|
38
|
+
interval?: NodeJS.Timeout;
|
|
39
|
+
/**
|
|
40
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
41
|
+
* cannot affect other instances.
|
|
42
|
+
*/
|
|
43
|
+
localKeys: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Method that initializes the store.
|
|
46
|
+
*
|
|
47
|
+
* @param options {ConfigType} - The options used to setup the middleware.
|
|
48
|
+
*/
|
|
49
|
+
init(options: ConfigType): void;
|
|
50
|
+
/**
|
|
51
|
+
* Method to fetch a client's hit count and reset time.
|
|
52
|
+
*
|
|
53
|
+
* @param key {string} - The identifier for a client.
|
|
54
|
+
*
|
|
55
|
+
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
get(key: string): Promise<ClientRateLimitInfo | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Method to increment a client's hit counter.
|
|
62
|
+
*
|
|
63
|
+
* @param key {string} - The identifier for a client.
|
|
64
|
+
*
|
|
65
|
+
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
increment(key: string): Promise<ClientRateLimitInfo>;
|
|
70
|
+
/**
|
|
71
|
+
* Method to decrement a client's hit counter.
|
|
72
|
+
*
|
|
73
|
+
* @param key {string} - The identifier for a client.
|
|
74
|
+
*
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
decrement(key: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Method to reset a client's hit counter.
|
|
80
|
+
*
|
|
81
|
+
* @param key {string} - The identifier for a client.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
resetKey(key: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Method to reset everyone's hit counter.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
resetAll(): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Method to stop the timer (if currently running) and prevent any memory
|
|
94
|
+
* leaks.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
shutdown(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
101
|
+
* `windowMs` milliseconds from now.
|
|
102
|
+
*
|
|
103
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
104
|
+
* `current` and `previous` maps.
|
|
105
|
+
*
|
|
106
|
+
* @param client {Client} - The client to recycle.
|
|
107
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
108
|
+
*
|
|
109
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
110
|
+
*/
|
|
111
|
+
private resetClient;
|
|
112
|
+
/**
|
|
113
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
114
|
+
* returned is in the `current` map.
|
|
115
|
+
*
|
|
116
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
117
|
+
*
|
|
118
|
+
* @returns {Client} - The requested client.
|
|
119
|
+
*/
|
|
120
|
+
private getClient;
|
|
121
|
+
/**
|
|
122
|
+
* Move current clients to previous, create a new map for current.
|
|
123
|
+
*
|
|
124
|
+
* This function is called every `windowMs`.
|
|
125
|
+
*/
|
|
126
|
+
private clearExpired;
|
|
127
|
+
}
|
|
128
|
+
export default MemoryStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:all[name]})}_export(exports,{MemoryStore:function(){return MemoryStore},default:function(){return _default}});let MemoryStore=class MemoryStore{init(options){this.windowMs=options.windowMs;if(this.interval)clearInterval(this.interval);this.interval=setInterval(()=>{this.clearExpired()},this.windowMs);if(this.interval.unref)this.interval.unref()}async get(key){var _this_current_get;return(_this_current_get=this.current.get(key))!=null?_this_current_get:this.previous.get(key)}async increment(key){const client=this.getClient(key);const now=Date.now();if(client.resetTime.getTime()<=now){this.resetClient(client,now)}client.totalHits++;return client}async decrement(key){const client=this.getClient(key);if(client.totalHits>0)client.totalHits--}async resetKey(key){this.current.delete(key);this.previous.delete(key)}async resetAll(){this.current.clear();this.previous.clear()}shutdown(){clearInterval(this.interval);void this.resetAll()}resetClient(client,now=Date.now()){client.totalHits=0;client.resetTime.setTime(now+this.windowMs);return client}getClient(key){if(this.current.has(key))return this.current.get(key);let client;if(this.previous.has(key)){client=this.previous.get(key);this.previous.delete(key)}else{client={totalHits:0,resetTime:new Date};this.resetClient(client)}this.current.set(key,client);return client}clearExpired(){this.previous=this.current;this.current=new Map}constructor(){this.previous=new Map;this.current=new Map;this.localKeys=true}};const _default=MemoryStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:all[name]})}_export(exports,{RedisStore:function(){return _store.RedisStore},default:function(){return _store.default}});const _export_star=require("@swc/helpers/_/_export_star");const _interop_require_wildcard=require("@swc/helpers/_/_interop_require_wildcard");_export_star._(require("./types.js"),exports);const _store=_interop_require_wildcard._(require("./store.js"));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"default",{enumerable:true,get:function(){return _default}});const scripts={increment:`
|
|
2
|
+
local totalHits = redis.call("INCR", KEYS[1])
|
|
3
|
+
local timeToExpire = redis.call("PTTL", KEYS[1])
|
|
4
|
+
if timeToExpire <= 0 or ARGV[1] == "1"
|
|
5
|
+
then
|
|
6
|
+
redis.call("PEXPIRE", KEYS[1], tonumber(ARGV[2]))
|
|
7
|
+
timeToExpire = tonumber(ARGV[2])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
return { totalHits, timeToExpire }
|
|
11
|
+
`.replaceAll(/^\s+/gm,"").trim(),get:`
|
|
12
|
+
local totalHits = redis.call("GET", KEYS[1])
|
|
13
|
+
local timeToExpire = redis.call("PTTL", KEYS[1])
|
|
14
|
+
|
|
15
|
+
return { totalHits, timeToExpire }
|
|
16
|
+
`.replaceAll(/^\s+/gm,"").trim()};const _default=scripts;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ClientRateLimitInfo, IncrementResponse, ConfigType as RateLimitConfiguration, Store } from "../types";
|
|
2
|
+
import type { Options, RedisReply, SendCommandFn } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A `Store` for the `express-rate-limit` package that stores hit counts in
|
|
5
|
+
* Redis.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RedisStore implements Store {
|
|
8
|
+
/**
|
|
9
|
+
* The function used to send raw commands to Redis.
|
|
10
|
+
*/
|
|
11
|
+
sendCommand: SendCommandFn;
|
|
12
|
+
/**
|
|
13
|
+
* The text to prepend to the key in Redis.
|
|
14
|
+
*/
|
|
15
|
+
prefix: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to reset the expiry for a particular key whenever its hit count
|
|
18
|
+
* changes.
|
|
19
|
+
*/
|
|
20
|
+
resetExpiryOnChange: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Stores the loaded SHA1s of the LUA scripts used for executing the increment
|
|
23
|
+
* and get key operations.
|
|
24
|
+
*/
|
|
25
|
+
incrementScriptSha: Promise<string>;
|
|
26
|
+
getScriptSha: Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* The number of milliseconds to remember that user's requests.
|
|
29
|
+
*/
|
|
30
|
+
windowMs: number;
|
|
31
|
+
/**
|
|
32
|
+
* @constructor for `RedisStore`.
|
|
33
|
+
*
|
|
34
|
+
* @param options {Options} - The configuration options for the store.
|
|
35
|
+
*/
|
|
36
|
+
constructor(options: Options);
|
|
37
|
+
/**
|
|
38
|
+
* Loads the script used to increment a client's hit count.
|
|
39
|
+
*/
|
|
40
|
+
loadIncrementScript(): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Loads the script used to fetch a client's hit count and expiry time.
|
|
43
|
+
*/
|
|
44
|
+
loadGetScript(): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Runs the increment command, and retries it if the script is not loaded.
|
|
47
|
+
*/
|
|
48
|
+
retryableIncrement(key: string): Promise<RedisReply>;
|
|
49
|
+
/**
|
|
50
|
+
* Method to prefix the keys with the given text.
|
|
51
|
+
*
|
|
52
|
+
* @param key {string} - The key.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} - The text + the key.
|
|
55
|
+
*/
|
|
56
|
+
prefixKey(key: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Method that actually initializes the store.
|
|
59
|
+
*
|
|
60
|
+
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
|
|
61
|
+
*/
|
|
62
|
+
init(options: RateLimitConfiguration): void;
|
|
63
|
+
/**
|
|
64
|
+
* Method to fetch a client's hit count and reset time.
|
|
65
|
+
*
|
|
66
|
+
* @param key {string} - The identifier for a client.
|
|
67
|
+
*
|
|
68
|
+
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
69
|
+
*/
|
|
70
|
+
get(key: string): Promise<ClientRateLimitInfo | undefined>;
|
|
71
|
+
/**
|
|
72
|
+
* Method to increment a client's hit counter.
|
|
73
|
+
*
|
|
74
|
+
* @param key {string} - The identifier for a client
|
|
75
|
+
*
|
|
76
|
+
* @returns {IncrementResponse} - The number of hits and reset time for that client
|
|
77
|
+
*/
|
|
78
|
+
increment(key: string): Promise<IncrementResponse>;
|
|
79
|
+
/**
|
|
80
|
+
* Method to decrement a client's hit counter.
|
|
81
|
+
*
|
|
82
|
+
* @param key {string} - The identifier for a client
|
|
83
|
+
*/
|
|
84
|
+
decrement(key: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Method to reset a client's hit counter.
|
|
87
|
+
*
|
|
88
|
+
* @param key {string} - The identifier for a client
|
|
89
|
+
*/
|
|
90
|
+
resetKey(key: string): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
export default RedisStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:all[name]})}_export(exports,{RedisStore:function(){return RedisStore},default:function(){return _default}});const _interop_require_default=require("@swc/helpers/_/_interop_require_default");const _scripts=_interop_require_default._(require("./scripts.js"));const toInt=input=>{if(typeof input==="number")return input;return Number.parseInt((input!=null?input:"").toString(),10)};const parseScriptResponse=results=>{if(!Array.isArray(results))throw new TypeError("Expected result to be array of values");if(results.length!==2)throw new Error(`Expected 2 replies, got ${results.length}`);const totalHits=results[0]===false?0:toInt(results[0]);const timeToExpire=toInt(results[1]);const resetTime=new Date(Date.now()+timeToExpire);return{totalHits,resetTime}};let RedisStore=class RedisStore{async loadIncrementScript(){const result=await this.sendCommand("SCRIPT","LOAD",_scripts.default.increment);if(typeof result!=="string"){throw new TypeError("unexpected reply from redis client")}return result}async loadGetScript(){const result=await this.sendCommand("SCRIPT","LOAD",_scripts.default.get);if(typeof result!=="string"){throw new TypeError("unexpected reply from redis client")}return result}async retryableIncrement(key){const evalCommand=async()=>this.sendCommand("EVALSHA",await this.incrementScriptSha,"1",this.prefixKey(key),this.resetExpiryOnChange?"1":"0",this.windowMs.toString());try{const result=await evalCommand();return result}catch(e){this.incrementScriptSha=this.loadIncrementScript();return evalCommand()}}prefixKey(key){return`${this.prefix}${key}`}init(options){this.windowMs=options.windowMs}async get(key){const results=await this.sendCommand("EVALSHA",await this.getScriptSha,"1",this.prefixKey(key));return parseScriptResponse(results)}async increment(key){const results=await this.retryableIncrement(key);return parseScriptResponse(results)}async decrement(key){await this.sendCommand("DECR",this.prefixKey(key))}async resetKey(key){await this.sendCommand("DEL",this.prefixKey(key))}constructor(options){this.sendCommand=options.sendCommand;var _options_prefix;this.prefix=(_options_prefix=options.prefix)!=null?_options_prefix:"rl:";var _options_resetExpiryOnChange;this.resetExpiryOnChange=(_options_resetExpiryOnChange=options.resetExpiryOnChange)!=null?_options_resetExpiryOnChange:false;this.incrementScriptSha=this.loadIncrementScript();this.getScriptSha=this.loadGetScript()}};const _default=RedisStore;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of data Redis might return to us.
|
|
3
|
+
*/
|
|
4
|
+
type Data = boolean | number | string;
|
|
5
|
+
export type RedisReply = Data | Data[];
|
|
6
|
+
/**
|
|
7
|
+
* The library sends Redis raw commands, so all we need to know are the
|
|
8
|
+
* 'raw-command-sending' functions for each redis client.
|
|
9
|
+
*/
|
|
10
|
+
export type SendCommandFn = (...args: string[]) => Promise<RedisReply>;
|
|
11
|
+
/**
|
|
12
|
+
* The configuration options for the store.
|
|
13
|
+
*/
|
|
14
|
+
export type Options = {
|
|
15
|
+
/**
|
|
16
|
+
* The function used to send commands to Redis.
|
|
17
|
+
*/
|
|
18
|
+
readonly sendCommand: SendCommandFn;
|
|
19
|
+
/**
|
|
20
|
+
* The text to prepend to the key in Redis.
|
|
21
|
+
*/
|
|
22
|
+
readonly prefix?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to reset the expiry for a particular key whenever its hit count
|
|
25
|
+
* changes.
|
|
26
|
+
*/
|
|
27
|
+
readonly resetExpiryOnChange?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Context, Env, Input, Next } from "hono";
|
|
2
|
+
import type { StatusCode } from "hono/utils/http-status";
|
|
3
|
+
import type { Promisify } from "./promisify";
|
|
4
|
+
import type { Store } from "./store";
|
|
5
|
+
/**
|
|
6
|
+
* Hono request handler that sends back a response when a client is
|
|
7
|
+
* rate-limited.
|
|
8
|
+
*
|
|
9
|
+
* @param context {Context} - The Hono context object.
|
|
10
|
+
* @param next {Next} - The Hono `next` function, can be called to skip responding.
|
|
11
|
+
* @param optionsUsed {ConfigType} - The options used to set up the middleware.
|
|
12
|
+
*/
|
|
13
|
+
export type RateLimitExceededEventHandler<E extends Env = any, P extends string = any, I extends Input = NonNullable<unknown>> = (c: Context<E, P, I>, next: Next, optionsUsed: ConfigType<E, P, I>) => void;
|
|
14
|
+
/**
|
|
15
|
+
* The configuration options for the rate limiter.
|
|
16
|
+
*/
|
|
17
|
+
export type ConfigType<E extends Env = any, P extends string = any, I extends Input = NonNullable<unknown>> = {
|
|
18
|
+
/**
|
|
19
|
+
* How long we should remember the requests.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to `60000` ms (= 1 minute).
|
|
22
|
+
*/
|
|
23
|
+
windowMs: number;
|
|
24
|
+
/**
|
|
25
|
+
* The maximum number of connections to allow during the `window` before
|
|
26
|
+
* rate limiting the client.
|
|
27
|
+
*
|
|
28
|
+
* Can be the limit itself as a number or express middleware that parses
|
|
29
|
+
* the request and then figures out the limit.
|
|
30
|
+
*
|
|
31
|
+
* Defaults to `5`.
|
|
32
|
+
*/
|
|
33
|
+
limit: number | ((c: Context<E, P, I>) => Promisify<number>);
|
|
34
|
+
/**
|
|
35
|
+
* The response body to send back when a client is rate limited.
|
|
36
|
+
*
|
|
37
|
+
* Defaults to `'Too many requests, please try again later.'`
|
|
38
|
+
*/
|
|
39
|
+
message: string | JSON | ((c: Context<E, P, I>) => Promisify<string | JSON>);
|
|
40
|
+
/**
|
|
41
|
+
* The HTTP status code to send back when a client is rate limited.
|
|
42
|
+
*
|
|
43
|
+
* Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
|
|
44
|
+
*/
|
|
45
|
+
statusCode: StatusCode;
|
|
46
|
+
/**
|
|
47
|
+
* Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
|
|
48
|
+
*
|
|
49
|
+
* Defaults to `draft-6`.
|
|
50
|
+
*/
|
|
51
|
+
standardHeaders: boolean | "draft-6" | "draft-7";
|
|
52
|
+
/**
|
|
53
|
+
* The name of the property on the request object to store the rate limit info.
|
|
54
|
+
*
|
|
55
|
+
* Defaults to `rateLimit`.
|
|
56
|
+
*/
|
|
57
|
+
requestPropertyName: string;
|
|
58
|
+
/**
|
|
59
|
+
* If `true`, the library will (by default) skip all requests that have a 4XX
|
|
60
|
+
* or 5XX status.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to `false`.
|
|
63
|
+
*/
|
|
64
|
+
skipFailedRequests: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* If `true`, the library will (by default) skip all requests that have a
|
|
67
|
+
* status code less than 400.
|
|
68
|
+
*
|
|
69
|
+
* Defaults to `false`.
|
|
70
|
+
*/
|
|
71
|
+
skipSuccessfulRequests: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Method to generate custom identifiers for clients.
|
|
74
|
+
*
|
|
75
|
+
* By default, the client's IP address is used.
|
|
76
|
+
*/
|
|
77
|
+
keyGenerator: (c: Context<E, P, I>) => Promisify<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Hono request handler that sends back a response when a client is
|
|
80
|
+
* rate-limited.
|
|
81
|
+
*
|
|
82
|
+
* By default, sends back the `statusCode` and `message` set via the options.
|
|
83
|
+
*/
|
|
84
|
+
handler: RateLimitExceededEventHandler<E, P, I>;
|
|
85
|
+
/**
|
|
86
|
+
* Method (in the form of middleware) to determine whether or not this request
|
|
87
|
+
* counts towards a client's quota.
|
|
88
|
+
*
|
|
89
|
+
* By default, skips no requests.
|
|
90
|
+
*/
|
|
91
|
+
skip: (c: Context<E, P, I>) => Promisify<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Method to determine whether or not the request counts as 'succesful'. Used
|
|
94
|
+
* when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
|
|
95
|
+
*
|
|
96
|
+
* By default, requests with a response status code less than 400 are considered
|
|
97
|
+
* successful.
|
|
98
|
+
*/
|
|
99
|
+
requestWasSuccessful: (c: Context<E, P, I>) => Promisify<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* The `Store` to use to store the hit count for each client.
|
|
102
|
+
*
|
|
103
|
+
* By default, the built-in `MemoryStore` will be used.
|
|
104
|
+
*/
|
|
105
|
+
store: Store;
|
|
106
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Promisify<T> = T | Promise<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ClientRateLimitInfo } from "./clientRateLimitInfo";
|
|
2
|
+
import type { ConfigType } from "./config";
|
|
3
|
+
export type IncrementResponse = ClientRateLimitInfo;
|
|
4
|
+
/**
|
|
5
|
+
* An interface that all hit counter stores must implement.
|
|
6
|
+
*/
|
|
7
|
+
export type Store = {
|
|
8
|
+
/**
|
|
9
|
+
* Method that initializes the store, and has access to the options passed to
|
|
10
|
+
* the middleware too.
|
|
11
|
+
*
|
|
12
|
+
* @param options {ConfigType} - The options used to setup the middleware.
|
|
13
|
+
*/
|
|
14
|
+
init?: (options: ConfigType) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Method to fetch a client's hit count and reset time.
|
|
17
|
+
*
|
|
18
|
+
* @param key {string} - The identifier for a client.
|
|
19
|
+
*
|
|
20
|
+
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
21
|
+
*/
|
|
22
|
+
get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Method to increment a client's hit counter.
|
|
25
|
+
*
|
|
26
|
+
* @param key {string} - The identifier for a client.
|
|
27
|
+
*
|
|
28
|
+
* @returns {IncrementResponse | undefined} - The number of hits and reset time for that client.
|
|
29
|
+
*/
|
|
30
|
+
increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
|
|
31
|
+
/**
|
|
32
|
+
* Method to decrement a client's hit counter.
|
|
33
|
+
*
|
|
34
|
+
* @param key {string} - The identifier for a client.
|
|
35
|
+
*/
|
|
36
|
+
decrement: (key: string) => Promise<void> | void;
|
|
37
|
+
/**
|
|
38
|
+
* Method to reset a client's hit counter.
|
|
39
|
+
*
|
|
40
|
+
* @param key {string} - The identifier for a client.
|
|
41
|
+
*/
|
|
42
|
+
resetKey: (key: string) => Promise<void> | void;
|
|
43
|
+
/**
|
|
44
|
+
* Method to reset everyone's hit counter.
|
|
45
|
+
*/
|
|
46
|
+
resetAll?: () => Promise<void> | void;
|
|
47
|
+
/**
|
|
48
|
+
* Method to shutdown the store, stop timers, and release all resources.
|
|
49
|
+
*/
|
|
50
|
+
shutdown?: () => Promise<void> | void;
|
|
51
|
+
/**
|
|
52
|
+
* Flag to indicate that keys incremented in one instance of this store can
|
|
53
|
+
* not affect other instances. Typically false if a database is used, true for
|
|
54
|
+
* MemoryStore.
|
|
55
|
+
*
|
|
56
|
+
* Used to help detect double-counting misconfigurations.
|
|
57
|
+
*/
|
|
58
|
+
localKeys?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Optional value that the store prepends to keys
|
|
61
|
+
*
|
|
62
|
+
* Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
|
|
63
|
+
*/
|
|
64
|
+
prefix?: string;
|
|
65
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|