navis.js 4.0.0 → 5.0.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 +16 -2
- package/examples/v5-features-demo.js +167 -0
- package/package.json +1 -1
- package/src/cache/cache.js +157 -0
- package/src/cache/redis-cache.js +174 -0
- package/src/core/app.js +9 -0
- package/src/core/graceful-shutdown.js +77 -0
- package/src/health/health-checker.js +120 -0
- package/src/index.js +21 -0
- package/src/middleware/cache-middleware.js +105 -0
- package/src/middleware/compression.js +97 -0
- package/src/middleware/cors.js +86 -0
- package/src/middleware/security.js +107 -0
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ navis metrics
|
|
|
120
120
|
- ✅ **LambdaHandler** - Optimized handler with warm-up support
|
|
121
121
|
- ✅ **Cold start tracking** - Monitor and log cold start metrics
|
|
122
122
|
|
|
123
|
-
### v4
|
|
123
|
+
### v4
|
|
124
124
|
|
|
125
125
|
- ✅ **Advanced routing** - Route parameters (`:id`), nested routes, PATCH method
|
|
126
126
|
- ✅ **Request validation** - Schema-based validation with comprehensive rules
|
|
@@ -129,6 +129,15 @@ navis metrics
|
|
|
129
129
|
- ✅ **Rate limiting** - In-memory rate limiting with configurable windows
|
|
130
130
|
- ✅ **Enhanced error handling** - Custom error classes and error handler middleware
|
|
131
131
|
|
|
132
|
+
### v5 (Current)
|
|
133
|
+
|
|
134
|
+
- ✅ **Caching layer** - In-memory cache with TTL and Redis adapter
|
|
135
|
+
- ✅ **CORS support** - Cross-Origin Resource Sharing middleware
|
|
136
|
+
- ✅ **Security headers** - Protection against common attacks
|
|
137
|
+
- ✅ **Response compression** - Gzip and Brotli compression
|
|
138
|
+
- ✅ **Health checks** - Liveness and readiness probes
|
|
139
|
+
- ✅ **Graceful shutdown** - Clean shutdown handling
|
|
140
|
+
|
|
132
141
|
## API Reference
|
|
133
142
|
|
|
134
143
|
### NavisApp
|
|
@@ -314,6 +323,7 @@ See the `examples/` directory:
|
|
|
314
323
|
- `lambda.js` - AWS Lambda handler example
|
|
315
324
|
- `lambda-optimized.js` - Optimized Lambda handler with cold start optimizations (v3.1)
|
|
316
325
|
- `v4-features-demo.js` - v4 features demonstration (routing, validation, auth, rate limiting, etc.)
|
|
326
|
+
- `v5-features-demo.js` - v5 features demonstration (caching, CORS, security, compression, health checks, etc.)
|
|
317
327
|
- `service-client-demo.js` - ServiceClient usage example
|
|
318
328
|
- `v2-features-demo.js` - v2 features demonstration (retry, circuit breaker, etc.)
|
|
319
329
|
- `v3-features-demo.js` - v3 features demonstration (messaging, observability, etc.)
|
|
@@ -329,14 +339,18 @@ Resilience patterns: retry, circuit breaker, service discovery, CLI generators
|
|
|
329
339
|
### v3 ✅
|
|
330
340
|
Advanced features: async messaging (SQS/Kafka/NATS), observability, enhanced CLI
|
|
331
341
|
|
|
332
|
-
### v4 ✅
|
|
342
|
+
### v4 ✅
|
|
333
343
|
Production-ready: advanced routing, validation, authentication, rate limiting, error handling
|
|
334
344
|
|
|
345
|
+
### v5 ✅ (Current)
|
|
346
|
+
Enterprise-grade: caching, CORS, security headers, compression, health checks, graceful shutdown
|
|
347
|
+
|
|
335
348
|
## Documentation
|
|
336
349
|
|
|
337
350
|
- [V2 Features Guide](./V2_FEATURES.md) - Complete v2 features documentation
|
|
338
351
|
- [V3 Features Guide](./V3_FEATURES.md) - Complete v3 features documentation
|
|
339
352
|
- [V4 Features Guide](./V4_FEATURES.md) - Complete v4 features documentation
|
|
353
|
+
- [V5 Features Guide](./V5_FEATURES.md) - Complete v5 features documentation
|
|
340
354
|
- [Lambda Optimization Guide](./LAMBDA_OPTIMIZATION.md) - Lambda cold start optimization guide (v3.1)
|
|
341
355
|
- [Verification Guide v2](./VERIFY_V2.md) - How to verify v2 features
|
|
342
356
|
- [Verification Guide v3](./VERIFY_V3.md) - How to verify v3 features
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navis.js v5 Features Demo
|
|
3
|
+
* Demonstrates caching, CORS, security, compression, health checks, and graceful shutdown
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
NavisApp,
|
|
8
|
+
response,
|
|
9
|
+
Cache,
|
|
10
|
+
cache,
|
|
11
|
+
cors,
|
|
12
|
+
security,
|
|
13
|
+
compress,
|
|
14
|
+
createHealthChecker,
|
|
15
|
+
gracefulShutdown,
|
|
16
|
+
} = require('../src/index');
|
|
17
|
+
|
|
18
|
+
const app = new NavisApp();
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// CORS Middleware
|
|
22
|
+
// ============================================
|
|
23
|
+
app.use(cors({
|
|
24
|
+
origin: ['http://localhost:3000', 'https://example.com'],
|
|
25
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
26
|
+
credentials: true,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// ============================================
|
|
30
|
+
// Security Headers
|
|
31
|
+
// ============================================
|
|
32
|
+
app.use(security({
|
|
33
|
+
helmet: true,
|
|
34
|
+
hsts: true,
|
|
35
|
+
noSniff: true,
|
|
36
|
+
xssFilter: true,
|
|
37
|
+
frameOptions: 'DENY',
|
|
38
|
+
referrerPolicy: 'no-referrer',
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// Response Compression
|
|
43
|
+
// ============================================
|
|
44
|
+
app.use(compress({
|
|
45
|
+
level: 6,
|
|
46
|
+
threshold: 1024,
|
|
47
|
+
algorithm: 'gzip',
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// ============================================
|
|
51
|
+
// Caching
|
|
52
|
+
// ============================================
|
|
53
|
+
const cacheStore = new Cache({
|
|
54
|
+
maxSize: 1000,
|
|
55
|
+
defaultTTL: 3600000, // 1 hour
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Cached route
|
|
59
|
+
app.get('/users/:id', cache({
|
|
60
|
+
cacheStore,
|
|
61
|
+
ttl: 1800, // 30 minutes
|
|
62
|
+
keyGenerator: (req) => `user:${req.params.id}`,
|
|
63
|
+
}), (req, res) => {
|
|
64
|
+
// Simulate database query
|
|
65
|
+
const user = {
|
|
66
|
+
id: req.params.id,
|
|
67
|
+
name: 'John Doe',
|
|
68
|
+
email: 'john@example.com',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
response.success(res, user);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Non-cached route
|
|
75
|
+
app.get('/users/:id/posts', (req, res) => {
|
|
76
|
+
response.success(res, {
|
|
77
|
+
userId: req.params.id,
|
|
78
|
+
posts: [],
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Health Checks
|
|
84
|
+
// ============================================
|
|
85
|
+
const healthChecker = createHealthChecker({
|
|
86
|
+
livenessPath: '/health/live',
|
|
87
|
+
readinessPath: '/health/ready',
|
|
88
|
+
checks: {
|
|
89
|
+
database: async () => {
|
|
90
|
+
// Simulate database check
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
cache: async () => {
|
|
94
|
+
// Check cache
|
|
95
|
+
return cacheStore.size() >= 0;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
app.use(healthChecker.middleware());
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// Routes
|
|
104
|
+
// ============================================
|
|
105
|
+
app.get('/', (req, res) => {
|
|
106
|
+
response.success(res, {
|
|
107
|
+
message: 'Navis.js v5 Features Demo',
|
|
108
|
+
features: [
|
|
109
|
+
'Caching',
|
|
110
|
+
'CORS',
|
|
111
|
+
'Security Headers',
|
|
112
|
+
'Compression',
|
|
113
|
+
'Health Checks',
|
|
114
|
+
'Graceful Shutdown',
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.get('/cache-stats', (req, res) => {
|
|
120
|
+
response.success(res, {
|
|
121
|
+
size: cacheStore.size(),
|
|
122
|
+
keys: cacheStore.keys().slice(0, 10), // First 10 keys
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
app.post('/cache/clear', (req, res) => {
|
|
127
|
+
cacheStore.clear();
|
|
128
|
+
response.success(res, { message: 'Cache cleared' });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ============================================
|
|
132
|
+
// Start Server
|
|
133
|
+
// ============================================
|
|
134
|
+
const PORT = 3000;
|
|
135
|
+
const server = app.listen(PORT, () => {
|
|
136
|
+
console.log(`\n🚀 Navis.js v5 Features Demo Server`);
|
|
137
|
+
console.log(`📡 Listening on http://localhost:${PORT}\n`);
|
|
138
|
+
console.log('Available endpoints:');
|
|
139
|
+
console.log(' GET /');
|
|
140
|
+
console.log(' GET /users/:id (cached)');
|
|
141
|
+
console.log(' GET /users/:id/posts');
|
|
142
|
+
console.log(' GET /health/live (liveness)');
|
|
143
|
+
console.log(' GET /health/ready (readiness)');
|
|
144
|
+
console.log(' GET /cache-stats');
|
|
145
|
+
console.log(' POST /cache/clear');
|
|
146
|
+
console.log('\n💡 Test with:');
|
|
147
|
+
console.log(' curl http://localhost:3000/');
|
|
148
|
+
console.log(' curl http://localhost:3000/users/123');
|
|
149
|
+
console.log(' curl http://localhost:3000/health/ready');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ============================================
|
|
153
|
+
// Graceful Shutdown
|
|
154
|
+
// ============================================
|
|
155
|
+
gracefulShutdown(server, {
|
|
156
|
+
timeout: 10000,
|
|
157
|
+
onShutdown: async () => {
|
|
158
|
+
console.log('Cleaning up...');
|
|
159
|
+
// Close database connections
|
|
160
|
+
// Close cache connections
|
|
161
|
+
cacheStore.destroy();
|
|
162
|
+
console.log('Cleanup complete');
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
module.exports = app;
|
|
167
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Cache
|
|
3
|
+
* v5: Simple in-memory caching with TTL support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Cache {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.store = new Map();
|
|
9
|
+
this.maxSize = options.maxSize || 1000;
|
|
10
|
+
this.defaultTTL = options.defaultTTL || 3600000; // 1 hour in ms
|
|
11
|
+
this.cleanupInterval = options.cleanupInterval || 60000; // 1 minute
|
|
12
|
+
|
|
13
|
+
// Start cleanup interval
|
|
14
|
+
this.intervalId = setInterval(() => {
|
|
15
|
+
this._cleanup();
|
|
16
|
+
}, this.cleanupInterval);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set a value in cache
|
|
21
|
+
* @param {string} key - Cache key
|
|
22
|
+
* @param {*} value - Value to cache
|
|
23
|
+
* @param {number} ttl - Time to live in milliseconds
|
|
24
|
+
*/
|
|
25
|
+
set(key, value, ttl = null) {
|
|
26
|
+
const expiration = Date.now() + (ttl || this.defaultTTL);
|
|
27
|
+
|
|
28
|
+
// Remove oldest entries if at max size
|
|
29
|
+
if (this.store.size >= this.maxSize && !this.store.has(key)) {
|
|
30
|
+
this._evictOldest();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.store.set(key, {
|
|
34
|
+
value,
|
|
35
|
+
expiration,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a value from cache
|
|
41
|
+
* @param {string} key - Cache key
|
|
42
|
+
* @returns {*} - Cached value or null
|
|
43
|
+
*/
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = this.store.get(key);
|
|
46
|
+
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if expired
|
|
52
|
+
if (Date.now() > entry.expiration) {
|
|
53
|
+
this.store.delete(key);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entry.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if key exists in cache
|
|
62
|
+
* @param {string} key - Cache key
|
|
63
|
+
* @returns {boolean} - True if key exists and not expired
|
|
64
|
+
*/
|
|
65
|
+
has(key) {
|
|
66
|
+
const entry = this.store.get(key);
|
|
67
|
+
|
|
68
|
+
if (!entry) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Date.now() > entry.expiration) {
|
|
73
|
+
this.store.delete(key);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete a key from cache
|
|
82
|
+
* @param {string} key - Cache key
|
|
83
|
+
* @returns {boolean} - True if key was deleted
|
|
84
|
+
*/
|
|
85
|
+
delete(key) {
|
|
86
|
+
return this.store.delete(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear all cache entries
|
|
91
|
+
*/
|
|
92
|
+
clear() {
|
|
93
|
+
this.store.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get cache size
|
|
98
|
+
* @returns {number} - Number of entries
|
|
99
|
+
*/
|
|
100
|
+
size() {
|
|
101
|
+
return this.store.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all keys
|
|
106
|
+
* @returns {Array} - Array of cache keys
|
|
107
|
+
*/
|
|
108
|
+
keys() {
|
|
109
|
+
return Array.from(this.store.keys());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cleanup expired entries
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
_cleanup() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
for (const [key, entry] of this.store.entries()) {
|
|
119
|
+
if (now > entry.expiration) {
|
|
120
|
+
this.store.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evict oldest entry (LRU-like)
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
_evictOldest() {
|
|
130
|
+
let oldestKey = null;
|
|
131
|
+
let oldestExpiration = Infinity;
|
|
132
|
+
|
|
133
|
+
for (const [key, entry] of this.store.entries()) {
|
|
134
|
+
if (entry.expiration < oldestExpiration) {
|
|
135
|
+
oldestExpiration = entry.expiration;
|
|
136
|
+
oldestKey = key;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (oldestKey) {
|
|
141
|
+
this.store.delete(oldestKey);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Destroy cache (cleanup)
|
|
147
|
+
*/
|
|
148
|
+
destroy() {
|
|
149
|
+
if (this.intervalId) {
|
|
150
|
+
clearInterval(this.intervalId);
|
|
151
|
+
}
|
|
152
|
+
this.clear();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = Cache;
|
|
157
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Cache Adapter
|
|
3
|
+
* v5: Redis-based caching (requires redis package)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RedisCache {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.client = null;
|
|
9
|
+
this.defaultTTL = options.defaultTTL || 3600; // 1 hour in seconds
|
|
10
|
+
this.prefix = options.prefix || 'navis:';
|
|
11
|
+
this.connected = false;
|
|
12
|
+
|
|
13
|
+
// Try to require redis (optional dependency)
|
|
14
|
+
try {
|
|
15
|
+
const redis = require('redis');
|
|
16
|
+
this.redis = redis;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error('Redis package not installed. Install with: npm install redis');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connect to Redis
|
|
24
|
+
* @param {Object} options - Redis connection options
|
|
25
|
+
*/
|
|
26
|
+
async connect(options = {}) {
|
|
27
|
+
if (this.connected && this.client) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
url = process.env.REDIS_URL || 'redis://localhost:6379',
|
|
33
|
+
socket = {},
|
|
34
|
+
...otherOptions
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
this.client = this.redis.createClient({
|
|
38
|
+
url,
|
|
39
|
+
socket: {
|
|
40
|
+
reconnectStrategy: (retries) => {
|
|
41
|
+
if (retries > 10) {
|
|
42
|
+
return new Error('Too many reconnection attempts');
|
|
43
|
+
}
|
|
44
|
+
return Math.min(retries * 100, 3000);
|
|
45
|
+
},
|
|
46
|
+
...socket,
|
|
47
|
+
},
|
|
48
|
+
...otherOptions,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.client.on('error', (err) => {
|
|
52
|
+
console.error('Redis Client Error:', err);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await this.client.connect();
|
|
56
|
+
this.connected = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Disconnect from Redis
|
|
61
|
+
*/
|
|
62
|
+
async disconnect() {
|
|
63
|
+
if (this.client && this.connected) {
|
|
64
|
+
await this.client.quit();
|
|
65
|
+
this.connected = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set a value in cache
|
|
71
|
+
* @param {string} key - Cache key
|
|
72
|
+
* @param {*} value - Value to cache
|
|
73
|
+
* @param {number} ttl - Time to live in seconds
|
|
74
|
+
*/
|
|
75
|
+
async set(key, value, ttl = null) {
|
|
76
|
+
if (!this.connected) {
|
|
77
|
+
throw new Error('Redis not connected. Call connect() first.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fullKey = this.prefix + key;
|
|
81
|
+
const serialized = JSON.stringify(value);
|
|
82
|
+
const expiration = ttl || this.defaultTTL;
|
|
83
|
+
|
|
84
|
+
if (expiration > 0) {
|
|
85
|
+
await this.client.setEx(fullKey, expiration, serialized);
|
|
86
|
+
} else {
|
|
87
|
+
await this.client.set(fullKey, serialized);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a value from cache
|
|
93
|
+
* @param {string} key - Cache key
|
|
94
|
+
* @returns {*} - Cached value or null
|
|
95
|
+
*/
|
|
96
|
+
async get(key) {
|
|
97
|
+
if (!this.connected) {
|
|
98
|
+
throw new Error('Redis not connected. Call connect() first.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fullKey = this.prefix + key;
|
|
102
|
+
const serialized = await this.client.get(fullKey);
|
|
103
|
+
|
|
104
|
+
if (!serialized) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(serialized);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if key exists in cache
|
|
117
|
+
* @param {string} key - Cache key
|
|
118
|
+
* @returns {boolean} - True if key exists
|
|
119
|
+
*/
|
|
120
|
+
async has(key) {
|
|
121
|
+
if (!this.connected) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fullKey = this.prefix + key;
|
|
126
|
+
const exists = await this.client.exists(fullKey);
|
|
127
|
+
return exists === 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a key from cache
|
|
132
|
+
* @param {string} key - Cache key
|
|
133
|
+
* @returns {boolean} - True if key was deleted
|
|
134
|
+
*/
|
|
135
|
+
async delete(key) {
|
|
136
|
+
if (!this.connected) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fullKey = this.prefix + key;
|
|
141
|
+
const result = await this.client.del(fullKey);
|
|
142
|
+
return result > 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear all cache entries with prefix
|
|
147
|
+
*/
|
|
148
|
+
async clear() {
|
|
149
|
+
if (!this.connected) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const keys = await this.client.keys(this.prefix + '*');
|
|
154
|
+
if (keys.length > 0) {
|
|
155
|
+
await this.client.del(keys);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get cache size (approximate)
|
|
161
|
+
* @returns {number} - Number of keys with prefix
|
|
162
|
+
*/
|
|
163
|
+
async size() {
|
|
164
|
+
if (!this.connected) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const keys = await this.client.keys(this.prefix + '*');
|
|
169
|
+
return keys.length;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = RedisCache;
|
|
174
|
+
|
package/src/core/app.js
CHANGED
|
@@ -238,6 +238,7 @@ class NavisApp {
|
|
|
238
238
|
* Start HTTP server (Node.js)
|
|
239
239
|
* @param {number} port - Port number
|
|
240
240
|
* @param {Function} callback - Optional callback
|
|
241
|
+
* @returns {Object} - HTTP server instance
|
|
241
242
|
*/
|
|
242
243
|
listen(port = 3000, callback) {
|
|
243
244
|
this.server = http.createServer((req, res) => {
|
|
@@ -251,6 +252,14 @@ class NavisApp {
|
|
|
251
252
|
|
|
252
253
|
return this.server;
|
|
253
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get server instance
|
|
258
|
+
* @returns {Object|null} - HTTP server instance
|
|
259
|
+
*/
|
|
260
|
+
getServer() {
|
|
261
|
+
return this.server;
|
|
262
|
+
}
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
module.exports = NavisApp;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful Shutdown Handler
|
|
3
|
+
* v5: Clean shutdown handling for Node.js servers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Graceful shutdown handler
|
|
8
|
+
* @param {Object} server - HTTP server instance
|
|
9
|
+
* @param {Object} options - Shutdown options
|
|
10
|
+
*/
|
|
11
|
+
function gracefulShutdown(server, options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
timeout = 10000, // 10 seconds default
|
|
14
|
+
onShutdown = async () => {}, // Cleanup function
|
|
15
|
+
signals = ['SIGTERM', 'SIGINT'],
|
|
16
|
+
log = console.log,
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
let isShuttingDown = false;
|
|
20
|
+
|
|
21
|
+
const shutdown = async (signal) => {
|
|
22
|
+
if (isShuttingDown) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isShuttingDown = true;
|
|
27
|
+
log(`Received ${signal}, starting graceful shutdown...`);
|
|
28
|
+
|
|
29
|
+
// Stop accepting new connections
|
|
30
|
+
server.close(() => {
|
|
31
|
+
log('HTTP server closed');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Set timeout for forced shutdown
|
|
35
|
+
const shutdownTimer = setTimeout(() => {
|
|
36
|
+
log('Forced shutdown after timeout');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}, timeout);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Run cleanup
|
|
42
|
+
await onShutdown();
|
|
43
|
+
clearTimeout(shutdownTimer);
|
|
44
|
+
log('Graceful shutdown completed');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
clearTimeout(shutdownTimer);
|
|
48
|
+
log('Error during shutdown:', error);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Register signal handlers
|
|
54
|
+
for (const signal of signals) {
|
|
55
|
+
process.on(signal, () => shutdown(signal));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle uncaught exceptions
|
|
59
|
+
process.on('uncaughtException', async (error) => {
|
|
60
|
+
log('Uncaught exception:', error);
|
|
61
|
+
await shutdown('uncaughtException');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Handle unhandled promise rejections
|
|
65
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
66
|
+
log('Unhandled rejection:', reason);
|
|
67
|
+
await shutdown('unhandledRejection');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
shutdown: () => shutdown('manual'),
|
|
72
|
+
isShuttingDown: () => isShuttingDown,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = gracefulShutdown;
|
|
77
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Middleware
|
|
3
|
+
* v5: Liveness and readiness probes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class HealthChecker {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.checks = options.checks || {};
|
|
9
|
+
this.livenessPath = options.livenessPath || '/health/live';
|
|
10
|
+
this.readinessPath = options.readinessPath || '/health/ready';
|
|
11
|
+
this.enabled = options.enabled !== false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a health check
|
|
16
|
+
* @param {string} name - Check name
|
|
17
|
+
* @param {Function} checkFn - Async function that returns true/false or throws
|
|
18
|
+
*/
|
|
19
|
+
addCheck(name, checkFn) {
|
|
20
|
+
this.checks[name] = checkFn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Remove a health check
|
|
25
|
+
* @param {string} name - Check name
|
|
26
|
+
*/
|
|
27
|
+
removeCheck(name) {
|
|
28
|
+
delete this.checks[name];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run all checks
|
|
33
|
+
* @param {boolean} includeReadiness - Include readiness checks
|
|
34
|
+
* @returns {Promise<Object>} - Health status
|
|
35
|
+
*/
|
|
36
|
+
async runChecks(includeReadiness = true) {
|
|
37
|
+
const results = {};
|
|
38
|
+
let allHealthy = true;
|
|
39
|
+
|
|
40
|
+
for (const [name, checkFn] of Object.entries(this.checks)) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await checkFn();
|
|
43
|
+
results[name] = {
|
|
44
|
+
status: result === false ? 'unhealthy' : 'healthy',
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (result === false) {
|
|
49
|
+
allHealthy = false;
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
results[name] = {
|
|
53
|
+
status: 'unhealthy',
|
|
54
|
+
error: error.message,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
allHealthy = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
status: allHealthy ? 'healthy' : 'unhealthy',
|
|
63
|
+
checks: results,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create health check middleware
|
|
70
|
+
* @returns {Function} - Middleware function
|
|
71
|
+
*/
|
|
72
|
+
middleware() {
|
|
73
|
+
return async (req, res, next) => {
|
|
74
|
+
if (!this.enabled) {
|
|
75
|
+
return next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const path = req.path || req.url;
|
|
79
|
+
|
|
80
|
+
// Liveness probe (always returns 200 if service is running)
|
|
81
|
+
if (path === this.livenessPath) {
|
|
82
|
+
res.statusCode = 200;
|
|
83
|
+
res.headers = res.headers || {};
|
|
84
|
+
res.headers['Content-Type'] = 'application/json';
|
|
85
|
+
res.body = {
|
|
86
|
+
status: 'alive',
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Readiness probe (checks all health checks)
|
|
93
|
+
if (path === this.readinessPath) {
|
|
94
|
+
const healthStatus = await this.runChecks(true);
|
|
95
|
+
res.statusCode = healthStatus.status === 'healthy' ? 200 : 503;
|
|
96
|
+
res.headers = res.headers || {};
|
|
97
|
+
res.headers['Content-Type'] = 'application/json';
|
|
98
|
+
res.body = healthStatus;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
next();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create health checker
|
|
109
|
+
* @param {Object} options - Health checker options
|
|
110
|
+
* @returns {HealthChecker} - Health checker instance
|
|
111
|
+
*/
|
|
112
|
+
function createHealthChecker(options = {}) {
|
|
113
|
+
return new HealthChecker(options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
HealthChecker,
|
|
118
|
+
createHealthChecker,
|
|
119
|
+
};
|
|
120
|
+
|
package/src/index.js
CHANGED
|
@@ -47,6 +47,16 @@ const {
|
|
|
47
47
|
notFoundHandler,
|
|
48
48
|
} = require('./errors/error-handler');
|
|
49
49
|
|
|
50
|
+
// v5: Enterprise Features
|
|
51
|
+
const Cache = require('./cache/cache');
|
|
52
|
+
const RedisCache = require('./cache/redis-cache');
|
|
53
|
+
const cache = require('./middleware/cache-middleware');
|
|
54
|
+
const cors = require('./middleware/cors');
|
|
55
|
+
const security = require('./middleware/security');
|
|
56
|
+
const compress = require('./middleware/compression');
|
|
57
|
+
const { HealthChecker, createHealthChecker } = require('./health/health-checker');
|
|
58
|
+
const gracefulShutdown = require('./core/graceful-shutdown');
|
|
59
|
+
|
|
50
60
|
module.exports = {
|
|
51
61
|
// Core
|
|
52
62
|
NavisApp,
|
|
@@ -100,6 +110,17 @@ module.exports = {
|
|
|
100
110
|
asyncHandler,
|
|
101
111
|
notFoundHandler,
|
|
102
112
|
|
|
113
|
+
// v5: Enterprise Features
|
|
114
|
+
Cache,
|
|
115
|
+
RedisCache,
|
|
116
|
+
cache,
|
|
117
|
+
cors,
|
|
118
|
+
security,
|
|
119
|
+
compress,
|
|
120
|
+
HealthChecker,
|
|
121
|
+
createHealthChecker,
|
|
122
|
+
gracefulShutdown,
|
|
123
|
+
|
|
103
124
|
// Utilities
|
|
104
125
|
response: {
|
|
105
126
|
success,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Middleware
|
|
3
|
+
* v5: Response caching middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create cache middleware
|
|
10
|
+
* @param {Object} options - Cache options
|
|
11
|
+
* @returns {Function} - Middleware function
|
|
12
|
+
*/
|
|
13
|
+
function cache(options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
ttl = 3600, // 1 hour in seconds
|
|
16
|
+
keyGenerator = (req) => {
|
|
17
|
+
// Default: method + path + query string
|
|
18
|
+
const queryStr = JSON.stringify(req.query || {});
|
|
19
|
+
return `${req.method}:${req.path}:${queryStr}`;
|
|
20
|
+
},
|
|
21
|
+
cacheStore = null, // Must be provided
|
|
22
|
+
skipCache = (req, res) => {
|
|
23
|
+
// Skip cache for non-GET requests or if status >= 400
|
|
24
|
+
return req.method !== 'GET' || (res.statusCode && res.statusCode >= 400);
|
|
25
|
+
},
|
|
26
|
+
vary = [], // Vary headers
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
if (!cacheStore) {
|
|
30
|
+
throw new Error('cacheStore is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return async (req, res, next) => {
|
|
34
|
+
// Generate cache key
|
|
35
|
+
const cacheKey = keyGenerator(req);
|
|
36
|
+
|
|
37
|
+
// Check if should skip cache
|
|
38
|
+
if (skipCache(req, res)) {
|
|
39
|
+
return next();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try to get from cache
|
|
43
|
+
try {
|
|
44
|
+
const cached = await (cacheStore.get ? cacheStore.get(cacheKey) : cacheStore.get(cacheKey));
|
|
45
|
+
|
|
46
|
+
if (cached) {
|
|
47
|
+
// Set cache headers
|
|
48
|
+
res.headers = res.headers || {};
|
|
49
|
+
res.headers['X-Cache'] = 'HIT';
|
|
50
|
+
res.headers['Cache-Control'] = `public, max-age=${ttl}`;
|
|
51
|
+
|
|
52
|
+
// Set Vary headers
|
|
53
|
+
if (vary.length > 0) {
|
|
54
|
+
res.headers['Vary'] = vary.join(', ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Return cached response
|
|
58
|
+
res.statusCode = cached.statusCode || 200;
|
|
59
|
+
res.body = cached.body;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Cache error - continue without cache
|
|
64
|
+
console.error('Cache get error:', error);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cache miss - continue to handler
|
|
68
|
+
res.headers = res.headers || {};
|
|
69
|
+
res.headers['X-Cache'] = 'MISS';
|
|
70
|
+
|
|
71
|
+
// Store original end/finish to capture response
|
|
72
|
+
const originalBody = res.body;
|
|
73
|
+
const originalStatusCode = res.statusCode;
|
|
74
|
+
|
|
75
|
+
// Wrap response to cache it
|
|
76
|
+
const originalFinish = res.finish || (() => {});
|
|
77
|
+
res.finish = async function(...args) {
|
|
78
|
+
// Only cache successful GET requests
|
|
79
|
+
if (req.method === 'GET' && res.statusCode < 400) {
|
|
80
|
+
try {
|
|
81
|
+
const cacheValue = {
|
|
82
|
+
statusCode: res.statusCode,
|
|
83
|
+
body: res.body,
|
|
84
|
+
headers: res.headers,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (cacheStore.set) {
|
|
88
|
+
await cacheStore.set(cacheKey, cacheValue, ttl * 1000);
|
|
89
|
+
} else {
|
|
90
|
+
cacheStore.set(cacheKey, cacheValue, ttl * 1000);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Cache set error:', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return originalFinish.apply(this, args);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = cache;
|
|
105
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Compression Middleware
|
|
3
|
+
* v5: Gzip and Brotli compression support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const zlib = require('zlib');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compression middleware
|
|
10
|
+
* @param {Object} options - Compression options
|
|
11
|
+
* @returns {Function} - Middleware function
|
|
12
|
+
*/
|
|
13
|
+
function compress(options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
level = 6, // Compression level (1-9)
|
|
16
|
+
threshold = 1024, // Minimum size to compress (bytes)
|
|
17
|
+
algorithm = 'gzip', // 'gzip' or 'brotli'
|
|
18
|
+
filter = (req, res) => {
|
|
19
|
+
// Default: compress JSON and text responses
|
|
20
|
+
const contentType = res.headers?.['content-type'] || '';
|
|
21
|
+
return contentType.includes('application/json') ||
|
|
22
|
+
contentType.includes('text/') ||
|
|
23
|
+
contentType.includes('application/javascript');
|
|
24
|
+
},
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
return async (req, res, next) => {
|
|
28
|
+
// Store original body setter
|
|
29
|
+
const originalBody = res.body;
|
|
30
|
+
const originalEnd = res.end || (() => {});
|
|
31
|
+
|
|
32
|
+
// Wrap response to compress before sending
|
|
33
|
+
res.end = function(...args) {
|
|
34
|
+
// Check if should compress
|
|
35
|
+
if (!filter(req, res)) {
|
|
36
|
+
return originalEnd.apply(this, args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get response body
|
|
40
|
+
let body = res.body;
|
|
41
|
+
if (typeof body === 'object') {
|
|
42
|
+
body = JSON.stringify(body);
|
|
43
|
+
} else if (typeof body !== 'string') {
|
|
44
|
+
body = String(body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check threshold
|
|
48
|
+
if (Buffer.byteLength(body, 'utf8') < threshold) {
|
|
49
|
+
return originalEnd.apply(this, args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if client supports compression
|
|
53
|
+
const acceptEncoding = req.headers['accept-encoding'] || req.headers['Accept-Encoding'] || '';
|
|
54
|
+
const supportsGzip = acceptEncoding.includes('gzip');
|
|
55
|
+
const supportsBrotli = acceptEncoding.includes('br');
|
|
56
|
+
|
|
57
|
+
// Choose compression algorithm
|
|
58
|
+
let compressed;
|
|
59
|
+
let encoding;
|
|
60
|
+
|
|
61
|
+
if (algorithm === 'brotli' && supportsBrotli) {
|
|
62
|
+
try {
|
|
63
|
+
compressed = zlib.brotliCompressSync(body, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } });
|
|
64
|
+
encoding = 'br';
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Fallback to gzip if brotli fails
|
|
67
|
+
compressed = zlib.gzipSync(body, { level });
|
|
68
|
+
encoding = 'gzip';
|
|
69
|
+
}
|
|
70
|
+
} else if (supportsGzip) {
|
|
71
|
+
compressed = zlib.gzipSync(body, { level });
|
|
72
|
+
encoding = 'gzip';
|
|
73
|
+
} else {
|
|
74
|
+
// No compression support
|
|
75
|
+
return originalEnd.apply(this, args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set compression headers
|
|
79
|
+
res.headers = res.headers || {};
|
|
80
|
+
res.headers['Content-Encoding'] = encoding;
|
|
81
|
+
res.headers['Vary'] = 'Accept-Encoding';
|
|
82
|
+
|
|
83
|
+
// Update content length
|
|
84
|
+
res.headers['Content-Length'] = compressed.length.toString();
|
|
85
|
+
|
|
86
|
+
// Update body with compressed data
|
|
87
|
+
res.body = compressed;
|
|
88
|
+
|
|
89
|
+
return originalEnd.apply(this, args);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
next();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = compress;
|
|
97
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Middleware
|
|
3
|
+
* v5: Cross-Origin Resource Sharing support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CORS middleware
|
|
8
|
+
* @param {Object} options - CORS options
|
|
9
|
+
* @returns {Function} - Middleware function
|
|
10
|
+
*/
|
|
11
|
+
function cors(options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
origin = '*',
|
|
14
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
15
|
+
allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
16
|
+
exposedHeaders = [],
|
|
17
|
+
credentials = false,
|
|
18
|
+
maxAge = 86400, // 24 hours
|
|
19
|
+
preflightContinue = false,
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
// Normalize origin
|
|
23
|
+
const allowedOrigins = Array.isArray(origin) ? origin : [origin];
|
|
24
|
+
const isWildcard = allowedOrigins.includes('*');
|
|
25
|
+
|
|
26
|
+
return async (req, res, next) => {
|
|
27
|
+
const requestOrigin = req.headers.origin || req.headers.Origin;
|
|
28
|
+
|
|
29
|
+
// Determine allowed origin
|
|
30
|
+
let allowedOrigin = null;
|
|
31
|
+
if (isWildcard) {
|
|
32
|
+
allowedOrigin = '*';
|
|
33
|
+
} else if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
|
|
34
|
+
allowedOrigin = requestOrigin;
|
|
35
|
+
} else if (allowedOrigins.length === 1 && allowedOrigins[0] !== '*') {
|
|
36
|
+
allowedOrigin = allowedOrigins[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle preflight requests
|
|
40
|
+
if (req.method === 'OPTIONS') {
|
|
41
|
+
res.headers = res.headers || {};
|
|
42
|
+
|
|
43
|
+
if (allowedOrigin) {
|
|
44
|
+
res.headers['Access-Control-Allow-Origin'] = allowedOrigin;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.headers['Access-Control-Allow-Methods'] = methods.join(', ');
|
|
48
|
+
res.headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
|
|
49
|
+
res.headers['Access-Control-Max-Age'] = maxAge.toString();
|
|
50
|
+
|
|
51
|
+
if (credentials) {
|
|
52
|
+
res.headers['Access-Control-Allow-Credentials'] = 'true';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (exposedHeaders.length > 0) {
|
|
56
|
+
res.headers['Access-Control-Expose-Headers'] = exposedHeaders.join(', ');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!preflightContinue) {
|
|
60
|
+
res.statusCode = 204;
|
|
61
|
+
res.body = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Set CORS headers for all responses
|
|
67
|
+
res.headers = res.headers || {};
|
|
68
|
+
|
|
69
|
+
if (allowedOrigin) {
|
|
70
|
+
res.headers['Access-Control-Allow-Origin'] = allowedOrigin;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (credentials && allowedOrigin && allowedOrigin !== '*') {
|
|
74
|
+
res.headers['Access-Control-Allow-Credentials'] = 'true';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (exposedHeaders.length > 0) {
|
|
78
|
+
res.headers['Access-Control-Expose-Headers'] = exposedHeaders.join(', ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
next();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = cors;
|
|
86
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers Middleware
|
|
3
|
+
* v5: Security headers for protection against common attacks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Security headers middleware
|
|
8
|
+
* @param {Object} options - Security options
|
|
9
|
+
* @returns {Function} - Middleware function
|
|
10
|
+
*/
|
|
11
|
+
function security(options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
helmet = true,
|
|
14
|
+
hsts = true,
|
|
15
|
+
hstsMaxAge = 31536000, // 1 year
|
|
16
|
+
hstsIncludeSubDomains = true,
|
|
17
|
+
hstsPreload = false,
|
|
18
|
+
noSniff = true,
|
|
19
|
+
xssFilter = true,
|
|
20
|
+
frameOptions = 'DENY', // DENY, SAMEORIGIN, or false
|
|
21
|
+
contentSecurityPolicy = false,
|
|
22
|
+
cspDirectives = {},
|
|
23
|
+
referrerPolicy = 'no-referrer',
|
|
24
|
+
permissionsPolicy = {},
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
return async (req, res, next) => {
|
|
28
|
+
res.headers = res.headers || {};
|
|
29
|
+
|
|
30
|
+
// X-Content-Type-Options
|
|
31
|
+
if (noSniff) {
|
|
32
|
+
res.headers['X-Content-Type-Options'] = 'nosniff';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// X-XSS-Protection
|
|
36
|
+
if (xssFilter) {
|
|
37
|
+
res.headers['X-XSS-Protection'] = '1; mode=block';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// X-Frame-Options
|
|
41
|
+
if (frameOptions) {
|
|
42
|
+
res.headers['X-Frame-Options'] = frameOptions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Strict-Transport-Security (HSTS)
|
|
46
|
+
if (hsts && req.headers['x-forwarded-proto'] === 'https') {
|
|
47
|
+
let hstsValue = `max-age=${hstsMaxAge}`;
|
|
48
|
+
if (hstsIncludeSubDomains) {
|
|
49
|
+
hstsValue += '; includeSubDomains';
|
|
50
|
+
}
|
|
51
|
+
if (hstsPreload) {
|
|
52
|
+
hstsValue += '; preload';
|
|
53
|
+
}
|
|
54
|
+
res.headers['Strict-Transport-Security'] = hstsValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Content-Security-Policy
|
|
58
|
+
if (contentSecurityPolicy) {
|
|
59
|
+
const directives = {
|
|
60
|
+
'default-src': ["'self'"],
|
|
61
|
+
'script-src': ["'self'"],
|
|
62
|
+
'style-src': ["'self'", "'unsafe-inline'"],
|
|
63
|
+
'img-src': ["'self'", 'data:', 'https:'],
|
|
64
|
+
'font-src': ["'self'"],
|
|
65
|
+
'connect-src': ["'self'"],
|
|
66
|
+
...cspDirectives,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const cspValue = Object.entries(directives)
|
|
70
|
+
.map(([key, values]) => {
|
|
71
|
+
const valuesStr = Array.isArray(values) ? values.join(' ') : values;
|
|
72
|
+
return `${key} ${valuesStr}`;
|
|
73
|
+
})
|
|
74
|
+
.join('; ');
|
|
75
|
+
|
|
76
|
+
res.headers['Content-Security-Policy'] = cspValue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Referrer-Policy
|
|
80
|
+
if (referrerPolicy) {
|
|
81
|
+
res.headers['Referrer-Policy'] = referrerPolicy;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Permissions-Policy (formerly Feature-Policy)
|
|
85
|
+
if (Object.keys(permissionsPolicy).length > 0) {
|
|
86
|
+
const policyValue = Object.entries(permissionsPolicy)
|
|
87
|
+
.map(([feature, allowlist]) => {
|
|
88
|
+
const allowlistStr = Array.isArray(allowlist) ? allowlist.join(', ') : allowlist;
|
|
89
|
+
return `${feature}=${allowlistStr}`;
|
|
90
|
+
})
|
|
91
|
+
.join(', ');
|
|
92
|
+
|
|
93
|
+
res.headers['Permissions-Policy'] = policyValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// X-Powered-By removal (security through obscurity)
|
|
97
|
+
if (helmet) {
|
|
98
|
+
// Remove X-Powered-By if present
|
|
99
|
+
delete res.headers['X-Powered-By'];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
next();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = security;
|
|
107
|
+
|