0http-bun 1.1.3 → 1.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 +72 -3
- package/common.d.ts +37 -2
- package/lib/middleware/README.md +758 -0
- package/lib/middleware/body-parser.js +783 -0
- package/lib/middleware/cors.js +225 -0
- package/lib/middleware/index.d.ts +236 -0
- package/lib/middleware/index.js +45 -0
- package/lib/middleware/jwt-auth.js +406 -0
- package/lib/middleware/logger.js +310 -0
- package/lib/middleware/rate-limit.js +306 -0
- package/lib/router/sequential.js +10 -2
- package/package.json +6 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory rate limiter implementation
|
|
3
|
+
* For production use, consider using Redis-based storage
|
|
4
|
+
*/
|
|
5
|
+
class MemoryStore {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.store = new Map()
|
|
8
|
+
this.resetTimes = new Map()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async increment(key, windowMs) {
|
|
12
|
+
const now = Date.now()
|
|
13
|
+
const windowStart = Math.floor(now / windowMs) * windowMs
|
|
14
|
+
const storeKey = `${key}:${windowStart}`
|
|
15
|
+
|
|
16
|
+
// Clean up old entries
|
|
17
|
+
this.cleanup(now)
|
|
18
|
+
|
|
19
|
+
const current = this.store.get(storeKey) || 0
|
|
20
|
+
const newValue = current + 1
|
|
21
|
+
|
|
22
|
+
this.store.set(storeKey, newValue)
|
|
23
|
+
this.resetTimes.set(storeKey, windowStart + windowMs)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
totalHits: newValue,
|
|
27
|
+
resetTime: new Date(windowStart + windowMs),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cleanup(now) {
|
|
32
|
+
for (const [key, resetTime] of this.resetTimes.entries()) {
|
|
33
|
+
if (now >= resetTime) {
|
|
34
|
+
this.store.delete(key)
|
|
35
|
+
this.resetTimes.delete(key)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async reset(key) {
|
|
41
|
+
for (const [storeKey] of this.store.entries()) {
|
|
42
|
+
if (storeKey.startsWith(key + ':')) {
|
|
43
|
+
this.store.delete(storeKey)
|
|
44
|
+
this.resetTimes.delete(storeKey)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a rate limiting middleware
|
|
52
|
+
* @param {Object} options - Rate limiter configuration
|
|
53
|
+
* @param {number} options.windowMs - Time window in milliseconds (default: 15 minutes)
|
|
54
|
+
* @param {number} options.max - Maximum number of requests per window (default: 100)
|
|
55
|
+
* @param {Function} options.keyGenerator - Function to generate rate limit key from request
|
|
56
|
+
* @param {Function} options.handler - Custom handler for rate limit exceeded
|
|
57
|
+
* @param {string} options.message - Custom message for rate limit exceeded (plain text)
|
|
58
|
+
* @param {Object} options.store - Custom store implementation
|
|
59
|
+
* @param {boolean} options.standardHeaders - Whether to send standard rate limit headers
|
|
60
|
+
* @param {Array<string>} options.excludePaths - Paths to exclude from rate limiting
|
|
61
|
+
* @param {Function} options.skip - Function to determine if request should be skipped
|
|
62
|
+
* @returns {Function} Middleware function
|
|
63
|
+
*/
|
|
64
|
+
function createRateLimit(options = {}) {
|
|
65
|
+
const {
|
|
66
|
+
windowMs = 15 * 60 * 1000, // 15 minutes
|
|
67
|
+
max = 100,
|
|
68
|
+
keyGenerator = defaultKeyGenerator,
|
|
69
|
+
handler = defaultHandler,
|
|
70
|
+
message,
|
|
71
|
+
store = new MemoryStore(),
|
|
72
|
+
standardHeaders = true,
|
|
73
|
+
excludePaths = [],
|
|
74
|
+
skip,
|
|
75
|
+
} = options
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Helper function to add rate limit headers to a response
|
|
79
|
+
* @param {Response} response - Response object to add headers to
|
|
80
|
+
* @param {number} totalHits - Current hit count
|
|
81
|
+
* @param {Date} resetTime - When the rate limit resets
|
|
82
|
+
* @returns {Response} Response with headers added
|
|
83
|
+
*/
|
|
84
|
+
const addRateLimitHeaders = (response, totalHits, resetTime) => {
|
|
85
|
+
if (standardHeaders && response && response.headers) {
|
|
86
|
+
response.headers.set('X-RateLimit-Limit', max.toString())
|
|
87
|
+
response.headers.set(
|
|
88
|
+
'X-RateLimit-Remaining',
|
|
89
|
+
Math.max(0, max - totalHits).toString(),
|
|
90
|
+
)
|
|
91
|
+
response.headers.set(
|
|
92
|
+
'X-RateLimit-Reset',
|
|
93
|
+
Math.ceil(resetTime.getTime() / 1000).toString(),
|
|
94
|
+
)
|
|
95
|
+
response.headers.set('X-RateLimit-Used', totalHits.toString())
|
|
96
|
+
}
|
|
97
|
+
return response
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return async function rateLimitMiddleware(req, next) {
|
|
101
|
+
// Allow test to inject a fresh store for isolation
|
|
102
|
+
const activeStore = req && req.rateLimitStore ? req.rateLimitStore : store
|
|
103
|
+
|
|
104
|
+
const url = new URL(req.url)
|
|
105
|
+
if (excludePaths.some((path) => url.pathname.startsWith(path))) {
|
|
106
|
+
return next()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check skip function first - if it returns true, completely bypass rate limiting
|
|
110
|
+
if (typeof skip === 'function' && skip(req)) {
|
|
111
|
+
return next()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const key = await keyGenerator(req)
|
|
116
|
+
const {totalHits, resetTime} = await activeStore.increment(key, windowMs)
|
|
117
|
+
|
|
118
|
+
if (totalHits > max) {
|
|
119
|
+
let response
|
|
120
|
+
|
|
121
|
+
// If a custom message is provided, use it as plain text
|
|
122
|
+
if (message) {
|
|
123
|
+
response = new Response(message, {status: 429})
|
|
124
|
+
} else {
|
|
125
|
+
response = await handler(req, totalHits, max, resetTime)
|
|
126
|
+
if (typeof response === 'string') {
|
|
127
|
+
response = new Response(response, {status: 429})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set rate limit context
|
|
135
|
+
req.ctx = req.ctx || {}
|
|
136
|
+
req.ctx.rateLimit = {
|
|
137
|
+
limit: max,
|
|
138
|
+
used: totalHits,
|
|
139
|
+
remaining: Math.max(0, max - totalHits),
|
|
140
|
+
resetTime,
|
|
141
|
+
current: totalHits,
|
|
142
|
+
reset: resetTime,
|
|
143
|
+
}
|
|
144
|
+
req.rateLimit = {
|
|
145
|
+
limit: max,
|
|
146
|
+
remaining: Math.max(0, max - totalHits),
|
|
147
|
+
current: totalHits,
|
|
148
|
+
reset: resetTime,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const response = await next()
|
|
152
|
+
if (response instanceof Response) {
|
|
153
|
+
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
154
|
+
}
|
|
155
|
+
return response
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// If key generation fails, fallback to a default key
|
|
158
|
+
try {
|
|
159
|
+
const key = 'unknown'
|
|
160
|
+
const {totalHits, resetTime} = await activeStore.increment(
|
|
161
|
+
key,
|
|
162
|
+
windowMs,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
req.ctx = req.ctx || {}
|
|
166
|
+
req.ctx.rateLimit = {
|
|
167
|
+
limit: max,
|
|
168
|
+
used: totalHits,
|
|
169
|
+
remaining: Math.max(0, max - totalHits),
|
|
170
|
+
resetTime,
|
|
171
|
+
current: totalHits,
|
|
172
|
+
reset: resetTime,
|
|
173
|
+
}
|
|
174
|
+
req.rateLimit = {
|
|
175
|
+
limit: max,
|
|
176
|
+
remaining: Math.max(0, max - totalHits),
|
|
177
|
+
current: totalHits,
|
|
178
|
+
reset: resetTime,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const response = await next()
|
|
182
|
+
if (response instanceof Response) {
|
|
183
|
+
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
184
|
+
}
|
|
185
|
+
return response
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return next()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Default key generator - uses IP address
|
|
195
|
+
* @param {Request} req - Request object
|
|
196
|
+
* @returns {string} Rate limit key
|
|
197
|
+
*/
|
|
198
|
+
function defaultKeyGenerator(req) {
|
|
199
|
+
// Try to get real IP from common headers
|
|
200
|
+
const forwarded = req.headers.get('x-forwarded-for')
|
|
201
|
+
const realIp = req.headers.get('x-real-ip')
|
|
202
|
+
const cfConnectingIp = req.headers.get('cf-connecting-ip')
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
cfConnectingIp ||
|
|
206
|
+
realIp ||
|
|
207
|
+
(forwarded && forwarded.split(',')[0].trim()) ||
|
|
208
|
+
'unknown'
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Default rate limit exceeded handler
|
|
214
|
+
* @param {Request} req - Request object
|
|
215
|
+
* @param {number} totalHits - Current hit count
|
|
216
|
+
* @param {number} max - Maximum allowed hits
|
|
217
|
+
* @param {Date} resetTime - When the rate limit resets
|
|
218
|
+
* @returns {Response} Response object
|
|
219
|
+
*/
|
|
220
|
+
function defaultHandler(req, totalHits, max, resetTime) {
|
|
221
|
+
const retryAfter = Math.ceil((resetTime.getTime() - Date.now()) / 1000)
|
|
222
|
+
|
|
223
|
+
return new Response(
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
error: 'Too many requests',
|
|
226
|
+
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
227
|
+
retryAfter,
|
|
228
|
+
}),
|
|
229
|
+
{
|
|
230
|
+
status: 429,
|
|
231
|
+
headers: {
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
'Retry-After': retryAfter.toString(),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Creates a sliding window rate limiter
|
|
241
|
+
* More precise than fixed window but uses more memory
|
|
242
|
+
* @param {Object} options - Rate limiter configuration
|
|
243
|
+
* @returns {Function} Middleware function
|
|
244
|
+
*/
|
|
245
|
+
function createSlidingWindowRateLimit(options = {}) {
|
|
246
|
+
const {
|
|
247
|
+
windowMs = 15 * 60 * 1000,
|
|
248
|
+
max = 100,
|
|
249
|
+
keyGenerator = defaultKeyGenerator,
|
|
250
|
+
handler = defaultHandler,
|
|
251
|
+
} = options
|
|
252
|
+
|
|
253
|
+
const requests = new Map() // key -> array of timestamps
|
|
254
|
+
|
|
255
|
+
return async function slidingWindowRateLimitMiddleware(req, next) {
|
|
256
|
+
try {
|
|
257
|
+
const key = await keyGenerator(req)
|
|
258
|
+
const now = Date.now()
|
|
259
|
+
|
|
260
|
+
// Get existing requests for this key
|
|
261
|
+
let userRequests = requests.get(key) || []
|
|
262
|
+
|
|
263
|
+
// Remove old requests outside the window
|
|
264
|
+
userRequests = userRequests.filter(
|
|
265
|
+
(timestamp) => now - timestamp < windowMs,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// Check if limit exceeded
|
|
269
|
+
if (userRequests.length >= max) {
|
|
270
|
+
const response = await handler(
|
|
271
|
+
req,
|
|
272
|
+
userRequests.length,
|
|
273
|
+
max,
|
|
274
|
+
new Date(now + windowMs),
|
|
275
|
+
)
|
|
276
|
+
return response
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add current request
|
|
280
|
+
userRequests.push(now)
|
|
281
|
+
requests.set(key, userRequests)
|
|
282
|
+
|
|
283
|
+
// Add rate limit info to context
|
|
284
|
+
req.ctx = req.ctx || {}
|
|
285
|
+
req.ctx.rateLimit = {
|
|
286
|
+
limit: max,
|
|
287
|
+
used: userRequests.length,
|
|
288
|
+
remaining: max - userRequests.length,
|
|
289
|
+
resetTime: new Date(userRequests[0] + windowMs),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return next()
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Sliding window rate limiting error:', error)
|
|
295
|
+
return next()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
createRateLimit,
|
|
302
|
+
createSlidingWindowRateLimit,
|
|
303
|
+
MemoryStore,
|
|
304
|
+
defaultKeyGenerator,
|
|
305
|
+
defaultHandler,
|
|
306
|
+
}
|
package/lib/router/sequential.js
CHANGED
|
@@ -98,9 +98,17 @@ module.exports = (config = {}) => {
|
|
|
98
98
|
|
|
99
99
|
if (hasParams) {
|
|
100
100
|
req.params = req.params || {}
|
|
101
|
-
//
|
|
101
|
+
// Secure property copy with prototype pollution protection
|
|
102
102
|
for (const key in params) {
|
|
103
|
-
|
|
103
|
+
// Prevent prototype pollution by filtering dangerous properties
|
|
104
|
+
if (
|
|
105
|
+
key !== '__proto__' &&
|
|
106
|
+
key !== 'constructor' &&
|
|
107
|
+
key !== 'prototype' &&
|
|
108
|
+
Object.prototype.hasOwnProperty.call(params, key)
|
|
109
|
+
) {
|
|
110
|
+
req.params[key] = params[key]
|
|
111
|
+
}
|
|
104
112
|
}
|
|
105
113
|
} else if (!req.params) {
|
|
106
114
|
req.params = emptyParams
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0http-bun",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "0http for Bun",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"fast-querystring": "^1.1.2",
|
|
14
|
-
"trouter": "^4.0.0"
|
|
14
|
+
"trouter": "^4.0.0",
|
|
15
|
+
"jose": "^6.0.11",
|
|
16
|
+
"pino": "^9.7.0"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -29,7 +31,8 @@
|
|
|
29
31
|
"0http-bun": "^1.1.2",
|
|
30
32
|
"bun-types": "^1.2.15",
|
|
31
33
|
"mitata": "^1.0.34",
|
|
32
|
-
"prettier": "^3.5.3"
|
|
34
|
+
"prettier": "^3.5.3",
|
|
35
|
+
"typescript": "^5.8.3"
|
|
33
36
|
},
|
|
34
37
|
"keywords": [
|
|
35
38
|
"http",
|