hono-rate-limiter 0.1.2 → 0.2.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 CHANGED
@@ -34,7 +34,7 @@ app.use(limiter);
34
34
 
35
35
  # Data Stores
36
36
 
37
- Express-rate-limit supports external data stores to synchronize hit counts across multiple processes and servers.
37
+ `hono-rate-limit` supports external data stores to synchronize hit counts across multiple processes and servers.
38
38
 
39
39
  By default, `MemoryStore` is used. This one does not synchronize its 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.
40
40
 
@@ -51,7 +51,7 @@ Here is a list of stores:
51
51
  | [`cluster-memory-store`](https://npm.im/@express-rate-limit/cluster-memory-store) | A memory-store wrapper that shares state across all processes on a single server via the [node:cluster](https://nodejs.org/api/cluster.html) module. Does not share state across multiple servers. |
52
52
  | [`precise-memory-rate-limit`](https://www.npm.im/precise-memory-rate-limit) | A memory store similar to the built-in one, except that it stores a distinct timestamp for each key. |
53
53
  | [`typeorm-rate-limit-store`](https://www.npmjs.com/package/typeorm-rate-limit-store) | Supports a variety of databases via [TypeORM](https://typeorm.io/): MySQL, MariaDB, CockroachDB, SQLite, Microsoft SQL Server, Oracle, SAP Hana, and more. |
54
- | [`@rlimit/storage`](<(https://www.npmjs.com/package/@rlimit/storage)>) | A distributed rlimit store, ideal for multi-regional deployments. |
54
+ | [`@rlimit/storage`](https://www.npmjs.com/package/@rlimit/storage) | A distributed rlimit store, ideal for multi-regional deployments. |
55
55
 
56
56
  Take a look at this [guide](https://express-rate-limit.mintlify.app/guides/creating-a-store) if you wish to create your own store.
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-rate-limiter",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -24,9 +24,6 @@
24
24
  "bugs": {
25
25
  "url": "https://github.com/rhinobase/hono-rate-limiter/issues"
26
26
  },
27
- "dependencies": {
28
- "@swc/helpers": "~0.5.2"
29
- },
30
27
  "peerDependencies": {
31
28
  "hono": "^4.1.1"
32
29
  },
@@ -1,5 +1,5 @@
1
1
  import type { Env, Input } from "hono";
2
- import type { ConfigType } from "../types";
2
+ import type { ConfigType } from "./types.js";
3
3
  /**
4
4
  *
5
5
  * Create an instance of IP rate-limiting middleware for Hono.
package/src/core.js ADDED
@@ -0,0 +1 @@
1
+ import{createMiddleware}from"hono/factory";import{defaultKeyGenerator}from"./defaultKeyGenerator.js";import{setDraft6Headers,setDraft7Headers,setRetryAfterHeader}from"./headers.js";import MemoryStore from"./memoryStore.js";import{isValidStore}from"./validations.js";export function rateLimiter(config){const{windowMs=6e4,limit=5,message="Too many requests, please try again later.",statusCode=429,standardHeaders="draft-6",requestPropertyName="rateLimit",requestStorePropertyName="rateLimitStore",skipFailedRequests=false,skipSuccessfulRequests=false,keyGenerator=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}=config??{};const options={windowMs,limit,message,statusCode,standardHeaders,requestPropertyName,requestStorePropertyName,skipFailedRequests,skipSuccessfulRequests,keyGenerator,skip,requestWasSuccessful,handler,store};if(!isValidStore(store))throw new Error("The store is not correctly implmented!");if(typeof store.init==="function")store.init(options);return 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);c.set(requestStorePropertyName,{getKey:store.get?.bind(store),resetKey:store.resetKey.bind(store)});if(standardHeaders&&!c.finalized){if(standardHeaders==="draft-7"){setDraft7Headers(c,info,windowMs)}else{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){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()}})}
@@ -1,5 +1,5 @@
1
1
  import type { Context } from "hono";
2
- import type { RateLimitInfo } from "../types";
2
+ import type { RateLimitInfo } from "./types.js";
3
3
  /**
4
4
  * Sets `RateLimit-*`` headers based on the sixth draft of the IETF specification.
5
5
  * See https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-06.
package/src/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { rateLimiter as default, rateLimiter } from "./core";
2
- export type * from "./types";
1
+ export { rateLimiter as default, rateLimiter } from "./core.js";
2
+ export type * from "./types.js";
package/src/index.js CHANGED
@@ -1 +1 @@
1
- export{rateLimiter as default,rateLimiter}from"./core";
1
+ export{rateLimiter as default,rateLimiter}from"./core.js";
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import type { ClientRateLimitInfo, ConfigType, Store } from "../types";
2
+ import type { ClientRateLimitInfo, ConfigType, Store } from "./types.js";
3
3
  /**
4
4
  * The record that stores information about a client - namely, how many times
5
5
  * they have hit the endpoint, and when their hit count resets.
@@ -1,7 +1,26 @@
1
1
  import type { Context, Env, Input, Next } from "hono";
2
2
  import type { StatusCode } from "hono/utils/http-status";
3
- import type { Promisify } from "./promisify";
4
- import type { Store } from "./store";
3
+ /**
4
+ * Data returned from the `Store` when a client's hit counter is incremented.
5
+ *
6
+ * @property totalHits {number} - The number of hits for that client so far.
7
+ * @property resetTime {Date | undefined} - The time when the counter resets.
8
+ */
9
+ export type ClientRateLimitInfo = {
10
+ totalHits: number;
11
+ resetTime?: Date;
12
+ };
13
+ export type Promisify<T> = T | Promise<T>;
14
+ /**
15
+ * The rate limit related information for each client included in the
16
+ * Hono context object.
17
+ */
18
+ export type RateLimitInfo = {
19
+ limit: number;
20
+ used: number;
21
+ remaining: number;
22
+ resetTime: Date | undefined;
23
+ };
5
24
  /**
6
25
  * Hono request handler that sends back a response when a client is
7
26
  * rate-limited.
@@ -110,3 +129,66 @@ export type ConfigType<E extends Env = any, P extends string = any, I extends In
110
129
  */
111
130
  store: Store;
112
131
  };
132
+ export type IncrementResponse = ClientRateLimitInfo;
133
+ /**
134
+ * An interface that all hit counter stores must implement.
135
+ */
136
+ export type Store = {
137
+ /**
138
+ * Method that initializes the store, and has access to the options passed to
139
+ * the middleware too.
140
+ *
141
+ * @param options {ConfigType} - The options used to setup the middleware.
142
+ */
143
+ init?: (options: ConfigType) => void;
144
+ /**
145
+ * Method to fetch a client's hit count and reset time.
146
+ *
147
+ * @param key {string} - The identifier for a client.
148
+ *
149
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
150
+ */
151
+ get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
152
+ /**
153
+ * Method to increment a client's hit counter.
154
+ *
155
+ * @param key {string} - The identifier for a client.
156
+ *
157
+ * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client.
158
+ */
159
+ increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
160
+ /**
161
+ * Method to decrement a client's hit counter.
162
+ *
163
+ * @param key {string} - The identifier for a client.
164
+ */
165
+ decrement: (key: string) => Promise<void> | void;
166
+ /**
167
+ * Method to reset a client's hit counter.
168
+ *
169
+ * @param key {string} - The identifier for a client.
170
+ */
171
+ resetKey: (key: string) => Promise<void> | void;
172
+ /**
173
+ * Method to reset everyone's hit counter.
174
+ */
175
+ resetAll?: () => Promise<void> | void;
176
+ /**
177
+ * Method to shutdown the store, stop timers, and release all resources.
178
+ */
179
+ shutdown?: () => Promise<void> | void;
180
+ /**
181
+ * Flag to indicate that keys incremented in one instance of this store can
182
+ * not affect other instances. Typically false if a database is used, true for
183
+ * MemoryStore.
184
+ *
185
+ * Used to help detect double-counting misconfigurations.
186
+ */
187
+ localKeys?: boolean;
188
+ /**
189
+ * Optional value that the store prepends to keys
190
+ *
191
+ * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
192
+ */
193
+ prefix?: string;
194
+ };
@@ -1,2 +1,2 @@
1
- import type { Store } from "../types";
1
+ import type { Store } from "./types";
2
2
  export declare const isValidStore: (value: Store) => value is Store;
package/src/core/index.js DELETED
@@ -1 +0,0 @@
1
- import{createMiddleware}from"hono/factory";import MemoryStore from"../memory-store";import{defaultKeyGenerator}from"./defaultKeyGenerator";import{setDraft6Headers,setDraft7Headers,setRetryAfterHeader}from"./headers";import{isValidStore}from"./validations";export function rateLimiter(config){const{windowMs=6e4,limit=5,message="Too many requests, please try again later.",statusCode=429,standardHeaders="draft-6",requestPropertyName="rateLimit",requestStorePropertyName="rateLimitStore",skipFailedRequests=false,skipSuccessfulRequests=false,keyGenerator=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}=config??{};const options={windowMs,limit,message,statusCode,standardHeaders,requestPropertyName,requestStorePropertyName,skipFailedRequests,skipSuccessfulRequests,keyGenerator,skip,requestWasSuccessful,handler,store};if(!isValidStore(store))throw new Error("The store is not correctly implmented!");if(typeof store.init==="function")store.init(options);return 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);c.set(requestStorePropertyName,{getKey:store.get?.bind(store),resetKey:store.resetKey.bind(store)});if(standardHeaders&&!c.finalized){if(standardHeaders==="draft-7"){setDraft7Headers(c,info,windowMs)}else{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){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()}})}
@@ -1,4 +0,0 @@
1
- export type ClientRateLimitInfo = {
2
- totalHits: number;
3
- resetTime?: Date;
4
- };
@@ -1 +0,0 @@
1
- export{};
@@ -1,5 +0,0 @@
1
- export type * from "./store";
2
- export type * from "./promisify";
3
- export type * from "./config";
4
- export type * from "./clientRateLimitInfo";
5
- export type * from "./rateLimitInfo";
@@ -1 +0,0 @@
1
- export{};
@@ -1 +0,0 @@
1
- export type Promisify<T> = T | Promise<T>;
@@ -1 +0,0 @@
1
- export{};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export{};
@@ -1,10 +0,0 @@
1
- /**
2
- * The rate limit related information for each client included in the
3
- * Hono context object.
4
- */
5
- export type RateLimitInfo = {
6
- limit: number;
7
- used: number;
8
- remaining: number;
9
- resetTime: Date | undefined;
10
- };
@@ -1 +0,0 @@
1
- export{};
@@ -1,65 +0,0 @@
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
- };
@@ -1 +0,0 @@
1
- export{};
File without changes
File without changes
File without changes
File without changes