@jellyfungus/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.
@@ -0,0 +1,64 @@
1
+ // src/store/cloudflare-kv.ts
2
+ var CloudflareKVStore = class {
3
+ namespace;
4
+ prefix;
5
+ windowMs = 6e4;
6
+ constructor(options) {
7
+ this.namespace = options.namespace;
8
+ this.prefix = options.prefix ?? "rl:";
9
+ }
10
+ init(windowMs) {
11
+ this.windowMs = windowMs;
12
+ }
13
+ async increment(key) {
14
+ const fullKey = `${this.prefix}${key}`;
15
+ const now = Date.now();
16
+ const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1e3));
17
+ const existing = await this.namespace.get(fullKey, {
18
+ type: "json"
19
+ });
20
+ let count;
21
+ let reset;
22
+ if (!existing || existing.reset <= now) {
23
+ count = 1;
24
+ reset = now + this.windowMs;
25
+ } else {
26
+ count = existing.count + 1;
27
+ reset = existing.reset;
28
+ }
29
+ await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {
30
+ expirationTtl: ttlSeconds
31
+ });
32
+ return { count, reset };
33
+ }
34
+ async get(key) {
35
+ const fullKey = `${this.prefix}${key}`;
36
+ const data = await this.namespace.get(fullKey, { type: "json" });
37
+ if (!data || data.reset <= Date.now()) {
38
+ return void 0;
39
+ }
40
+ return { count: data.count, reset: data.reset };
41
+ }
42
+ async decrement(key) {
43
+ const fullKey = `${this.prefix}${key}`;
44
+ const now = Date.now();
45
+ const existing = await this.namespace.get(fullKey, {
46
+ type: "json"
47
+ });
48
+ if (existing && existing.count > 0 && existing.reset > now) {
49
+ const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1e3));
50
+ await this.namespace.put(
51
+ fullKey,
52
+ JSON.stringify({ count: existing.count - 1, reset: existing.reset }),
53
+ { expirationTtl: ttlSeconds }
54
+ );
55
+ }
56
+ }
57
+ async resetKey(key) {
58
+ await this.namespace.delete(`${this.prefix}${key}`);
59
+ }
60
+ };
61
+ export {
62
+ CloudflareKVStore
63
+ };
64
+ //# sourceMappingURL=cloudflare-kv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/store/cloudflare-kv.ts"],"sourcesContent":["/**\n * Cloudflare KV store for rate limiting.\n *\n * Note: KV is eventually consistent (~60s propagation).\n * For strict rate limiting, consider Durable Objects.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Cloudflare KV Namespace interface\n */\nexport type KVNamespace = {\n get: <T = string>(\n key: string,\n options?: { type: \"json\" },\n ) => Promise<T | null>;\n put: (\n key: string,\n value: string,\n options?: { expirationTtl?: number },\n ) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/**\n * Options for Cloudflare KV store\n */\nexport type CloudflareKVStoreOptions = {\n /**\n * KV Namespace binding\n */\n namespace: KVNamespace;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\ntype KVEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * Cloudflare KV store for rate limiting in Workers.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { CloudflareKVStore } from 'hono-rate-limit/store/cloudflare-kv'\n *\n * type Bindings = { RATE_LIMIT_KV: KVNamespace }\n *\n * app.use('*', async (c, next) => {\n * const limiter = rateLimiter({\n * store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),\n * })\n * return limiter(c, next)\n * })\n * ```\n */\nexport class CloudflareKVStore implements RateLimitStore {\n private namespace: KVNamespace;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: CloudflareKVStoreOptions) {\n this.namespace = options.namespace;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n // KV minimum TTL is 60 seconds\n const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1000));\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n let count: number;\n let reset: number;\n\n if (!existing || existing.reset <= now) {\n // New window\n count = 1;\n reset = now + this.windowMs;\n } else {\n // Increment\n count = existing.count + 1;\n reset = existing.reset;\n }\n\n await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {\n expirationTtl: ttlSeconds,\n });\n\n return { count, reset };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const data = await this.namespace.get<KVEntry>(fullKey, { type: \"json\" });\n\n if (!data || data.reset <= Date.now()) {\n return undefined;\n }\n\n return { count: data.count, reset: data.reset };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n if (existing && existing.count > 0 && existing.reset > now) {\n const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1000));\n await this.namespace.put(\n fullKey,\n JSON.stringify({ count: existing.count - 1, reset: existing.reset }),\n { expirationTtl: ttlSeconds },\n );\n }\n }\n\n async resetKey(key: string): Promise<void> {\n await this.namespace.delete(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";AAgEO,IAAM,oBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAAmC;AAC7C,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,WAAW,GAAI,CAAC;AAE/D,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI;AACJ,QAAI;AAEJ,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,cAAQ;AACR,cAAQ,MAAM,KAAK;AAAA,IACrB,OAAO;AAEL,cAAQ,SAAS,QAAQ;AACzB,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,KAAK,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG;AAAA,MAClE,eAAe;AAAA,IACjB,CAAC;AAED,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK,UAAU,IAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAExE,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,SAAS,QAAQ,KAAK,SAAS,QAAQ,KAAK;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAI,CAAC;AACxE,YAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,KAAK,UAAU,EAAE,OAAO,SAAS,QAAQ,GAAG,OAAO,SAAS,MAAM,CAAC;AAAA,QACnE,EAAE,eAAe,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,UAAU,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EACpD;AACF;","names":[]}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/store/redis.ts
21
+ var redis_exports = {};
22
+ __export(redis_exports, {
23
+ RedisStore: () => RedisStore
24
+ });
25
+ module.exports = __toCommonJS(redis_exports);
26
+ var INCR_SCRIPT = `
27
+ local key = KEYS[1]
28
+ local window = tonumber(ARGV[1])
29
+ local now = tonumber(ARGV[2])
30
+
31
+ local count = redis.call('INCR', key)
32
+ if count == 1 then
33
+ redis.call('PEXPIRE', key, window)
34
+ end
35
+
36
+ local ttl = redis.call('PTTL', key)
37
+ local reset = now + ttl
38
+
39
+ return {count, reset}
40
+ `;
41
+ var RedisStore = class {
42
+ client;
43
+ prefix;
44
+ windowMs = 6e4;
45
+ constructor(options) {
46
+ this.client = options.client;
47
+ this.prefix = options.prefix ?? "rl:";
48
+ }
49
+ init(windowMs) {
50
+ this.windowMs = windowMs;
51
+ }
52
+ async increment(key) {
53
+ const fullKey = `${this.prefix}${key}`;
54
+ const now = Date.now();
55
+ const result = await this.client.eval(
56
+ INCR_SCRIPT,
57
+ [fullKey],
58
+ [this.windowMs, now]
59
+ );
60
+ return {
61
+ count: result[0],
62
+ reset: result[1]
63
+ };
64
+ }
65
+ async get(key) {
66
+ const fullKey = `${this.prefix}${key}`;
67
+ const value = await this.client.get(fullKey);
68
+ if (!value) {
69
+ return void 0;
70
+ }
71
+ return {
72
+ count: parseInt(value, 10),
73
+ reset: Date.now() + this.windowMs
74
+ };
75
+ }
76
+ async decrement(key) {
77
+ const fullKey = `${this.prefix}${key}`;
78
+ await this.client.eval(
79
+ 'local count = redis.call("GET", KEYS[1]); if count and tonumber(count) > 0 then redis.call("DECR", KEYS[1]) end',
80
+ [fullKey],
81
+ []
82
+ );
83
+ }
84
+ async resetKey(key) {
85
+ await this.client.del(`${this.prefix}${key}`);
86
+ }
87
+ };
88
+ // Annotate the CommonJS export names for ESM import in node:
89
+ 0 && (module.exports = {
90
+ RedisStore
91
+ });
92
+ //# sourceMappingURL=redis.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AACF;","names":[]}
@@ -0,0 +1,59 @@
1
+ import { RateLimitStore, StoreResult } from '../index.cjs';
2
+ import 'hono';
3
+
4
+ /**
5
+ * Redis store for rate limiting.
6
+ * Compatible with ioredis, @upstash/redis, and similar clients.
7
+ */
8
+
9
+ /**
10
+ * Redis client interface
11
+ */
12
+ type RedisClient = {
13
+ eval: (script: string, keys: string[], args: (string | number)[]) => Promise<unknown> | unknown;
14
+ get: (key: string) => Promise<string | null> | string | null;
15
+ del: (...keys: string[]) => Promise<number> | number;
16
+ };
17
+ /**
18
+ * Options for Redis store
19
+ */
20
+ type RedisStoreOptions = {
21
+ /**
22
+ * Redis client instance
23
+ */
24
+ client: RedisClient;
25
+ /**
26
+ * Key prefix for rate limit entries
27
+ * @default 'rl:'
28
+ */
29
+ prefix?: string;
30
+ };
31
+ /**
32
+ * Redis store for distributed rate limiting.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { rateLimiter } from 'hono-rate-limit'
37
+ * import { RedisStore } from 'hono-rate-limit/store/redis'
38
+ * import Redis from 'ioredis'
39
+ *
40
+ * const redis = new Redis(process.env.REDIS_URL)
41
+ *
42
+ * app.use(rateLimiter({
43
+ * store: new RedisStore({ client: redis }),
44
+ * }))
45
+ * ```
46
+ */
47
+ declare class RedisStore implements RateLimitStore {
48
+ private client;
49
+ private prefix;
50
+ private windowMs;
51
+ constructor(options: RedisStoreOptions);
52
+ init(windowMs: number): void;
53
+ increment(key: string): Promise<StoreResult>;
54
+ get(key: string): Promise<StoreResult | undefined>;
55
+ decrement(key: string): Promise<void>;
56
+ resetKey(key: string): Promise<void>;
57
+ }
58
+
59
+ export { type RedisClient, RedisStore, type RedisStoreOptions };
@@ -0,0 +1,59 @@
1
+ import { RateLimitStore, StoreResult } from '../index.js';
2
+ import 'hono';
3
+
4
+ /**
5
+ * Redis store for rate limiting.
6
+ * Compatible with ioredis, @upstash/redis, and similar clients.
7
+ */
8
+
9
+ /**
10
+ * Redis client interface
11
+ */
12
+ type RedisClient = {
13
+ eval: (script: string, keys: string[], args: (string | number)[]) => Promise<unknown> | unknown;
14
+ get: (key: string) => Promise<string | null> | string | null;
15
+ del: (...keys: string[]) => Promise<number> | number;
16
+ };
17
+ /**
18
+ * Options for Redis store
19
+ */
20
+ type RedisStoreOptions = {
21
+ /**
22
+ * Redis client instance
23
+ */
24
+ client: RedisClient;
25
+ /**
26
+ * Key prefix for rate limit entries
27
+ * @default 'rl:'
28
+ */
29
+ prefix?: string;
30
+ };
31
+ /**
32
+ * Redis store for distributed rate limiting.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { rateLimiter } from 'hono-rate-limit'
37
+ * import { RedisStore } from 'hono-rate-limit/store/redis'
38
+ * import Redis from 'ioredis'
39
+ *
40
+ * const redis = new Redis(process.env.REDIS_URL)
41
+ *
42
+ * app.use(rateLimiter({
43
+ * store: new RedisStore({ client: redis }),
44
+ * }))
45
+ * ```
46
+ */
47
+ declare class RedisStore implements RateLimitStore {
48
+ private client;
49
+ private prefix;
50
+ private windowMs;
51
+ constructor(options: RedisStoreOptions);
52
+ init(windowMs: number): void;
53
+ increment(key: string): Promise<StoreResult>;
54
+ get(key: string): Promise<StoreResult | undefined>;
55
+ decrement(key: string): Promise<void>;
56
+ resetKey(key: string): Promise<void>;
57
+ }
58
+
59
+ export { type RedisClient, RedisStore, type RedisStoreOptions };
@@ -0,0 +1,67 @@
1
+ // src/store/redis.ts
2
+ var INCR_SCRIPT = `
3
+ local key = KEYS[1]
4
+ local window = tonumber(ARGV[1])
5
+ local now = tonumber(ARGV[2])
6
+
7
+ local count = redis.call('INCR', key)
8
+ if count == 1 then
9
+ redis.call('PEXPIRE', key, window)
10
+ end
11
+
12
+ local ttl = redis.call('PTTL', key)
13
+ local reset = now + ttl
14
+
15
+ return {count, reset}
16
+ `;
17
+ var RedisStore = class {
18
+ client;
19
+ prefix;
20
+ windowMs = 6e4;
21
+ constructor(options) {
22
+ this.client = options.client;
23
+ this.prefix = options.prefix ?? "rl:";
24
+ }
25
+ init(windowMs) {
26
+ this.windowMs = windowMs;
27
+ }
28
+ async increment(key) {
29
+ const fullKey = `${this.prefix}${key}`;
30
+ const now = Date.now();
31
+ const result = await this.client.eval(
32
+ INCR_SCRIPT,
33
+ [fullKey],
34
+ [this.windowMs, now]
35
+ );
36
+ return {
37
+ count: result[0],
38
+ reset: result[1]
39
+ };
40
+ }
41
+ async get(key) {
42
+ const fullKey = `${this.prefix}${key}`;
43
+ const value = await this.client.get(fullKey);
44
+ if (!value) {
45
+ return void 0;
46
+ }
47
+ return {
48
+ count: parseInt(value, 10),
49
+ reset: Date.now() + this.windowMs
50
+ };
51
+ }
52
+ async decrement(key) {
53
+ const fullKey = `${this.prefix}${key}`;
54
+ await this.client.eval(
55
+ 'local count = redis.call("GET", KEYS[1]); if count and tonumber(count) > 0 then redis.call("DECR", KEYS[1]) end',
56
+ [fullKey],
57
+ []
58
+ );
59
+ }
60
+ async resetKey(key) {
61
+ await this.client.del(`${this.prefix}${key}`);
62
+ }
63
+ };
64
+ export {
65
+ RedisStore
66
+ };
67
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@jellyfungus/hono-rate-limiter",
3
+ "version": "0.1.0",
4
+ "description": "Rate limiting middleware for Hono web framework",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./store/redis": {
16
+ "types": "./dist/store/redis.d.ts",
17
+ "import": "./dist/store/redis.js",
18
+ "require": "./dist/store/redis.cjs"
19
+ },
20
+ "./store/cloudflare-kv": {
21
+ "types": "./dist/store/cloudflare-kv.d.ts",
22
+ "import": "./dist/store/cloudflare-kv.js",
23
+ "require": "./dist/store/cloudflare-kv.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "typecheck": "tsc --noEmit",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "keywords": [
37
+ "hono",
38
+ "rate-limit",
39
+ "rate-limiter",
40
+ "middleware",
41
+ "api",
42
+ "cloudflare",
43
+ "workers",
44
+ "deno",
45
+ "bun",
46
+ "node"
47
+ ],
48
+ "author": "",
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/rokasta12/hono-rate-limiter.git"
53
+ },
54
+ "peerDependencies": {
55
+ "hono": ">=4.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.0.10",
59
+ "hono": "^4.6.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.0.0",
62
+ "vitest": "^2.0.0"
63
+ }
64
+ }