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.
@@ -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
+ }
@@ -98,9 +98,17 @@ module.exports = (config = {}) => {
98
98
 
99
99
  if (hasParams) {
100
100
  req.params = req.params || {}
101
- // Direct property copy - faster than Object.keys() + loop
101
+ // Secure property copy with prototype pollution protection
102
102
  for (const key in params) {
103
- req.params[key] = params[key]
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.1.3",
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",