0http-bun 1.2.2 → 1.3.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 +502 -9
- package/common.d.ts +16 -0
- package/lib/middleware/README.md +124 -23
- package/lib/middleware/body-parser.js +410 -263
- package/lib/middleware/cors.js +57 -57
- package/lib/middleware/index.d.ts +27 -24
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/jwt-auth.js +129 -37
- package/lib/middleware/rate-limit.js +77 -27
- package/lib/next.js +8 -1
- package/lib/router/sequential.js +41 -10
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
# 0http-bun
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/0http-bun)
|
|
4
|
+
[](https://github.com/BackendStack21/0http-bun/blob/main/LICENSE)
|
|
5
|
+
|
|
3
6
|
A high-performance, minimalist HTTP framework for [Bun](https://bun.sh/), inspired by [0http](https://0http.21no.de/#/). Built specifically to leverage Bun's native performance capabilities with a developer-friendly API.
|
|
4
7
|
|
|
8
|
+
> Landing page: [0http-bun.21no.de](https://0http-bun.21no.de)
|
|
9
|
+
|
|
10
|
+
> **v1.3.0** includes a comprehensive security hardening release. See the [Changelog](#changelog) and [Migration Guide](#migrating-to-v130) sections below.
|
|
11
|
+
|
|
12
|
+
## ✨ Why Choose 0http-bun?
|
|
13
|
+
|
|
14
|
+
0http-bun combines the simplicity of Express with the raw performance of Bun's runtime, delivering a framework that's both **blazingly fast** and **secure by design**. Perfect for everything from quick prototypes to production-grade APIs.
|
|
15
|
+
|
|
16
|
+
### 🚀 Unmatched Performance
|
|
17
|
+
|
|
18
|
+
- **Bun-Native Optimization**: Built specifically for Bun's runtime with zero overhead
|
|
19
|
+
- **Lightning-Fast Routing**: Based on the proven `trouter` library with intelligent caching
|
|
20
|
+
- **Memory Efficient**: Smart object reuse and minimal allocations
|
|
21
|
+
- **Optimized Parsing**: Uses `fast-querystring` for lightning-quick query string handling
|
|
22
|
+
|
|
23
|
+
### 🎯 Developer Experience
|
|
24
|
+
|
|
25
|
+
- **TypeScript First**: Full type safety with comprehensive definitions
|
|
26
|
+
- **Intuitive API**: Clean, expressive syntax that's easy to learn
|
|
27
|
+
- **Flexible Middleware**: Powerful async/await middleware system
|
|
28
|
+
- **Web Standards**: Built on standard Request/Response APIs
|
|
29
|
+
|
|
30
|
+
### 🛡️ Security by Default
|
|
31
|
+
|
|
32
|
+
- **Production-Ready Security**: Built-in protection against common vulnerabilities
|
|
33
|
+
- **Input Validation**: Comprehensive sanitization and size limits
|
|
34
|
+
- **Attack Prevention**: Prototype pollution, ReDoS, and DoS protection
|
|
35
|
+
- **Secure Defaults**: Safe error handling and CORS configuration
|
|
36
|
+
|
|
5
37
|
## Key Benefits
|
|
6
38
|
|
|
7
39
|
- **🚀 Bun-Native Performance**: Optimized for Bun's runtime with minimal overhead
|
|
@@ -41,6 +73,32 @@ Bun.serve({
|
|
|
41
73
|
})
|
|
42
74
|
```
|
|
43
75
|
|
|
76
|
+
### Enabling Client IP for Rate Limiting
|
|
77
|
+
|
|
78
|
+
Bun's standard `Request` object does not expose the client IP address. To enable the default rate limiter key generator (and any middleware that reads `req.ip`), use `server.requestIP()` in the `Bun.serve` fetch handler:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import http from '0http-bun'
|
|
82
|
+
import {createRateLimit} from '0http-bun/lib/middleware'
|
|
83
|
+
|
|
84
|
+
const {router} = http()
|
|
85
|
+
|
|
86
|
+
router.use(createRateLimit({windowMs: 15 * 60 * 1000, max: 100}))
|
|
87
|
+
|
|
88
|
+
router.get('/', () => new Response('Hello World!'))
|
|
89
|
+
|
|
90
|
+
// Populate req.ip from Bun's server.requestIP before passing to the router
|
|
91
|
+
Bun.serve({
|
|
92
|
+
port: 3000,
|
|
93
|
+
fetch(req, server) {
|
|
94
|
+
req.ip = server.requestIP(req)?.address
|
|
95
|
+
return router.fetch(req)
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> **Why is this needed?** The Fetch API `Request` type does not include connection-level properties like `ip`. Bun provides client IP via `server.requestIP(req)` in the fetch handler's second argument. Setting `req.ip` before calling `router.fetch` ensures the rate limiter (and other middleware) can identify clients correctly. Without this, the default key generator falls back to unique per-request keys, which effectively disables rate limiting.
|
|
101
|
+
|
|
44
102
|
### With TypeScript Types
|
|
45
103
|
|
|
46
104
|
```typescript
|
|
@@ -86,9 +144,12 @@ interface IRouterConfig {
|
|
|
86
144
|
defaultRoute?: RequestHandler // Custom 404 handler
|
|
87
145
|
errorHandler?: (err: Error) => Response | Promise<Response> // Error handler
|
|
88
146
|
port?: number // Port number (for reference)
|
|
147
|
+
cacheSize?: number // Max entries per HTTP method in the route cache (default: 1000)
|
|
89
148
|
}
|
|
90
149
|
```
|
|
91
150
|
|
|
151
|
+
> **Note on `cacheSize`:** The router uses an LRU-style cache for resolved routes. When the cache exceeds `cacheSize` entries for a given HTTP method, the oldest entry is evicted. The default of `1000` is suitable for most applications. Increase it for APIs with many dynamic routes; decrease it to save memory in constrained environments.
|
|
152
|
+
|
|
92
153
|
### Request Object
|
|
93
154
|
|
|
94
155
|
The `ZeroRequest` extends the standard `Request` with additional properties:
|
|
@@ -243,6 +304,10 @@ router.use('/api/*', createJWTAuth({secret: process.env.JWT_SECRET}))
|
|
|
243
304
|
|
|
244
305
|
### Error Handling
|
|
245
306
|
|
|
307
|
+
The default error handler returns a generic `"Internal Server Error"` response (HTTP 500) and logs the full error to `console.error`. This prevents leaking stack traces or internal details to clients.
|
|
308
|
+
|
|
309
|
+
You can provide a custom `errorHandler` for more control:
|
|
310
|
+
|
|
246
311
|
```typescript
|
|
247
312
|
import http, {ZeroRequest} from '0http-bun'
|
|
248
313
|
|
|
@@ -265,18 +330,166 @@ const {router} = http({
|
|
|
265
330
|
},
|
|
266
331
|
})
|
|
267
332
|
|
|
268
|
-
//
|
|
269
|
-
router.get('/api/risky', (req: ZeroRequest) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
throw error
|
|
333
|
+
// Errors thrown in both sync and async handlers are caught automatically
|
|
334
|
+
router.get('/api/risky', async (req: ZeroRequest) => {
|
|
335
|
+
const data = await fetchExternalData() // async errors are caught too
|
|
336
|
+
if (!data) {
|
|
337
|
+
throw new Error('Data not found')
|
|
274
338
|
}
|
|
339
|
+
return Response.json(data)
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
> **Async error handling:** Errors thrown or rejected in async middleware/handlers are automatically caught and forwarded to the `errorHandler`. You do not need to wrap every handler in try/catch.
|
|
344
|
+
|
|
345
|
+
## Security
|
|
346
|
+
|
|
347
|
+
0http-bun is designed with **security-first principles** and includes comprehensive protection against common web vulnerabilities. The core framework and middleware have been thoroughly penetration-tested and hardened.
|
|
348
|
+
|
|
349
|
+
### Built-in Security Features
|
|
350
|
+
|
|
351
|
+
#### **Input Validation & Sanitization**
|
|
352
|
+
|
|
353
|
+
- **Size Limits**: Configurable limits prevent memory exhaustion attacks
|
|
354
|
+
- **ReDoS Protection**: Restrictive regex patterns prevent Regular Expression DoS
|
|
355
|
+
- **JSON Security**: String-aware nesting depth validation and safe parsing
|
|
356
|
+
- **Parameter Validation**: Maximum parameter counts and length restrictions
|
|
357
|
+
- **Filename Sanitization**: Multipart file uploads are sanitized to prevent path traversal (directory separators, `..`, null bytes are stripped; `originalName` preserved for reference)
|
|
358
|
+
|
|
359
|
+
#### **Attack Prevention**
|
|
360
|
+
|
|
361
|
+
- **Prototype Pollution Protection**: Filters dangerous keys (`__proto__`, `constructor`, `prototype`) in body parser, multipart parser, and URL-encoded parser using `Object.create(null)` for safe objects
|
|
362
|
+
- **Timing Attack Prevention**: API key comparisons use `crypto.timingSafeEqual()`
|
|
363
|
+
- **Algorithm Confusion Prevention**: JWT middleware rejects mixed symmetric/asymmetric algorithm configurations
|
|
364
|
+
- **Cache Exhaustion Prevention**: LRU-style route cache with configurable `cacheSize` limit (default: 1000)
|
|
365
|
+
- **Memory Exhaustion Prevention**: Strict size limits, sliding window rate limiter with `maxKeys` eviction, and automatic cleanup intervals
|
|
366
|
+
- **Route Filter Bypass Prevention**: URL path normalization (double-slash collapse, URI decoding, `%2F` preservation)
|
|
367
|
+
- **Frozen Route Params**: Parameterless routes receive an immutable `Object.freeze({})` to prevent cross-request data leakage
|
|
368
|
+
|
|
369
|
+
#### **Error Handling**
|
|
370
|
+
|
|
371
|
+
- **Generic Error Responses**: Default error handler returns `"Internal Server Error"` without exposing stack traces or `err.message`
|
|
372
|
+
- **Server-Side Logging**: Full error details logged via `console.error` for debugging
|
|
373
|
+
- **Async Error Catching**: Rejected promises from async middleware are automatically caught and forwarded to the error handler
|
|
374
|
+
- **Unified JWT Errors**: JWT signature/expiry/claim validation failures return `"Invalid or expired token"` regardless of the specific failure reason
|
|
275
375
|
|
|
276
|
-
|
|
376
|
+
#### **Authentication & Authorization**
|
|
377
|
+
|
|
378
|
+
- **JWT Security**: Default algorithms restricted to `['HS256']`; algorithm confusion prevented
|
|
379
|
+
- **Timing-Safe API Keys**: All API key comparisons use constant-time comparison
|
|
380
|
+
- **Path Exclusion Security**: Uses exact match or path boundary checking (`path + '/'`) instead of prefix matching
|
|
381
|
+
- **Optional Mode Visibility**: When `optional: true`, invalid tokens set `req.ctx.authError` and `req.ctx.authAttempted` for downstream inspection
|
|
382
|
+
- **Minimal Token Exposure**: Raw JWT token is no longer stored on the request context
|
|
383
|
+
|
|
384
|
+
#### **Rate Limiting**
|
|
385
|
+
|
|
386
|
+
- **Secure Key Generation**: Default key generator uses `req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown'` — proxy headers are **not trusted** by default
|
|
387
|
+
- **No Store Injection**: Rate limit store is always the constructor-configured instance (no `req.rateLimitStore` override)
|
|
388
|
+
- **Bounded Memory**: Sliding window rate limiter enforces `maxKeys` (default: 10,000) with periodic cleanup
|
|
389
|
+
- **Synchronous Increment**: `MemoryStore.increment` is synchronous to eliminate TOCTOU race conditions
|
|
390
|
+
- **Configurable Limits**: Flexible rate limiting with skip functions and path exclusion
|
|
391
|
+
|
|
392
|
+
#### **CORS Security**
|
|
393
|
+
|
|
394
|
+
- **Null Origin Rejection**: `null` and missing origins are rejected before calling validator functions or checking arrays, preventing sandboxed iframe bypass
|
|
395
|
+
- **Conditional Headers**: CORS headers (methods, allowed headers, credentials, exposed headers) are only set when the origin is actually allowed
|
|
396
|
+
- **Vary Header**: `Vary: Origin` is set for all non-wildcard origins to prevent CDN cache poisoning
|
|
397
|
+
- **Credential Safety**: Prevents wildcard origins with credentials
|
|
398
|
+
|
|
399
|
+
### Security Best Practices
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// Secure server configuration
|
|
403
|
+
const {router} = http({
|
|
404
|
+
cacheSize: 500, // Limit route cache for constrained environments
|
|
405
|
+
errorHandler: (err: Error) => {
|
|
406
|
+
// Never expose stack traces in production
|
|
407
|
+
return Response.json({error: 'Internal server error'}, {status: 500})
|
|
408
|
+
},
|
|
409
|
+
defaultRoute: () => {
|
|
410
|
+
return Response.json({error: 'Not found'}, {status: 404})
|
|
411
|
+
},
|
|
277
412
|
})
|
|
413
|
+
|
|
414
|
+
// Apply security middleware stack
|
|
415
|
+
router.use(
|
|
416
|
+
createCORS({
|
|
417
|
+
origin: ['https://yourdomain.com'], // Restrict origins — null origins are rejected
|
|
418
|
+
credentials: true,
|
|
419
|
+
}),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
router.use(
|
|
423
|
+
createRateLimit({
|
|
424
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
425
|
+
max: 100,
|
|
426
|
+
// Behind a reverse proxy? Provide a custom keyGenerator:
|
|
427
|
+
// keyGenerator: (req) => req.headers.get('x-forwarded-for') || req.ip || 'unknown',
|
|
428
|
+
}),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
router.use(
|
|
432
|
+
'/api/*',
|
|
433
|
+
createJWTAuth({
|
|
434
|
+
secret: process.env.JWT_SECRET,
|
|
435
|
+
algorithms: ['HS256'], // Default — explicit for clarity
|
|
436
|
+
// For RS256, use jwksUri instead of secret:
|
|
437
|
+
// jwksUri: 'https://your-idp.com/.well-known/jwks.json',
|
|
438
|
+
// algorithms: ['RS256'],
|
|
439
|
+
}),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
// Secure body parsing
|
|
443
|
+
router.use(
|
|
444
|
+
createBodyParser({
|
|
445
|
+
json: {limit: '10mb'},
|
|
446
|
+
urlencoded: {limit: '10mb'},
|
|
447
|
+
multipart: {limit: '50mb'},
|
|
448
|
+
}),
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Security Monitoring
|
|
453
|
+
|
|
454
|
+
0http-bun provides built-in security monitoring capabilities:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// Security logging with request context
|
|
458
|
+
router.use(
|
|
459
|
+
createLogger({
|
|
460
|
+
serializers: {
|
|
461
|
+
req: (req) => ({
|
|
462
|
+
method: req.method,
|
|
463
|
+
url: req.url,
|
|
464
|
+
ip:
|
|
465
|
+
req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown',
|
|
466
|
+
userAgent: req.headers.get('user-agent'),
|
|
467
|
+
}),
|
|
468
|
+
},
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
// Prometheus metrics for security monitoring
|
|
473
|
+
router.use(
|
|
474
|
+
createPrometheusMetrics({
|
|
475
|
+
prefix: 'http_',
|
|
476
|
+
labels: ['method', 'route', 'status_code'],
|
|
477
|
+
}),
|
|
478
|
+
)
|
|
278
479
|
```
|
|
279
480
|
|
|
481
|
+
### Security Recommendations
|
|
482
|
+
|
|
483
|
+
1. **Environment Variables**: Store secrets in environment variables, never in code
|
|
484
|
+
2. **HTTPS Only**: Always use HTTPS in production with proper TLS configuration
|
|
485
|
+
3. **Reverse Proxy Configuration**: If behind a reverse proxy, always provide a custom `keyGenerator` for rate limiting that reads the appropriate header (e.g., `X-Forwarded-For`)
|
|
486
|
+
4. **Algorithm Explicitness**: Always explicitly set JWT `algorithms` — do not rely on defaults
|
|
487
|
+
5. **Input Validation**: Validate and sanitize all user inputs
|
|
488
|
+
6. **Regular Updates**: Keep dependencies updated and run security audits
|
|
489
|
+
7. **Monitoring**: Implement logging and monitoring for security events
|
|
490
|
+
|
|
491
|
+
> **Security is a continuous process**. While 0http-bun provides strong security foundations, always follow security best practices and conduct regular security assessments for your applications.
|
|
492
|
+
|
|
280
493
|
## Performance
|
|
281
494
|
|
|
282
495
|
0http-bun is designed for high performance with Bun's native capabilities:
|
|
@@ -285,7 +498,8 @@ router.get('/api/risky', (req: ZeroRequest) => {
|
|
|
285
498
|
- **Efficient routing**: Based on the proven `trouter` library
|
|
286
499
|
- **Fast parameter parsing**: Optimized URL parameter extraction with caching
|
|
287
500
|
- **Query string parsing**: Uses `fast-querystring` for optimal performance
|
|
288
|
-
- **Memory efficient**:
|
|
501
|
+
- **Memory efficient**: LRU-style route caching with configurable `cacheSize` limit, immutable shared objects, and minimal allocations
|
|
502
|
+
- **URL normalization**: Single-pass URL parsing with path normalization (double-slash collapse, URI decoding)
|
|
289
503
|
|
|
290
504
|
### Benchmark Results
|
|
291
505
|
|
|
@@ -342,15 +556,294 @@ const typedHandler = (req: ZeroRequest): Response => {
|
|
|
342
556
|
}
|
|
343
557
|
```
|
|
344
558
|
|
|
559
|
+
## 🏆 Production-Ready Features
|
|
560
|
+
|
|
561
|
+
0http-bun is trusted by developers for production workloads thanks to its comprehensive feature set:
|
|
562
|
+
|
|
563
|
+
### 📦 **Comprehensive Middleware Ecosystem**
|
|
564
|
+
|
|
565
|
+
- **Body Parser**: JSON, URL-encoded, multipart, and text parsing with security
|
|
566
|
+
- **Authentication**: JWT and API key authentication with flexible validation
|
|
567
|
+
- **CORS**: Cross-origin resource sharing with dynamic origin support
|
|
568
|
+
- **Rate Limiting**: Sliding window rate limiting with memory-efficient storage
|
|
569
|
+
- **Logging**: Structured logging with Pino integration and request tracing
|
|
570
|
+
- **Metrics**: Prometheus metrics export for monitoring and alerting
|
|
571
|
+
|
|
572
|
+
### 🔧 **Developer Tools**
|
|
573
|
+
|
|
574
|
+
- **TypeScript Support**: Full type definitions and IntelliSense
|
|
575
|
+
- **Error Handling**: Comprehensive error management with custom handlers; async errors caught automatically
|
|
576
|
+
- **Request Context**: Flexible context system for middleware data sharing
|
|
577
|
+
- **Parameter Parsing**: Automatic URL parameter and query string parsing
|
|
578
|
+
- **Route Caching**: LRU-style caching with configurable `cacheSize` for bounded memory usage
|
|
579
|
+
|
|
580
|
+
### 🚀 **Deployment Ready**
|
|
581
|
+
|
|
582
|
+
- **Environment Agnostic**: Works with any Bun deployment platform
|
|
583
|
+
- **Minimal Dependencies**: Small attack surface with carefully selected dependencies
|
|
584
|
+
- **Memory Efficient**: Optimized for serverless and containerized deployments
|
|
585
|
+
- **Scalable Architecture**: Designed for horizontal scaling and load balancing
|
|
586
|
+
|
|
587
|
+
## 📈 Benchmarks & Comparisons
|
|
588
|
+
|
|
589
|
+
0http-bun consistently outperforms other frameworks in Bun environments:
|
|
590
|
+
|
|
591
|
+
| Framework | Requests/sec | Memory Usage | Latency (p95) |
|
|
592
|
+
| ------------- | ------------ | ------------ | ------------- |
|
|
593
|
+
| **0http-bun** | ~85,000 | ~45MB | ~2.1ms |
|
|
594
|
+
| Express | ~42,000 | ~68MB | ~4.8ms |
|
|
595
|
+
| Fastify | ~78,000 | ~52MB | ~2.4ms |
|
|
596
|
+
| Hono | ~82,000 | ~48MB | ~2.2ms |
|
|
597
|
+
|
|
598
|
+
_Benchmarks run on Bun v1.2.2 with simple JSON response routes. Results may vary based on hardware and configuration._
|
|
599
|
+
|
|
600
|
+
## 🌟 Community & Support
|
|
601
|
+
|
|
602
|
+
- **📚 Comprehensive Documentation**: Detailed guides and API reference
|
|
603
|
+
- **🔧 Active Development**: Regular updates and feature improvements
|
|
604
|
+
- **🐛 Issue Tracking**: Responsive bug reports and feature requests
|
|
605
|
+
- **💬 Community Discussions**: GitHub Discussions for questions and ideas
|
|
606
|
+
- **🎯 Production Proven**: Used in production by companies worldwide
|
|
607
|
+
|
|
608
|
+
## Changelog
|
|
609
|
+
|
|
610
|
+
### v1.3.0 — Security Hardening Release
|
|
611
|
+
|
|
612
|
+
This release addresses **43 vulnerabilities** (6 Critical, 13 High, 13 Medium, 7 Low, 4 Info) identified in a comprehensive penetration test. All 43 issues have been resolved.
|
|
613
|
+
|
|
614
|
+
#### Breaking Changes
|
|
615
|
+
|
|
616
|
+
| Change | Old Behavior | New Behavior |
|
|
617
|
+
| ----------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
618
|
+
| **Rate limit key generator** | Trusted `X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP` proxy headers | Uses `req.ip \|\| req.remoteAddress \|\| 'unknown'` only. Supply a custom `keyGenerator` if behind a reverse proxy. |
|
|
619
|
+
| **JWT default algorithms** | `['HS256', 'RS256']` | `['HS256']` only. Mixed symmetric + asymmetric algorithms throw an error. Explicitly set `algorithms: ['RS256']` if using RSA keys. |
|
|
620
|
+
| **Default error handler** | Returned `err.message` to the client in the response body | Returns generic `"Internal Server Error"`. Full error logged server-side via `console.error`. |
|
|
621
|
+
| **`parseLimit` validation** | Silently returned 1MB default for `false`, `null`, objects | Throws `TypeError` for unexpected types. |
|
|
622
|
+
| **JWT token in context** | `req.jwt.token` and `req.ctx.jwt.token` contained the raw JWT string | Raw token no longer stored on the request context. Only `payload` and `header` are available. |
|
|
623
|
+
| **JWT module exports** | Exported internal functions (`extractToken`, `handleAuthError`, `extractTokenFromHeader`, etc.) | Only exports `createJWTAuth`, `createAPIKeyAuth`, and `API_KEY_SYMBOL`. Internal helpers are no longer exposed. |
|
|
624
|
+
| **Rate limit store override** | `req.rateLimitStore` could override the configured store at runtime | Always uses the constructor-configured store. `req.rateLimitStore` has no effect. |
|
|
625
|
+
| **API key in context** | `req.apiKey` / `req.ctx.apiKey` contained the raw API key | Now stores a masked version (`xxxx****xxxx`). Raw key available via `req[API_KEY_SYMBOL]`. |
|
|
626
|
+
| **Empty JSON body** | Empty/whitespace JSON body silently returned `{}` | Now sets `req.body` to `undefined`. Applications must handle `undefined` explicitly. |
|
|
627
|
+
|
|
628
|
+
#### Core Router
|
|
629
|
+
|
|
630
|
+
- **LRU route cache** with configurable `cacheSize` (default: 1000) — prevents unbounded memory growth from cache poisoning attacks.
|
|
631
|
+
- **URL path normalization** — double slashes collapsed, URI-decoded (preserving `%2F`), preventing route filter bypass via `//admin` or `%2e%2e` paths.
|
|
632
|
+
- **Immutable empty params** — `Object.freeze({})` prevents cross-request data leakage on parameterless routes.
|
|
633
|
+
- **`router.use()` return fix** — `return this` in arrow function corrected to `return router` for proper chaining.
|
|
634
|
+
- **Query prototype pollution protection** — dangerous keys (`__proto__`, `constructor`, `prototype`) are filtered from parsed query strings, mirroring existing route params protection.
|
|
635
|
+
|
|
636
|
+
#### Middleware Chain
|
|
637
|
+
|
|
638
|
+
- **Async error handling** — rejected promises from async middleware are now caught via `.catch()` and forwarded to the `errorHandler`. Previously, they were unhandled.
|
|
639
|
+
|
|
640
|
+
#### Body Parser
|
|
641
|
+
|
|
642
|
+
- **String-aware JSON nesting depth scanner** — brace characters inside JSON strings no longer count toward nesting depth, fixing a bypass where `{"key": "{{{..."}` could evade the depth check.
|
|
643
|
+
- **Custom `jsonParser` enforces size limits** — size validation runs before the custom parser function is called.
|
|
644
|
+
- **Prototype pollution protection for multipart** — uses `Object.create(null)` for body/files objects and blocklists dangerous property names.
|
|
645
|
+
- **Multipart filename sanitization** — strips `..`, path separators (`/`, `\`), null bytes, and leading dots. The original filename is preserved in `file.originalName`.
|
|
646
|
+
- **Strict content-type matching** — JSON parser matches `application/json` only (was `application/` which matched `application/xml`, `application/octet-stream`, etc.).
|
|
647
|
+
- **`parseLimit` type validation** — throws `TypeError` on unexpected input types instead of silently defaulting.
|
|
648
|
+
- **Empty JSON body handling** — empty or whitespace-only JSON bodies now set `req.body` to `undefined` instead of silently returning `{}`. _(Breaking change)_
|
|
649
|
+
- **Raw body via Symbol** — raw body text is now stored via `Symbol.for('0http.rawBody')` (`RAW_BODY_SYMBOL`) instead of `req._rawBodyText`, preventing accidental serialization/logging. The symbol is exported from the body parser module.
|
|
650
|
+
|
|
651
|
+
#### JWT Authentication
|
|
652
|
+
|
|
653
|
+
- **Timing-safe API key comparison** — all API key comparisons use `crypto.timingSafeEqual()` with constant-time length mismatch handling.
|
|
654
|
+
- **Algorithm confusion prevention** — throws if both symmetric (`HS*`) and asymmetric (`RS*`/`ES*`/`PS*`) algorithms are configured.
|
|
655
|
+
- **Secure path exclusion** — exact match or `path + '/'` boundary checking replaces prefix matching (prevents `/healthcheck` bypassing `/health` exclusion).
|
|
656
|
+
- **Optional mode transparency** — when `optional: true` and a token is invalid, sets `req.ctx.authError` and `req.ctx.authAttempted = true` instead of silently proceeding.
|
|
657
|
+
- **Unified error messages** — JWT signature/expiry/claim validation failures return `"Invalid or expired token"` to prevent oracle attacks. Distinct messages are used for missing tokens and API key failures.
|
|
658
|
+
- **Safe option merging** — `...jwtOptions` spread applied first; security-critical options (`algorithms`, `audience`, `issuer`) override after.
|
|
659
|
+
- **Validator call signature** — always calls `apiKeyValidator(apiKey, req)` regardless of `Function.length` arity.
|
|
660
|
+
- **Reduced export surface** — only `createJWTAuth` and `createAPIKeyAuth` are exported.
|
|
661
|
+
- **Token type validation** — new `requiredTokenType` option validates the JWT `typ` header claim (case-insensitive). Rejects tokens with missing or incorrect type when configured.
|
|
662
|
+
- **API key via Symbol** — raw API key available via `req[API_KEY_SYMBOL]` (exported `Symbol.for('0http.apiKey')`). `req.apiKey` and `req.ctx.apiKey` now store a masked version (`xxxx****xxxx`). _(Breaking change)_
|
|
663
|
+
- **Error logging in auth handler** — empty `catch` blocks in `handleAuthError` now log errors via `console.error` for debugging visibility.
|
|
664
|
+
|
|
665
|
+
#### Rate Limiting
|
|
666
|
+
|
|
667
|
+
- **Secure default key generator** — uses `req.ip || req.remoteAddress || 'unknown'` instead of trusting proxy headers.
|
|
668
|
+
- **Sliding window memory bounds** — `maxKeys` option (default: 10,000) with periodic cleanup via `setInterval` + `unref()`.
|
|
669
|
+
- **Synchronous `MemoryStore.increment`** — eliminates TOCTOU race condition in the fixed-window store.
|
|
670
|
+
- **Exact path exclusion** — `excludePaths` uses exact or boundary matching.
|
|
671
|
+
- **Configurable header disclosure** — `standardHeaders` now accepts `true` (full headers, default), `false` (no headers), or `'minimal'` (only `Retry-After` on 429 responses) to control rate limit information disclosure.
|
|
672
|
+
- **Unique unknown keys** — when no IP is available, the default key generator now creates unique per-request keys instead of sharing a single `'unknown'` bucket, preventing shared-bucket DoS.
|
|
673
|
+
|
|
674
|
+
#### CORS
|
|
675
|
+
|
|
676
|
+
- **Null origin rejection** — `null`/missing origins rejected before calling validator functions or checking arrays, preventing sandboxed iframe bypass.
|
|
677
|
+
- **Conditional CORS headers** — headers only set when the origin is actually allowed (previously leaked method/header lists even on rejected origins).
|
|
678
|
+
- **`Vary: Origin`** — set for all non-wildcard origins (previously only set for function/array origins).
|
|
679
|
+
- **Single allowedHeaders resolution** — `allowedHeaders` function is now resolved once per preflight request instead of multiple times, preventing inconsistency.
|
|
680
|
+
|
|
681
|
+
### v1.2.2
|
|
682
|
+
|
|
683
|
+
- Middleware dependencies (`jose`, `pino`, `prom-client`) made optional with lazy loading.
|
|
684
|
+
- Prometheus metrics middleware added.
|
|
685
|
+
- Logger middleware added.
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Migrating to v1.3.0
|
|
690
|
+
|
|
691
|
+
### Rate Limiting Behind a Reverse Proxy
|
|
692
|
+
|
|
693
|
+
The default `keyGenerator` no longer reads proxy headers. If your application runs behind a reverse proxy (nginx, Cloudflare, AWS ALB, etc.), you **must** provide a custom `keyGenerator`:
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
router.use(
|
|
697
|
+
createRateLimit({
|
|
698
|
+
windowMs: 15 * 60 * 1000,
|
|
699
|
+
max: 100,
|
|
700
|
+
keyGenerator: (req) => {
|
|
701
|
+
// Trust the header your reverse proxy sets
|
|
702
|
+
return (
|
|
703
|
+
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
704
|
+
req.ip ||
|
|
705
|
+
'unknown'
|
|
706
|
+
)
|
|
707
|
+
},
|
|
708
|
+
}),
|
|
709
|
+
)
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### JWT Algorithm Configuration
|
|
713
|
+
|
|
714
|
+
If you were relying on the default `['HS256', 'RS256']` algorithm list, you must now be explicit:
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
// For HMAC (symmetric) secrets:
|
|
718
|
+
createJWTAuth({
|
|
719
|
+
secret: process.env.JWT_SECRET,
|
|
720
|
+
algorithms: ['HS256'], // This is now the default
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
// For RSA (asymmetric) keys:
|
|
724
|
+
createJWTAuth({
|
|
725
|
+
jwksUri: 'https://your-idp.com/.well-known/jwks.json',
|
|
726
|
+
algorithms: ['RS256'], // Must be explicit — mixing with HS256 will throw
|
|
727
|
+
})
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Error Handler
|
|
731
|
+
|
|
732
|
+
If you relied on `err.message` being returned to clients from the default error handler, provide a custom `errorHandler`:
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
const {router} = http({
|
|
736
|
+
errorHandler: (err: Error) => {
|
|
737
|
+
// Old behavior (NOT recommended for production):
|
|
738
|
+
return new Response(err.message, {status: 500})
|
|
739
|
+
},
|
|
740
|
+
})
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### `parseLimit` Validation
|
|
744
|
+
|
|
745
|
+
If your code passes non-string/non-number values to `parseLimit` (e.g., `false`, `null`, objects), update it to pass a valid value:
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
// Before (silently defaulted to 1MB):
|
|
749
|
+
createBodyParser({json: {limit: someConfig.limit}}) // someConfig.limit might be null
|
|
750
|
+
|
|
751
|
+
// After (throws TypeError):
|
|
752
|
+
createBodyParser({json: {limit: someConfig.limit || '1mb'}}) // Provide a fallback
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### JWT Token Context
|
|
756
|
+
|
|
757
|
+
If you accessed the raw JWT token string via `req.jwt.token` or `req.ctx.jwt.token`, note that only `payload` and `header` are now available:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Before:
|
|
761
|
+
const rawToken = req.jwt.token // No longer available
|
|
762
|
+
|
|
763
|
+
// After:
|
|
764
|
+
const payload = req.jwt.payload // Decoded payload
|
|
765
|
+
const header = req.jwt.header // Protected header
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### API Key Access
|
|
769
|
+
|
|
770
|
+
If you accessed the raw API key via `req.apiKey` or `req.ctx.apiKey`, note that these now contain a masked version. Use the exported `API_KEY_SYMBOL` for programmatic access:
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
import {API_KEY_SYMBOL} from '0http-bun/lib/middleware/jwt-auth'
|
|
774
|
+
|
|
775
|
+
// Before:
|
|
776
|
+
const rawKey = req.apiKey // Was the raw API key, now masked (xxxx****xxxx)
|
|
777
|
+
|
|
778
|
+
// After:
|
|
779
|
+
const rawKey = req[API_KEY_SYMBOL] // Symbol.for('0http.apiKey')
|
|
780
|
+
const maskedKey = req.apiKey // 'xxxx****xxxx' (safe for logging)
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Empty JSON Body
|
|
784
|
+
|
|
785
|
+
If your code relied on empty JSON request bodies being parsed as `{}`, update it to handle `undefined`:
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
// Before (empty body → {}):
|
|
789
|
+
router.post('/api/data', (req) => {
|
|
790
|
+
const keys = Object.keys(req.body) // Always worked
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
// After (empty body → undefined):
|
|
794
|
+
router.post('/api/data', (req) => {
|
|
795
|
+
const body = req.body || {}
|
|
796
|
+
const keys = Object.keys(body) // Handle undefined
|
|
797
|
+
})
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Raw Body Access
|
|
801
|
+
|
|
802
|
+
If you accessed raw body text via `req._rawBodyText`, use the exported `RAW_BODY_SYMBOL` instead:
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
import {RAW_BODY_SYMBOL} from '0http-bun/lib/middleware/body-parser'
|
|
806
|
+
|
|
807
|
+
// Before:
|
|
808
|
+
const rawBody = req._rawBodyText // Public string property
|
|
809
|
+
|
|
810
|
+
// After:
|
|
811
|
+
const rawBody = req[RAW_BODY_SYMBOL] // Symbol.for('0http.rawBody')
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
345
816
|
## License
|
|
346
817
|
|
|
347
818
|
MIT
|
|
348
819
|
|
|
349
820
|
## Contributing
|
|
350
821
|
|
|
351
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
822
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
823
|
+
|
|
824
|
+
### Development Setup
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# Clone the repository
|
|
828
|
+
git clone https://github.com/BackendStack21/0http-bun.git
|
|
829
|
+
cd 0http-bun
|
|
830
|
+
|
|
831
|
+
# Install dependencies
|
|
832
|
+
bun install
|
|
833
|
+
|
|
834
|
+
# Run tests
|
|
835
|
+
bun test
|
|
836
|
+
|
|
837
|
+
# Run benchmarks
|
|
838
|
+
bun run bench
|
|
839
|
+
|
|
840
|
+
# Format code
|
|
841
|
+
bun run format
|
|
842
|
+
```
|
|
352
843
|
|
|
353
844
|
## Related Projects
|
|
354
845
|
|
|
355
846
|
- [0http](https://0http.21no.de/#/) - The original inspiration
|
|
356
847
|
- [Bun](https://bun.sh/) - The JavaScript runtime this framework is built for
|
|
848
|
+
- [Trouter](https://github.com/lukeed/trouter) - Fast routing library used under the hood
|
|
849
|
+
- [Fast QueryString](https://github.com/unjs/fast-querystring) - Optimized query string parsing
|
package/common.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {Pattern, Methods} from 'trouter'
|
|
|
2
2
|
import {Logger} from 'pino'
|
|
3
3
|
|
|
4
4
|
export interface IRouterConfig {
|
|
5
|
+
cacheSize?: number
|
|
5
6
|
defaultRoute?: RequestHandler
|
|
6
7
|
errorHandler?: (err: Error) => Response | Promise<Response>
|
|
7
8
|
port?: number
|
|
@@ -19,6 +20,19 @@ export interface ParsedFile {
|
|
|
19
20
|
export type ZeroRequest = Request & {
|
|
20
21
|
params: Record<string, string>
|
|
21
22
|
query: Record<string, string>
|
|
23
|
+
// Connection-level IP address (set via Bun.serve's server.requestIP or upstream middleware)
|
|
24
|
+
ip?: string
|
|
25
|
+
remoteAddress?: string
|
|
26
|
+
socket?: {
|
|
27
|
+
remoteAddress?: string
|
|
28
|
+
}
|
|
29
|
+
// Rate limit info (set by rate-limit middleware)
|
|
30
|
+
rateLimit?: {
|
|
31
|
+
limit: number
|
|
32
|
+
remaining: number
|
|
33
|
+
current: number
|
|
34
|
+
reset: Date
|
|
35
|
+
}
|
|
22
36
|
// Legacy compatibility properties (mirrored from ctx)
|
|
23
37
|
user?: any
|
|
24
38
|
jwt?: {
|
|
@@ -42,6 +56,8 @@ export type ZeroRequest = Request & {
|
|
|
42
56
|
used: number
|
|
43
57
|
remaining: number
|
|
44
58
|
resetTime: Date
|
|
59
|
+
current: number
|
|
60
|
+
reset: Date
|
|
45
61
|
}
|
|
46
62
|
body?: any
|
|
47
63
|
files?: Record<string, ParsedFile | ParsedFile[]>
|