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 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 (Current)
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 ✅ (Current)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navis.js",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
+