@joint-ops/hitlimit-bun 1.0.0 → 1.0.2
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 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/headers.d.ts +3 -0
- package/dist/core/headers.d.ts.map +1 -0
- package/dist/core/limiter.d.ts +3 -0
- package/dist/core/limiter.d.ts.map +1 -0
- package/dist/core/response.d.ts +3 -0
- package/dist/core/response.d.ts.map +1 -0
- package/dist/core/utils.d.ts +2 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/elysia.d.ts +38 -0
- package/dist/elysia.d.ts.map +1 -0
- package/dist/elysia.js +17877 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +458 -0
- package/dist/loggers/console.d.ts +4 -0
- package/dist/loggers/console.d.ts.map +1 -0
- package/dist/stores/memory.d.ts +3 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +70 -0
- package/dist/stores/redis.d.ts +7 -0
- package/dist/stores/redis.d.ts.map +1 -0
- package/dist/stores/redis.js +9648 -0
- package/dist/stores/sqlite.d.ts +6 -0
- package/dist/stores/sqlite.d.ts.map +1 -0
- package/dist/stores/sqlite.js +81 -0
- package/package.json +20 -5
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
|
|
2
|
+
export type { HitLimitOptions, HitLimitInfo, HitLimitResult, HitLimitStore, StoreResult, TierConfig, HeadersConfig, ResolvedConfig, KeyGenerator, TierResolver, SkipFunction, StoreErrorHandler, ResponseFormatter, ResponseConfig } from '@joint-ops/hitlimit-types';
|
|
3
|
+
export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
|
|
4
|
+
export { sqliteStore } from './stores/sqlite.js';
|
|
5
|
+
export { checkLimit } from './core/limiter.js';
|
|
6
|
+
export interface BunHitLimitOptions extends HitLimitOptions<Request> {
|
|
7
|
+
sqlitePath?: string;
|
|
8
|
+
}
|
|
9
|
+
type BunServer = {
|
|
10
|
+
requestIP(req: Request): {
|
|
11
|
+
address: string;
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
type FetchHandler = (req: Request, server: BunServer) => Response | Promise<Response>;
|
|
15
|
+
export declare function hitlimit(options: BunHitLimitOptions, handler: FetchHandler): (req: Request, server: BunServer) => Response | Promise<Response>;
|
|
16
|
+
export interface HitLimiter {
|
|
17
|
+
check(req: Request, server: BunServer): Response | null | Promise<Response | null>;
|
|
18
|
+
reset(key: string): Promise<void> | void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createHitLimit(options?: BunHitLimitOptions): HitLimiter;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAIhE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AACrQ,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC7G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,MAAM,WAAW,kBAAmB,SAAQ,eAAe,CAAC,OAAO,CAAC;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,KAAK,SAAS,GAAG;IAAE,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAMrF,wBAAgB,QAAQ,CACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,YAAY,GACpB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkLnE;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAClF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACzC;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAsF3E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
|
+
var __require = import.meta.require;
|
|
30
|
+
|
|
31
|
+
// src/stores/sqlite.ts
|
|
32
|
+
import { Database } from "bun:sqlite";
|
|
33
|
+
|
|
34
|
+
class BunSqliteStore {
|
|
35
|
+
db;
|
|
36
|
+
hitStmt;
|
|
37
|
+
getStmt;
|
|
38
|
+
resetStmt;
|
|
39
|
+
cleanupTimer;
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.db = new Database(options.path ?? ":memory:");
|
|
42
|
+
this.db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS hitlimit (
|
|
44
|
+
key TEXT PRIMARY KEY,
|
|
45
|
+
count INTEGER NOT NULL,
|
|
46
|
+
reset_at INTEGER NOT NULL
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
this.hitStmt = this.db.prepare(`
|
|
50
|
+
INSERT INTO hitlimit (key, count, reset_at) VALUES (?1, 1, ?2)
|
|
51
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
52
|
+
count = CASE WHEN reset_at <= ?3 THEN 1 ELSE count + 1 END,
|
|
53
|
+
reset_at = CASE WHEN reset_at <= ?3 THEN ?2 ELSE reset_at END
|
|
54
|
+
`);
|
|
55
|
+
this.getStmt = this.db.prepare("SELECT count, reset_at FROM hitlimit WHERE key = ?");
|
|
56
|
+
this.resetStmt = this.db.prepare("DELETE FROM hitlimit WHERE key = ?");
|
|
57
|
+
this.cleanupTimer = setInterval(() => {
|
|
58
|
+
this.db.prepare("DELETE FROM hitlimit WHERE reset_at <= ?").run(Date.now());
|
|
59
|
+
}, 60000);
|
|
60
|
+
}
|
|
61
|
+
hit(key, windowMs, _limit) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const resetAt = now + windowMs;
|
|
64
|
+
this.hitStmt.run(key, resetAt, now);
|
|
65
|
+
const row = this.getStmt.get(key);
|
|
66
|
+
return { count: row.count, resetAt: row.reset_at };
|
|
67
|
+
}
|
|
68
|
+
reset(key) {
|
|
69
|
+
this.resetStmt.run(key);
|
|
70
|
+
}
|
|
71
|
+
shutdown() {
|
|
72
|
+
clearInterval(this.cleanupTimer);
|
|
73
|
+
this.db.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function sqliteStore(options) {
|
|
77
|
+
return new BunSqliteStore(options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/core/utils.ts
|
|
81
|
+
var UNITS = {
|
|
82
|
+
s: 1000,
|
|
83
|
+
m: 60 * 1000,
|
|
84
|
+
h: 60 * 60 * 1000,
|
|
85
|
+
d: 24 * 60 * 60 * 1000
|
|
86
|
+
};
|
|
87
|
+
function parseWindow(window) {
|
|
88
|
+
if (typeof window === "number")
|
|
89
|
+
return window;
|
|
90
|
+
const match = window.match(/^(\d+)(s|m|h|d)$/);
|
|
91
|
+
if (!match)
|
|
92
|
+
throw new Error(`Invalid window format: ${window}`);
|
|
93
|
+
return parseInt(match[1]) * UNITS[match[2]];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/config.ts
|
|
97
|
+
function resolveConfig(options, defaultStore, defaultKey) {
|
|
98
|
+
return {
|
|
99
|
+
limit: options.limit ?? 100,
|
|
100
|
+
windowMs: parseWindow(options.window ?? "1m"),
|
|
101
|
+
key: options.key ?? defaultKey,
|
|
102
|
+
tiers: options.tiers,
|
|
103
|
+
tier: options.tier,
|
|
104
|
+
response: options.response ?? { hitlimit: true, message: "Whoa there! Rate limit exceeded." },
|
|
105
|
+
headers: {
|
|
106
|
+
standard: options.headers?.standard ?? true,
|
|
107
|
+
legacy: options.headers?.legacy ?? true,
|
|
108
|
+
retryAfter: options.headers?.retryAfter ?? true
|
|
109
|
+
},
|
|
110
|
+
store: options.store ?? defaultStore,
|
|
111
|
+
onStoreError: options.onStoreError ?? (() => "allow"),
|
|
112
|
+
skip: options.skip
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../types/dist/index.js
|
|
117
|
+
var DEFAULT_LIMIT = 100;
|
|
118
|
+
var DEFAULT_WINDOW = "1m";
|
|
119
|
+
var DEFAULT_WINDOW_MS = 60000;
|
|
120
|
+
var DEFAULT_MESSAGE = "Whoa there! Rate limit exceeded.";
|
|
121
|
+
// src/core/headers.ts
|
|
122
|
+
function buildHeaders(info, config, allowed) {
|
|
123
|
+
const headers = {};
|
|
124
|
+
if (config.standard) {
|
|
125
|
+
headers["RateLimit-Limit"] = String(info.limit);
|
|
126
|
+
headers["RateLimit-Remaining"] = String(info.remaining);
|
|
127
|
+
headers["RateLimit-Reset"] = String(Math.ceil(info.resetAt / 1000));
|
|
128
|
+
}
|
|
129
|
+
if (config.legacy) {
|
|
130
|
+
headers["X-RateLimit-Limit"] = String(info.limit);
|
|
131
|
+
headers["X-RateLimit-Remaining"] = String(info.remaining);
|
|
132
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(info.resetAt / 1000));
|
|
133
|
+
}
|
|
134
|
+
if (!allowed && config.retryAfter) {
|
|
135
|
+
headers["Retry-After"] = String(info.resetIn);
|
|
136
|
+
}
|
|
137
|
+
return headers;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/core/response.ts
|
|
141
|
+
function buildBody(response, info) {
|
|
142
|
+
if (typeof response === "function") {
|
|
143
|
+
return response(info);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
...response,
|
|
147
|
+
limit: info.limit,
|
|
148
|
+
remaining: info.remaining,
|
|
149
|
+
resetIn: info.resetIn
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/core/limiter.ts
|
|
154
|
+
async function checkLimit(config, req) {
|
|
155
|
+
const key = await config.key(req);
|
|
156
|
+
let limit = config.limit;
|
|
157
|
+
let windowMs = config.windowMs;
|
|
158
|
+
let tierName;
|
|
159
|
+
if (config.tier && config.tiers) {
|
|
160
|
+
tierName = await config.tier(req);
|
|
161
|
+
const tierConfig = config.tiers[tierName];
|
|
162
|
+
if (tierConfig) {
|
|
163
|
+
limit = tierConfig.limit;
|
|
164
|
+
if (tierConfig.window) {
|
|
165
|
+
windowMs = parseWindow(tierConfig.window);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (limit === Infinity) {
|
|
170
|
+
return {
|
|
171
|
+
allowed: true,
|
|
172
|
+
info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName },
|
|
173
|
+
headers: {},
|
|
174
|
+
body: {}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const result = await config.store.hit(key, windowMs, limit);
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
|
|
180
|
+
const remaining = Math.max(0, limit - result.count);
|
|
181
|
+
const allowed = result.count <= limit;
|
|
182
|
+
const info = {
|
|
183
|
+
limit,
|
|
184
|
+
remaining,
|
|
185
|
+
resetIn,
|
|
186
|
+
resetAt: result.resetAt,
|
|
187
|
+
key,
|
|
188
|
+
tier: tierName
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
allowed,
|
|
192
|
+
info,
|
|
193
|
+
headers: buildHeaders(info, config.headers, allowed),
|
|
194
|
+
body: allowed ? {} : buildBody(config.response, info)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/index.ts
|
|
199
|
+
function getDefaultKey(req, server) {
|
|
200
|
+
return server.requestIP(req)?.address || "unknown";
|
|
201
|
+
}
|
|
202
|
+
function hitlimit(options, handler) {
|
|
203
|
+
const store = options.store ?? sqliteStore({ path: options.sqlitePath });
|
|
204
|
+
const config = resolveConfig(options, store, () => "unknown");
|
|
205
|
+
const hasSkip = !!config.skip;
|
|
206
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
207
|
+
const standardHeaders = config.headers.standard;
|
|
208
|
+
const legacyHeaders = config.headers.legacy;
|
|
209
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
210
|
+
const limit = config.limit;
|
|
211
|
+
const windowMs = config.windowMs;
|
|
212
|
+
const response = config.response;
|
|
213
|
+
const customKey = options.key;
|
|
214
|
+
const blockedBody = JSON.stringify(response);
|
|
215
|
+
if (!hasSkip && !hasTiers) {
|
|
216
|
+
return async (req, server) => {
|
|
217
|
+
try {
|
|
218
|
+
const key = customKey ? await customKey(req) : getDefaultKey(req, server);
|
|
219
|
+
const result = await store.hit(key, windowMs, limit);
|
|
220
|
+
const allowed = result.count <= limit;
|
|
221
|
+
if (!allowed) {
|
|
222
|
+
const headers = { "Content-Type": "application/json" };
|
|
223
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
224
|
+
if (standardHeaders) {
|
|
225
|
+
headers["RateLimit-Limit"] = String(limit);
|
|
226
|
+
headers["RateLimit-Remaining"] = "0";
|
|
227
|
+
headers["RateLimit-Reset"] = String(resetIn);
|
|
228
|
+
}
|
|
229
|
+
if (legacyHeaders) {
|
|
230
|
+
headers["X-RateLimit-Limit"] = String(limit);
|
|
231
|
+
headers["X-RateLimit-Remaining"] = "0";
|
|
232
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
233
|
+
}
|
|
234
|
+
if (retryAfterHeader) {
|
|
235
|
+
headers["Retry-After"] = String(resetIn);
|
|
236
|
+
}
|
|
237
|
+
return new Response(blockedBody, { status: 429, headers });
|
|
238
|
+
}
|
|
239
|
+
const res = await handler(req, server);
|
|
240
|
+
if (standardHeaders || legacyHeaders) {
|
|
241
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
242
|
+
const remaining = Math.max(0, limit - result.count);
|
|
243
|
+
const newHeaders = new Headers(res.headers);
|
|
244
|
+
if (standardHeaders) {
|
|
245
|
+
newHeaders.set("RateLimit-Limit", String(limit));
|
|
246
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
247
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
248
|
+
}
|
|
249
|
+
if (legacyHeaders) {
|
|
250
|
+
newHeaders.set("X-RateLimit-Limit", String(limit));
|
|
251
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
252
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
253
|
+
}
|
|
254
|
+
return new Response(res.body, {
|
|
255
|
+
status: res.status,
|
|
256
|
+
statusText: res.statusText,
|
|
257
|
+
headers: newHeaders
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return res;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const action = await config.onStoreError(error, req);
|
|
263
|
+
if (action === "deny") {
|
|
264
|
+
return new Response('{"hitlimit":true,"message":"Rate limit error"}', {
|
|
265
|
+
status: 429,
|
|
266
|
+
headers: { "Content-Type": "application/json" }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return handler(req, server);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return async (req, server) => {
|
|
274
|
+
if (hasSkip) {
|
|
275
|
+
const shouldSkip = await config.skip(req);
|
|
276
|
+
if (shouldSkip) {
|
|
277
|
+
return handler(req, server);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const key = customKey ? await customKey(req) : getDefaultKey(req, server);
|
|
282
|
+
let effectiveLimit = limit;
|
|
283
|
+
let effectiveWindowMs = windowMs;
|
|
284
|
+
if (hasTiers) {
|
|
285
|
+
const tierName = await config.tier(req);
|
|
286
|
+
const tierConfig = config.tiers[tierName];
|
|
287
|
+
if (tierConfig) {
|
|
288
|
+
effectiveLimit = tierConfig.limit;
|
|
289
|
+
if (tierConfig.window) {
|
|
290
|
+
effectiveWindowMs = parseWindow2(tierConfig.window);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (effectiveLimit === Infinity) {
|
|
295
|
+
return handler(req, server);
|
|
296
|
+
}
|
|
297
|
+
const result = await store.hit(key, effectiveWindowMs, effectiveLimit);
|
|
298
|
+
const allowed = result.count <= effectiveLimit;
|
|
299
|
+
if (!allowed) {
|
|
300
|
+
const headers = { "Content-Type": "application/json" };
|
|
301
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
302
|
+
if (standardHeaders) {
|
|
303
|
+
headers["RateLimit-Limit"] = String(effectiveLimit);
|
|
304
|
+
headers["RateLimit-Remaining"] = "0";
|
|
305
|
+
headers["RateLimit-Reset"] = String(resetIn);
|
|
306
|
+
}
|
|
307
|
+
if (legacyHeaders) {
|
|
308
|
+
headers["X-RateLimit-Limit"] = String(effectiveLimit);
|
|
309
|
+
headers["X-RateLimit-Remaining"] = "0";
|
|
310
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
311
|
+
}
|
|
312
|
+
if (retryAfterHeader) {
|
|
313
|
+
headers["Retry-After"] = String(resetIn);
|
|
314
|
+
}
|
|
315
|
+
return new Response(blockedBody, { status: 429, headers });
|
|
316
|
+
}
|
|
317
|
+
const res = await handler(req, server);
|
|
318
|
+
if (standardHeaders || legacyHeaders) {
|
|
319
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
320
|
+
const remaining = Math.max(0, effectiveLimit - result.count);
|
|
321
|
+
const newHeaders = new Headers(res.headers);
|
|
322
|
+
if (standardHeaders) {
|
|
323
|
+
newHeaders.set("RateLimit-Limit", String(effectiveLimit));
|
|
324
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
325
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
326
|
+
}
|
|
327
|
+
if (legacyHeaders) {
|
|
328
|
+
newHeaders.set("X-RateLimit-Limit", String(effectiveLimit));
|
|
329
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
330
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
331
|
+
}
|
|
332
|
+
return new Response(res.body, {
|
|
333
|
+
status: res.status,
|
|
334
|
+
statusText: res.statusText,
|
|
335
|
+
headers: newHeaders
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return res;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const action = await config.onStoreError(error, req);
|
|
341
|
+
if (action === "deny") {
|
|
342
|
+
return new Response('{"hitlimit":true,"message":"Rate limit error"}', {
|
|
343
|
+
status: 429,
|
|
344
|
+
headers: { "Content-Type": "application/json" }
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return handler(req, server);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function createHitLimit(options = {}) {
|
|
352
|
+
const store = options.store ?? sqliteStore({ path: options.sqlitePath });
|
|
353
|
+
const config = resolveConfig(options, store, () => "unknown");
|
|
354
|
+
const hasSkip = !!config.skip;
|
|
355
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
356
|
+
const standardHeaders = config.headers.standard;
|
|
357
|
+
const legacyHeaders = config.headers.legacy;
|
|
358
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
359
|
+
const limit = config.limit;
|
|
360
|
+
const windowMs = config.windowMs;
|
|
361
|
+
const response = config.response;
|
|
362
|
+
const customKey = options.key;
|
|
363
|
+
const blockedBody = JSON.stringify(response);
|
|
364
|
+
return {
|
|
365
|
+
async check(req, server) {
|
|
366
|
+
if (hasSkip) {
|
|
367
|
+
const shouldSkip = await config.skip(req);
|
|
368
|
+
if (shouldSkip) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const key = customKey ? await customKey(req) : getDefaultKey(req, server);
|
|
374
|
+
let effectiveLimit = limit;
|
|
375
|
+
let effectiveWindowMs = windowMs;
|
|
376
|
+
if (hasTiers) {
|
|
377
|
+
const tierName = await config.tier(req);
|
|
378
|
+
const tierConfig = config.tiers[tierName];
|
|
379
|
+
if (tierConfig) {
|
|
380
|
+
effectiveLimit = tierConfig.limit;
|
|
381
|
+
if (tierConfig.window) {
|
|
382
|
+
effectiveWindowMs = parseWindow2(tierConfig.window);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (effectiveLimit === Infinity) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const result = await store.hit(key, effectiveWindowMs, effectiveLimit);
|
|
390
|
+
const allowed = result.count <= effectiveLimit;
|
|
391
|
+
if (!allowed) {
|
|
392
|
+
const headers = { "Content-Type": "application/json" };
|
|
393
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
394
|
+
if (standardHeaders) {
|
|
395
|
+
headers["RateLimit-Limit"] = String(effectiveLimit);
|
|
396
|
+
headers["RateLimit-Remaining"] = "0";
|
|
397
|
+
headers["RateLimit-Reset"] = String(resetIn);
|
|
398
|
+
}
|
|
399
|
+
if (legacyHeaders) {
|
|
400
|
+
headers["X-RateLimit-Limit"] = String(effectiveLimit);
|
|
401
|
+
headers["X-RateLimit-Remaining"] = "0";
|
|
402
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
403
|
+
}
|
|
404
|
+
if (retryAfterHeader) {
|
|
405
|
+
headers["Retry-After"] = String(resetIn);
|
|
406
|
+
}
|
|
407
|
+
return new Response(blockedBody, { status: 429, headers });
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const action = await config.onStoreError(error, req);
|
|
412
|
+
if (action === "deny") {
|
|
413
|
+
return new Response('{"hitlimit":true,"message":"Rate limit error"}', {
|
|
414
|
+
status: 429,
|
|
415
|
+
headers: { "Content-Type": "application/json" }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
reset(key) {
|
|
422
|
+
return store.reset(key);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function parseWindow2(window) {
|
|
427
|
+
if (typeof window === "number")
|
|
428
|
+
return window;
|
|
429
|
+
const match = window.match(/^(\d+)(ms|s|m|h|d)$/);
|
|
430
|
+
if (!match)
|
|
431
|
+
return 60000;
|
|
432
|
+
const value = parseInt(match[1], 10);
|
|
433
|
+
const unit = match[2];
|
|
434
|
+
switch (unit) {
|
|
435
|
+
case "ms":
|
|
436
|
+
return value;
|
|
437
|
+
case "s":
|
|
438
|
+
return value * 1000;
|
|
439
|
+
case "m":
|
|
440
|
+
return value * 60 * 1000;
|
|
441
|
+
case "h":
|
|
442
|
+
return value * 60 * 60 * 1000;
|
|
443
|
+
case "d":
|
|
444
|
+
return value * 24 * 60 * 60 * 1000;
|
|
445
|
+
default:
|
|
446
|
+
return 60000;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
export {
|
|
450
|
+
sqliteStore,
|
|
451
|
+
hitlimit,
|
|
452
|
+
createHitLimit,
|
|
453
|
+
checkLimit,
|
|
454
|
+
DEFAULT_WINDOW_MS,
|
|
455
|
+
DEFAULT_WINDOW,
|
|
456
|
+
DEFAULT_MESSAGE,
|
|
457
|
+
DEFAULT_LIMIT
|
|
458
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../src/loggers/console.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE/D,wBAAgB,aAAa,IAAI,cAAc,CAO9C;AAED,wBAAgB,YAAY,IAAI,cAAc,CAO7C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AA+D3E,wBAAgB,WAAW,IAAI,aAAa,CAE3C"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
|
+
var __require = import.meta.require;
|
|
30
|
+
|
|
31
|
+
// src/stores/memory.ts
|
|
32
|
+
class MemoryStore {
|
|
33
|
+
hits = new Map;
|
|
34
|
+
hit(key, windowMs, _limit) {
|
|
35
|
+
const entry = this.hits.get(key);
|
|
36
|
+
if (entry !== undefined) {
|
|
37
|
+
entry.count++;
|
|
38
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
39
|
+
}
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const resetAt = now + windowMs;
|
|
42
|
+
const timeoutId = setTimeout(() => {
|
|
43
|
+
this.hits.delete(key);
|
|
44
|
+
}, windowMs);
|
|
45
|
+
if (typeof timeoutId.unref === "function") {
|
|
46
|
+
timeoutId.unref();
|
|
47
|
+
}
|
|
48
|
+
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
49
|
+
return { count: 1, resetAt };
|
|
50
|
+
}
|
|
51
|
+
reset(key) {
|
|
52
|
+
const entry = this.hits.get(key);
|
|
53
|
+
if (entry) {
|
|
54
|
+
clearTimeout(entry.timeoutId);
|
|
55
|
+
this.hits.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
shutdown() {
|
|
59
|
+
for (const [, entry] of this.hits) {
|
|
60
|
+
clearTimeout(entry.timeoutId);
|
|
61
|
+
}
|
|
62
|
+
this.hits.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function memoryStore() {
|
|
66
|
+
return new MemoryStore;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
memoryStore
|
|
70
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA2CD,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAErE"}
|