0http-bun 1.2.0 → 1.2.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 +3 -0
- package/lib/middleware/README.md +396 -5
- package/lib/middleware/index.d.ts +66 -2
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/jwt-auth.js +29 -11
- package/lib/middleware/logger.js +19 -4
- package/lib/middleware/prometheus.js +491 -0
- package/lib/middleware/rate-limit.js +71 -107
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -206,11 +206,14 @@ Bun.serve({
|
|
|
206
206
|
|
|
207
207
|
0http-bun includes a comprehensive middleware system with built-in middlewares for common use cases:
|
|
208
208
|
|
|
209
|
+
> 📦 **Note**: Starting with v1.2.2, some middleware dependencies are optional. Install only what you need: `jose` (JWT), `pino` (Logger), `prom-client` (Prometheus).
|
|
210
|
+
|
|
209
211
|
- **[Body Parser](./lib/middleware/README.md#body-parser)** - Automatic request body parsing (JSON, form data, text)
|
|
210
212
|
- **[CORS](./lib/middleware/README.md#cors)** - Cross-Origin Resource Sharing with flexible configuration
|
|
211
213
|
- **[JWT Authentication](./lib/middleware/README.md#jwt-authentication)** - JSON Web Token authentication and authorization
|
|
212
214
|
- **[Logger](./lib/middleware/README.md#logger)** - Request logging with multiple output formats
|
|
213
215
|
- **[Rate Limiting](./lib/middleware/README.md#rate-limiting)** - Flexible rate limiting with sliding window support
|
|
216
|
+
- **[Prometheus Metrics](./lib/middleware/README.md#prometheus-metrics)** - Export metrics for monitoring and alerting
|
|
214
217
|
|
|
215
218
|
### Quick Example
|
|
216
219
|
|
package/lib/middleware/README.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
0http-bun provides a comprehensive middleware system with built-in middlewares for common use cases. All middleware functions are TypeScript-ready and follow the standard middleware pattern.
|
|
4
4
|
|
|
5
|
+
## Dependency Installation
|
|
6
|
+
|
|
7
|
+
⚠️ **Important**: Starting with v1.2.2, middleware dependencies are now **optional** and must be installed separately when needed. This reduces the framework's footprint and improves startup performance through lazy loading.
|
|
8
|
+
|
|
9
|
+
Install only the dependencies you need:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# For JWT Authentication middleware
|
|
13
|
+
bun install jose
|
|
14
|
+
|
|
15
|
+
# For Logger middleware
|
|
16
|
+
bun install pino
|
|
17
|
+
|
|
18
|
+
# For Prometheus Metrics middleware
|
|
19
|
+
bun install prom-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Benefits of Lazy Loading:**
|
|
23
|
+
|
|
24
|
+
- 📦 **Smaller Bundle**: Only install what you use
|
|
25
|
+
- ⚡ **Faster Startup**: Dependencies loaded only when middleware is used
|
|
26
|
+
- 💾 **Lower Memory**: Reduced initial memory footprint
|
|
27
|
+
- 🔧 **Better Control**: Explicit dependency management
|
|
28
|
+
|
|
5
29
|
## Table of Contents
|
|
6
30
|
|
|
7
31
|
- [Middleware Pattern](#middleware-pattern)
|
|
@@ -10,6 +34,7 @@
|
|
|
10
34
|
- [CORS](#cors)
|
|
11
35
|
- [JWT Authentication](#jwt-authentication)
|
|
12
36
|
- [Logger](#logger)
|
|
37
|
+
- [Prometheus Metrics](#prometheus-metrics)
|
|
13
38
|
- [Rate Limiting](#rate-limiting)
|
|
14
39
|
- [Creating Custom Middleware](#creating-custom-middleware)
|
|
15
40
|
|
|
@@ -50,6 +75,7 @@ import {
|
|
|
50
75
|
createLogger,
|
|
51
76
|
createJWTAuth,
|
|
52
77
|
createRateLimit,
|
|
78
|
+
createPrometheusIntegration,
|
|
53
79
|
} from '0http-bun/lib/middleware'
|
|
54
80
|
```
|
|
55
81
|
|
|
@@ -65,6 +91,9 @@ const {
|
|
|
65
91
|
createJWTAuth,
|
|
66
92
|
createLogger,
|
|
67
93
|
createRateLimit,
|
|
94
|
+
createPrometheusMiddleware,
|
|
95
|
+
createMetricsHandler,
|
|
96
|
+
createPrometheusIntegration,
|
|
68
97
|
} = require('0http-bun/lib/middleware')
|
|
69
98
|
```
|
|
70
99
|
|
|
@@ -78,6 +107,9 @@ import {
|
|
|
78
107
|
createJWTAuth,
|
|
79
108
|
createLogger,
|
|
80
109
|
createRateLimit,
|
|
110
|
+
createPrometheusMiddleware,
|
|
111
|
+
createMetricsHandler,
|
|
112
|
+
createPrometheusIntegration,
|
|
81
113
|
} from '0http-bun/lib/middleware'
|
|
82
114
|
|
|
83
115
|
// Import types
|
|
@@ -94,6 +126,8 @@ import type {
|
|
|
94
126
|
|
|
95
127
|
Automatically parses request bodies based on Content-Type header.
|
|
96
128
|
|
|
129
|
+
> ✅ **No additional dependencies required** - Uses Bun's built-in parsing capabilities.
|
|
130
|
+
|
|
97
131
|
```javascript
|
|
98
132
|
const {createBodyParser} = require('0http-bun/lib/middleware')
|
|
99
133
|
|
|
@@ -142,6 +176,8 @@ router.use(createBodyParser(bodyParserOptions))
|
|
|
142
176
|
|
|
143
177
|
Cross-Origin Resource Sharing middleware with flexible configuration.
|
|
144
178
|
|
|
179
|
+
> ✅ **No additional dependencies required** - Built-in CORS implementation.
|
|
180
|
+
|
|
145
181
|
```javascript
|
|
146
182
|
const {createCORS} = require('0http-bun/lib/middleware')
|
|
147
183
|
|
|
@@ -173,7 +209,7 @@ router.use(
|
|
|
173
209
|
},
|
|
174
210
|
}),
|
|
175
211
|
)
|
|
176
|
-
|
|
212
|
+
```
|
|
177
213
|
|
|
178
214
|
**TypeScript Usage:**
|
|
179
215
|
|
|
@@ -194,6 +230,8 @@ router.use(createCORS(corsOptions))
|
|
|
194
230
|
|
|
195
231
|
JSON Web Token authentication and authorization middleware with support for static secrets, JWKS endpoints, and API key authentication.
|
|
196
232
|
|
|
233
|
+
> 📦 **Required dependency**: `bun install jose`
|
|
234
|
+
|
|
197
235
|
#### Basic JWT with Static Secret
|
|
198
236
|
|
|
199
237
|
```javascript
|
|
@@ -445,6 +483,9 @@ router.get('/api/profile', (req) => {
|
|
|
445
483
|
|
|
446
484
|
Request logging middleware with customizable output formats.
|
|
447
485
|
|
|
486
|
+
> 📦 **Required dependency for structured logging**: `bun install pino`
|
|
487
|
+
> ✅ **Simple logger** (`simpleLogger`) has no dependencies - uses `console.log`
|
|
488
|
+
|
|
448
489
|
```javascript
|
|
449
490
|
const {createLogger, simpleLogger} = require('0http-bun/lib/middleware')
|
|
450
491
|
|
|
@@ -503,10 +544,228 @@ router.use(createLogger(loggerOptions))
|
|
|
503
544
|
- `tiny` - Minimal output
|
|
504
545
|
- `dev` - Development-friendly colored output
|
|
505
546
|
|
|
547
|
+
### Prometheus Metrics
|
|
548
|
+
|
|
549
|
+
Comprehensive Prometheus metrics integration for monitoring and observability with built-in security and performance optimizations.
|
|
550
|
+
|
|
551
|
+
> 📦 **Required dependency**: `bun install prom-client`
|
|
552
|
+
|
|
553
|
+
```javascript
|
|
554
|
+
const {
|
|
555
|
+
createPrometheusMiddleware,
|
|
556
|
+
createMetricsHandler,
|
|
557
|
+
createPrometheusIntegration,
|
|
558
|
+
} = require('0http-bun/lib/middleware')
|
|
559
|
+
|
|
560
|
+
// Simple setup with default metrics
|
|
561
|
+
const prometheus = createPrometheusIntegration()
|
|
562
|
+
|
|
563
|
+
router.use(prometheus.middleware)
|
|
564
|
+
router.get('/metrics', prometheus.metricsHandler)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### Default Metrics Collected
|
|
568
|
+
|
|
569
|
+
The Prometheus middleware automatically collects:
|
|
570
|
+
|
|
571
|
+
- **HTTP Request Duration** - Histogram of request durations in seconds (buckets: 0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10)
|
|
572
|
+
- **HTTP Request Count** - Counter of total requests by method, route, and status
|
|
573
|
+
- **HTTP Request Size** - Histogram of request body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
|
|
574
|
+
- **HTTP Response Size** - Histogram of response body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
|
|
575
|
+
- **Active Connections** - Gauge of currently active HTTP connections
|
|
576
|
+
- **Node.js Metrics** - Memory usage, CPU, garbage collection (custom buckets), event loop lag (5ms precision)
|
|
577
|
+
|
|
578
|
+
#### Advanced Configuration
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
const {
|
|
582
|
+
createPrometheusMiddleware,
|
|
583
|
+
createMetricsHandler,
|
|
584
|
+
createPrometheusIntegration,
|
|
585
|
+
} = require('0http-bun/lib/middleware')
|
|
586
|
+
|
|
587
|
+
const prometheus = createPrometheusIntegration({
|
|
588
|
+
// Control default Node.js metrics collection
|
|
589
|
+
collectDefaultMetrics: true,
|
|
590
|
+
|
|
591
|
+
// Exclude paths from metrics collection (optimized for performance)
|
|
592
|
+
excludePaths: ['/health', '/ping', '/favicon.ico'],
|
|
593
|
+
|
|
594
|
+
// Skip certain HTTP methods
|
|
595
|
+
skipMethods: ['OPTIONS'],
|
|
596
|
+
|
|
597
|
+
// Custom route normalization with security controls
|
|
598
|
+
normalizeRoute: (req) => {
|
|
599
|
+
const url = new URL(req.url, 'http://localhost')
|
|
600
|
+
return url.pathname
|
|
601
|
+
.replace(/\/users\/\d+/, '/users/:id')
|
|
602
|
+
.replace(/\/api\/v\d+/, '/api/:version')
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Add custom labels with automatic sanitization
|
|
606
|
+
extractLabels: (req, response) => {
|
|
607
|
+
return {
|
|
608
|
+
user_type: req.headers.get('x-user-type') || 'anonymous',
|
|
609
|
+
api_version: req.headers.get('x-api-version') || 'v1',
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
// Use custom metrics object instead of default metrics
|
|
614
|
+
metrics: customMetricsObject,
|
|
615
|
+
})
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### Custom Business Metrics
|
|
619
|
+
|
|
620
|
+
```javascript
|
|
621
|
+
const {
|
|
622
|
+
createPrometheusIntegration,
|
|
623
|
+
} = require('0http-bun/lib/middleware')
|
|
624
|
+
|
|
625
|
+
// Get the prometheus client from the integration
|
|
626
|
+
const prometheus = createPrometheusIntegration()
|
|
627
|
+
const {promClient} = prometheus
|
|
628
|
+
|
|
629
|
+
// Create custom metrics
|
|
630
|
+
const orderCounter = new promClient.Counter({
|
|
631
|
+
name: 'orders_total',
|
|
632
|
+
help: 'Total number of orders processed',
|
|
633
|
+
labelNames: ['status', 'payment_method'],
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const orderValue = new promClient.Histogram({
|
|
637
|
+
name: 'order_value_dollars',
|
|
638
|
+
help: 'Value of orders in dollars',
|
|
639
|
+
labelNames: ['payment_method'],
|
|
640
|
+
buckets: [10, 50, 100, 500, 1000, 5000],
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
// Use in your routes
|
|
644
|
+
router.post('/orders', async (req) => {
|
|
645
|
+
const order = await processOrder(req.body)
|
|
646
|
+
|
|
647
|
+
// Record custom metrics
|
|
648
|
+
orderCounter.inc({
|
|
649
|
+
status: order.status,
|
|
650
|
+
payment_method: order.payment_method,
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
if (order.status === 'completed') {
|
|
654
|
+
orderValue.observe(
|
|
655
|
+
{
|
|
656
|
+
payment_method: order.payment_method,
|
|
657
|
+
},
|
|
658
|
+
order.amount,
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return Response.json(order)
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
#### Metrics Endpoint Options
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
const {createMetricsHandler} = require('0http-bun/lib/middleware')
|
|
670
|
+
|
|
671
|
+
// Custom metrics endpoint
|
|
672
|
+
const metricsHandler = createMetricsHandler({
|
|
673
|
+
endpoint: '/custom-metrics', // Default: '/metrics'
|
|
674
|
+
registry: customRegistry, // Default: promClient.register
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
router.get('/custom-metrics', metricsHandler)
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### Route Normalization & Security
|
|
681
|
+
|
|
682
|
+
The middleware automatically normalizes routes and implements security measures to prevent high cardinality and potential attacks:
|
|
683
|
+
|
|
684
|
+
```javascript
|
|
685
|
+
// URLs like these:
|
|
686
|
+
// /users/123, /users/456, /users/789
|
|
687
|
+
// Are normalized to: /users/:id
|
|
688
|
+
|
|
689
|
+
// /products/abc-123, /products/def-456
|
|
690
|
+
// Are normalized to: /products/:slug
|
|
691
|
+
|
|
692
|
+
// /api/v1/data, /api/v2/data
|
|
693
|
+
// Are normalized to: /api/:version/data
|
|
694
|
+
|
|
695
|
+
// Route sanitization examples:
|
|
696
|
+
// /users/:id → _users__id (special characters replaced with underscores)
|
|
697
|
+
// /api/v1/orders → _api_v1_orders
|
|
698
|
+
// Very long tokens → _api__token (pattern-based normalization)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Route Sanitization:**
|
|
702
|
+
|
|
703
|
+
- Special characters (`/`, `:`, etc.) are replaced with underscores (`_`) for Prometheus compatibility
|
|
704
|
+
- UUIDs are automatically normalized to `:id` patterns
|
|
705
|
+
- Long tokens (>20 characters) are normalized to `:token` patterns
|
|
706
|
+
- Numeric IDs are normalized to `:id` patterns
|
|
707
|
+
- Route complexity is limited to 10 segments maximum
|
|
708
|
+
|
|
709
|
+
**Security Features:**
|
|
710
|
+
|
|
711
|
+
- **Label Sanitization**: Removes potentially dangerous characters from metric labels and truncates values to 100 characters
|
|
712
|
+
- **Cardinality Limits**: Prevents memory exhaustion from too many unique metric combinations
|
|
713
|
+
- **Route Complexity Limits**: Caps the number of route segments to 10 to prevent DoS attacks
|
|
714
|
+
- **Size Limits**: Limits request/response body size processing (up to 100MB) to prevent memory issues
|
|
715
|
+
- **Header Processing Limits**: Caps the number of headers processed per request (50 for requests, 20 for responses)
|
|
716
|
+
- **URL Processing**: Handles both full URLs and pathname-only URLs with proper fallback handling
|
|
717
|
+
|
|
718
|
+
#### Performance Optimizations
|
|
719
|
+
|
|
720
|
+
- **Fast Path for Excluded Routes**: Bypasses all metric collection for excluded paths with smart URL parsing
|
|
721
|
+
- **Lazy Evaluation**: Only processes metrics when actually needed
|
|
722
|
+
- **Efficient Size Calculation**: Optimized request/response size measurement with capping at 1MB estimation
|
|
723
|
+
- **Error Handling**: Graceful handling of malformed URLs and invalid data with fallback mechanisms
|
|
724
|
+
- **Header Count Limits**: Prevents excessive header processing overhead (50 request headers, 20 response headers)
|
|
725
|
+
- **Smart URL Parsing**: Handles both full URLs and pathname-only URLs efficiently
|
|
726
|
+
|
|
727
|
+
#### Production Considerations
|
|
728
|
+
|
|
729
|
+
- **Performance**: Adds <1ms overhead per request with optimized fast paths
|
|
730
|
+
- **Memory**: Metrics stored in memory with cardinality controls; use recording rules for high cardinality
|
|
731
|
+
- **Security**: Built-in protections against label injection and cardinality bombs
|
|
732
|
+
- **Cardinality**: Automatic limits prevent high cardinality issues
|
|
733
|
+
- **Monitoring**: Consider protecting `/metrics` endpoint in production
|
|
734
|
+
|
|
735
|
+
#### Integration with Monitoring
|
|
736
|
+
|
|
737
|
+
```yaml
|
|
738
|
+
# prometheus.yml
|
|
739
|
+
scrape_configs:
|
|
740
|
+
- job_name: '0http-bun-app'
|
|
741
|
+
static_configs:
|
|
742
|
+
- targets: ['localhost:3000']
|
|
743
|
+
scrape_interval: 15s
|
|
744
|
+
metrics_path: /metrics
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
#### Troubleshooting
|
|
748
|
+
|
|
749
|
+
**Common Issues:**
|
|
750
|
+
|
|
751
|
+
- **High Memory Usage**: Check for high cardinality metrics. Route patterns should be normalized (e.g., `/users/:id` not `/users/12345`)
|
|
752
|
+
- **Missing Metrics**: Ensure paths aren't in `excludePaths` and HTTP methods aren't in `skipMethods`
|
|
753
|
+
- **Route Sanitization**: Routes are automatically sanitized (special characters become underscores: `/users/:id` → `_users__id`)
|
|
754
|
+
- **URL Parsing Errors**: The middleware handles both full URLs and pathname-only URLs with graceful fallback
|
|
755
|
+
|
|
756
|
+
**Performance Tips:**
|
|
757
|
+
|
|
758
|
+
- Use `excludePaths` for health checks and static assets
|
|
759
|
+
- Consider using `skipMethods` for OPTIONS requests
|
|
760
|
+
- Monitor memory usage in production for metric cardinality
|
|
761
|
+
- Use Prometheus recording rules for high-cardinality aggregations
|
|
762
|
+
|
|
506
763
|
### Rate Limiting
|
|
507
764
|
|
|
508
765
|
Configurable rate limiting middleware with multiple store options.
|
|
509
766
|
|
|
767
|
+
> ✅ **No additional dependencies required** - Uses built-in memory store.
|
|
768
|
+
|
|
510
769
|
```javascript
|
|
511
770
|
const {createRateLimit, MemoryStore} = require('0http-bun/lib/middleware')
|
|
512
771
|
|
|
@@ -590,15 +849,127 @@ class CustomStore implements RateLimitStore {
|
|
|
590
849
|
}
|
|
591
850
|
```
|
|
592
851
|
|
|
593
|
-
|
|
852
|
+
#### Sliding Window Rate Limiter
|
|
853
|
+
|
|
854
|
+
For more precise rate limiting, use the sliding window implementation that **prevents burst traffic** at any point in time:
|
|
855
|
+
|
|
856
|
+
```javascript
|
|
857
|
+
const {createSlidingWindowRateLimit} = require('0http-bun/lib/middleware')
|
|
858
|
+
|
|
859
|
+
// Basic sliding window rate limiter
|
|
860
|
+
router.use(
|
|
861
|
+
createSlidingWindowRateLimit({
|
|
862
|
+
windowMs: 60 * 1000, // 1 minute sliding window
|
|
863
|
+
max: 10, // Max 10 requests per minute
|
|
864
|
+
keyGenerator: (req) => req.headers.get('x-forwarded-for') || 'default',
|
|
865
|
+
}),
|
|
866
|
+
)
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**TypeScript Usage:**
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
import {createSlidingWindowRateLimit} from '0http-bun/lib/middleware'
|
|
873
|
+
import type {RateLimitOptions} from '0http-bun/lib/middleware'
|
|
874
|
+
|
|
875
|
+
const slidingOptions: RateLimitOptions = {
|
|
876
|
+
windowMs: 60 * 1000, // 1 minute
|
|
877
|
+
max: 10, // 10 requests max
|
|
878
|
+
keyGenerator: (req) => req.user?.id || req.headers.get('x-forwarded-for'),
|
|
879
|
+
handler: (req, hits, max, resetTime) => {
|
|
880
|
+
return Response.json(
|
|
881
|
+
{
|
|
882
|
+
error: 'Rate limit exceeded',
|
|
883
|
+
retryAfter: Math.ceil((resetTime.getTime() - Date.now()) / 1000),
|
|
884
|
+
limit: max,
|
|
885
|
+
used: hits,
|
|
886
|
+
},
|
|
887
|
+
{status: 429},
|
|
888
|
+
)
|
|
889
|
+
},
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
router.use(createSlidingWindowRateLimit(slidingOptions))
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**How Sliding Window Differs from Fixed Window:**
|
|
896
|
+
|
|
897
|
+
The sliding window approach provides **more accurate and fair rate limiting** by tracking individual request timestamps:
|
|
898
|
+
|
|
899
|
+
- **Fixed Window**: Divides time into discrete chunks (e.g., 09:00:00-09:00:59, 09:01:00-09:01:59)
|
|
900
|
+
- ⚠️ **Problem**: Allows burst traffic at window boundaries (20 requests in 2 seconds)
|
|
901
|
+
- **Sliding Window**: Uses a continuous, moving time window from current moment
|
|
902
|
+
- ✅ **Advantage**: Prevents bursts at any point in time (true rate limiting)
|
|
903
|
+
|
|
904
|
+
**Use Cases for Sliding Window:**
|
|
905
|
+
|
|
906
|
+
```javascript
|
|
907
|
+
// Financial API - Zero tolerance for payment bursts
|
|
908
|
+
router.use(
|
|
909
|
+
'/api/payments/*',
|
|
910
|
+
createSlidingWindowRateLimit({
|
|
911
|
+
windowMs: 60 * 1000, // 1 minute
|
|
912
|
+
max: 3, // Only 3 payment attempts per minute
|
|
913
|
+
keyGenerator: (req) => req.user.accountId,
|
|
914
|
+
}),
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
// User Registration - Prevent automated signups
|
|
918
|
+
router.use(
|
|
919
|
+
'/api/register',
|
|
920
|
+
createSlidingWindowRateLimit({
|
|
921
|
+
windowMs: 3600 * 1000, // 1 hour
|
|
922
|
+
max: 3, // 3 accounts per IP per hour
|
|
923
|
+
keyGenerator: (req) => req.headers.get('x-forwarded-for'),
|
|
924
|
+
}),
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
// File Upload - Prevent abuse
|
|
928
|
+
router.use(
|
|
929
|
+
'/api/upload',
|
|
930
|
+
createSlidingWindowRateLimit({
|
|
931
|
+
windowMs: 300 * 1000, // 5 minutes
|
|
932
|
+
max: 10, // 10 uploads per 5 minutes
|
|
933
|
+
keyGenerator: (req) => req.user.id,
|
|
934
|
+
}),
|
|
935
|
+
)
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Performance Considerations:**
|
|
939
|
+
|
|
940
|
+
- **Memory Usage**: Higher than fixed window (stores timestamp arrays)
|
|
941
|
+
- **Time Complexity**: O(n) per request where n = requests in window
|
|
942
|
+
- **Best For**: Critical APIs, financial transactions, user-facing features
|
|
943
|
+
- **Use Fixed Window For**: High-volume APIs where approximate limiting is acceptable
|
|
944
|
+
|
|
945
|
+
**Advanced Configuration:**
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
// Tiered rate limiting based on user level
|
|
949
|
+
const createTieredRateLimit = (req) => {
|
|
950
|
+
const userTier = req.user?.tier || 'free'
|
|
951
|
+
const configs = {
|
|
952
|
+
free: {windowMs: 60 * 1000, max: 10},
|
|
953
|
+
premium: {windowMs: 60 * 1000, max: 100},
|
|
954
|
+
enterprise: {windowMs: 60 * 1000, max: 1000},
|
|
955
|
+
}
|
|
956
|
+
return createSlidingWindowRateLimit(configs[userTier])
|
|
957
|
+
}
|
|
958
|
+
```
|
|
594
959
|
|
|
595
960
|
**Rate Limit Headers:**
|
|
596
961
|
|
|
962
|
+
Both rate limiters send the following headers when `standardHeaders: true`:
|
|
963
|
+
|
|
597
964
|
- `X-RateLimit-Limit` - Request limit
|
|
598
965
|
- `X-RateLimit-Remaining` - Remaining requests
|
|
599
966
|
- `X-RateLimit-Reset` - Reset time (Unix timestamp)
|
|
600
967
|
- `X-RateLimit-Used` - Used requests
|
|
601
968
|
|
|
969
|
+
**Error Handling:**
|
|
970
|
+
|
|
971
|
+
Rate limiting middleware allows errors to bubble up as proper HTTP 500 responses. If your `keyGenerator` function or custom `store.increment()` method throws an error, it will not be caught and masked - the error will propagate up the middleware chain for proper error handling.
|
|
972
|
+
|
|
602
973
|
## Creating Custom Middleware
|
|
603
974
|
|
|
604
975
|
### Basic Middleware
|
|
@@ -619,7 +990,7 @@ const customMiddleware = (req: ZeroRequest, next: StepFunction) => {
|
|
|
619
990
|
}
|
|
620
991
|
|
|
621
992
|
router.use(customMiddleware)
|
|
622
|
-
|
|
993
|
+
```
|
|
623
994
|
|
|
624
995
|
### Async Middleware
|
|
625
996
|
|
|
@@ -682,8 +1053,8 @@ Apply middleware only to specific paths:
|
|
|
682
1053
|
|
|
683
1054
|
```typescript
|
|
684
1055
|
// API-only middleware
|
|
685
|
-
router.use('/api/*',
|
|
686
|
-
router.use('/api/*',
|
|
1056
|
+
router.use('/api/*', createJWTAuth({secret: 'api-secret'}))
|
|
1057
|
+
router.use('/api/*', createRateLimit({max: 1000}))
|
|
687
1058
|
|
|
688
1059
|
// Admin-only middleware
|
|
689
1060
|
router.use('/admin/*', adminAuthMiddleware)
|
|
@@ -755,4 +1126,24 @@ router.get('/api/public/status', () => Response.json({status: 'ok'}))
|
|
|
755
1126
|
router.get('/api/protected/data', (req) => Response.json({user: req.user}))
|
|
756
1127
|
```
|
|
757
1128
|
|
|
1129
|
+
## Dependency Summary
|
|
1130
|
+
|
|
1131
|
+
For your convenience, here's a quick reference of which dependencies you need to install for each middleware:
|
|
1132
|
+
|
|
1133
|
+
| Middleware | Dependencies Required | Install Command |
|
|
1134
|
+
| ----------------------- | --------------------- | ------------------------- |
|
|
1135
|
+
| **Body Parser** | ✅ None | Built-in |
|
|
1136
|
+
| **CORS** | ✅ None | Built-in |
|
|
1137
|
+
| **Rate Limiting** | ✅ None | Built-in |
|
|
1138
|
+
| **Logger** (simple) | ✅ None | Built-in |
|
|
1139
|
+
| **Logger** (structured) | 📦 `pino` | `bun install pino` |
|
|
1140
|
+
| **JWT Authentication** | 📦 `jose` | `bun install jose` |
|
|
1141
|
+
| **Prometheus Metrics** | 📦 `prom-client` | `bun install prom-client` |
|
|
1142
|
+
|
|
1143
|
+
**Install all optional dependencies at once:**
|
|
1144
|
+
|
|
1145
|
+
```bash
|
|
1146
|
+
bun install pino jose prom-client
|
|
1147
|
+
```
|
|
1148
|
+
|
|
758
1149
|
This middleware stack provides a solid foundation for most web applications with security, logging, and performance features built-in.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {RequestHandler, ZeroRequest
|
|
2
|
-
import {Logger} from 'pino'
|
|
1
|
+
import {RequestHandler, ZeroRequest} from '../../common'
|
|
3
2
|
|
|
4
3
|
// Logger middleware types
|
|
5
4
|
export interface LoggerOptions {
|
|
@@ -234,3 +233,68 @@ export function createMultipartParser(
|
|
|
234
233
|
export function createBodyParser(options?: BodyParserOptions): RequestHandler
|
|
235
234
|
export function hasBody(req: ZeroRequest): boolean
|
|
236
235
|
export function shouldParse(req: ZeroRequest, type: string): boolean
|
|
236
|
+
|
|
237
|
+
// Prometheus metrics middleware types
|
|
238
|
+
export interface PrometheusMetrics {
|
|
239
|
+
httpRequestDuration: any // prom-client Histogram
|
|
240
|
+
httpRequestTotal: any // prom-client Counter
|
|
241
|
+
httpRequestSize: any // prom-client Histogram
|
|
242
|
+
httpResponseSize: any // prom-client Histogram
|
|
243
|
+
httpActiveConnections: any // prom-client Gauge
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface PrometheusMiddlewareOptions {
|
|
247
|
+
/** Custom metrics object to use instead of default metrics */
|
|
248
|
+
metrics?: PrometheusMetrics
|
|
249
|
+
/** Paths to exclude from metrics collection (default: ['/health', '/ping', '/favicon.ico', '/metrics']) */
|
|
250
|
+
excludePaths?: string[]
|
|
251
|
+
/** Whether to collect default Node.js metrics (default: true) */
|
|
252
|
+
collectDefaultMetrics?: boolean
|
|
253
|
+
/** Custom route normalization function */
|
|
254
|
+
normalizeRoute?: (req: ZeroRequest) => string
|
|
255
|
+
/** Custom label extraction function */
|
|
256
|
+
extractLabels?: (
|
|
257
|
+
req: ZeroRequest,
|
|
258
|
+
response: Response,
|
|
259
|
+
) => Record<string, string>
|
|
260
|
+
/** HTTP methods to skip from metrics collection */
|
|
261
|
+
skipMethods?: string[]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface MetricsHandlerOptions {
|
|
265
|
+
/** The endpoint path for metrics (default: '/metrics') */
|
|
266
|
+
endpoint?: string
|
|
267
|
+
/** Custom Prometheus registry to use */
|
|
268
|
+
registry?: any // prom-client Registry
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface PrometheusIntegration {
|
|
272
|
+
/** The middleware function */
|
|
273
|
+
middleware: RequestHandler
|
|
274
|
+
/** The metrics handler function */
|
|
275
|
+
metricsHandler: RequestHandler
|
|
276
|
+
/** The Prometheus registry */
|
|
277
|
+
registry: any // prom-client Registry
|
|
278
|
+
/** The prom-client module */
|
|
279
|
+
promClient: any
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function createPrometheusMiddleware(
|
|
283
|
+
options?: PrometheusMiddlewareOptions,
|
|
284
|
+
): RequestHandler
|
|
285
|
+
export function createMetricsHandler(
|
|
286
|
+
options?: MetricsHandlerOptions,
|
|
287
|
+
): RequestHandler
|
|
288
|
+
export function createPrometheusIntegration(
|
|
289
|
+
options?: PrometheusMiddlewareOptions & MetricsHandlerOptions,
|
|
290
|
+
): PrometheusIntegration
|
|
291
|
+
export function createDefaultMetrics(): PrometheusMetrics
|
|
292
|
+
export function extractRoutePattern(req: ZeroRequest): string
|
|
293
|
+
|
|
294
|
+
// Simple interface exports for common use cases
|
|
295
|
+
export const logger: typeof createLogger
|
|
296
|
+
export const jwtAuth: typeof createJWTAuth
|
|
297
|
+
export const rateLimit: typeof createRateLimit
|
|
298
|
+
export const cors: typeof createCORS
|
|
299
|
+
export const bodyParser: typeof createBodyParser
|
|
300
|
+
export const prometheus: typeof createPrometheusIntegration
|
package/lib/middleware/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const jwtAuthModule = require('./jwt-auth')
|
|
|
4
4
|
const rateLimitModule = require('./rate-limit')
|
|
5
5
|
const corsModule = require('./cors')
|
|
6
6
|
const bodyParserModule = require('./body-parser')
|
|
7
|
+
const prometheusModule = require('./prometheus')
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
// Simple interface for common use cases (matches test expectations)
|
|
@@ -12,6 +13,7 @@ module.exports = {
|
|
|
12
13
|
rateLimit: rateLimitModule.createRateLimit,
|
|
13
14
|
cors: corsModule.createCORS,
|
|
14
15
|
bodyParser: bodyParserModule.createBodyParser,
|
|
16
|
+
prometheus: prometheusModule.createPrometheusIntegration,
|
|
15
17
|
|
|
16
18
|
// Complete factory functions for advanced usage
|
|
17
19
|
createLogger: loggerModule.createLogger,
|
|
@@ -42,4 +44,11 @@ module.exports = {
|
|
|
42
44
|
createBodyParser: bodyParserModule.createBodyParser,
|
|
43
45
|
hasBody: bodyParserModule.hasBody,
|
|
44
46
|
shouldParse: bodyParserModule.shouldParse,
|
|
47
|
+
|
|
48
|
+
// Prometheus metrics middleware
|
|
49
|
+
createPrometheusMiddleware: prometheusModule.createPrometheusMiddleware,
|
|
50
|
+
createMetricsHandler: prometheusModule.createMetricsHandler,
|
|
51
|
+
createPrometheusIntegration: prometheusModule.createPrometheusIntegration,
|
|
52
|
+
createDefaultMetrics: prometheusModule.createDefaultMetrics,
|
|
53
|
+
extractRoutePattern: prometheusModule.extractRoutePattern,
|
|
45
54
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Lazy load jose to improve startup performance
|
|
2
|
+
let joseLib = null
|
|
3
|
+
function loadJose() {
|
|
4
|
+
if (!joseLib) {
|
|
5
|
+
try {
|
|
6
|
+
joseLib = require('jose')
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'jose is required for JWT middleware. Install it with: bun install jose',
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return joseLib
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
/**
|
|
4
17
|
* Creates JWT authentication middleware
|
|
@@ -60,6 +73,7 @@ function createJWTAuth(options = {}) {
|
|
|
60
73
|
keyLike = jwks
|
|
61
74
|
}
|
|
62
75
|
} else if (jwksUri) {
|
|
76
|
+
const {createRemoteJWKSet} = loadJose()
|
|
63
77
|
keyLike = createRemoteJWKSet(new URL(jwksUri))
|
|
64
78
|
} else if (typeof secret === 'function') {
|
|
65
79
|
keyLike = secret
|
|
@@ -143,6 +157,7 @@ function createJWTAuth(options = {}) {
|
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
// Verify JWT token
|
|
160
|
+
const {jwtVerify} = loadJose()
|
|
146
161
|
const {payload, protectedHeader} = await jwtVerify(
|
|
147
162
|
token,
|
|
148
163
|
keyLike,
|
|
@@ -302,16 +317,19 @@ function handleAuthError(error, handlers = {}, req) {
|
|
|
302
317
|
message = 'Invalid API key'
|
|
303
318
|
} else if (error.message === 'JWT verification not configured') {
|
|
304
319
|
message = 'JWT verification not configured'
|
|
305
|
-
} else
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
} else {
|
|
321
|
+
const {errors} = loadJose()
|
|
322
|
+
if (error instanceof errors.JWTExpired) {
|
|
323
|
+
message = 'Token expired'
|
|
324
|
+
} else if (error instanceof errors.JWTInvalid) {
|
|
325
|
+
message = 'Invalid token format'
|
|
326
|
+
} else if (error instanceof errors.JWKSNoMatchingKey) {
|
|
327
|
+
message = 'Token signature verification failed'
|
|
328
|
+
} else if (error.message.includes('audience')) {
|
|
329
|
+
message = 'Invalid token audience'
|
|
330
|
+
} else if (error.message.includes('issuer')) {
|
|
331
|
+
message = 'Invalid token issuer'
|
|
332
|
+
}
|
|
315
333
|
}
|
|
316
334
|
|
|
317
335
|
return new Response(JSON.stringify({error: message}), {
|