navis.js 3.1.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.
@@ -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
@@ -22,6 +22,41 @@ const { LazyInit, createLazyInit } = require('./utils/lazy-init');
22
22
  const LambdaHandler = require('./core/lambda-handler');
23
23
  const { coldStartTracker } = require('./middleware/cold-start-tracker');
24
24
 
25
+ // v4: Advanced Features
26
+ const AdvancedRouter = require('./core/advanced-router');
27
+ const { validate, ValidationError } = require('./validation/validator');
28
+ const {
29
+ authenticateJWT,
30
+ authenticateAPIKey,
31
+ authorize,
32
+ optionalAuth,
33
+ AuthenticationError,
34
+ AuthorizationError,
35
+ } = require('./auth/authenticator');
36
+ const { rateLimit, RateLimiter } = require('./middleware/rate-limiter');
37
+ const {
38
+ AppError,
39
+ NotFoundError,
40
+ BadRequestError,
41
+ UnauthorizedError,
42
+ ForbiddenError,
43
+ ConflictError,
44
+ InternalServerError,
45
+ errorHandler,
46
+ asyncHandler,
47
+ notFoundHandler,
48
+ } = require('./errors/error-handler');
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
+
25
60
  module.exports = {
26
61
  // Core
27
62
  NavisApp,
@@ -52,6 +87,40 @@ module.exports = {
52
87
  LambdaHandler,
53
88
  coldStartTracker,
54
89
 
90
+ // v4: Advanced Features
91
+ AdvancedRouter,
92
+ validate,
93
+ ValidationError,
94
+ authenticateJWT,
95
+ authenticateAPIKey,
96
+ authorize,
97
+ optionalAuth,
98
+ AuthenticationError,
99
+ AuthorizationError,
100
+ rateLimit,
101
+ RateLimiter,
102
+ AppError,
103
+ NotFoundError,
104
+ BadRequestError,
105
+ UnauthorizedError,
106
+ ForbiddenError,
107
+ ConflictError,
108
+ InternalServerError,
109
+ errorHandler,
110
+ asyncHandler,
111
+ notFoundHandler,
112
+
113
+ // v5: Enterprise Features
114
+ Cache,
115
+ RedisCache,
116
+ cache,
117
+ cors,
118
+ security,
119
+ compress,
120
+ HealthChecker,
121
+ createHealthChecker,
122
+ gracefulShutdown,
123
+
55
124
  // Utilities
56
125
  response: {
57
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,159 @@
1
+ /**
2
+ * Rate Limiting Middleware
3
+ * v4: In-memory rate limiting with configurable windows
4
+ */
5
+
6
+ class RateLimiter {
7
+ constructor(options = {}) {
8
+ this.windowMs = options.windowMs || 60000; // 1 minute default
9
+ this.max = options.max || 100; // 100 requests default
10
+ this.store = new Map(); // In-memory store
11
+ this.skipSuccessfulRequests = options.skipSuccessfulRequests || false;
12
+ this.skipFailedRequests = options.skipFailedRequests || false;
13
+ this.keyGenerator = options.keyGenerator || ((req) => {
14
+ // Default: IP address
15
+ return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
16
+ req.headers['x-real-ip'] ||
17
+ 'unknown';
18
+ });
19
+
20
+ // Cleanup old entries periodically
21
+ this.cleanupInterval = setInterval(() => {
22
+ this._cleanup();
23
+ }, this.windowMs);
24
+ }
25
+
26
+ /**
27
+ * Rate limit middleware
28
+ * @returns {Function} - Middleware function
29
+ */
30
+ middleware() {
31
+ const self = this; // Capture 'this' reference
32
+
33
+ return async (req, res, next) => {
34
+ const key = self.keyGenerator(req);
35
+ const now = Date.now();
36
+
37
+ // Get or create rate limit entry
38
+ let entry = self.store.get(key);
39
+
40
+ if (!entry || now - entry.resetTime > self.windowMs) {
41
+ // Create new entry or reset expired one
42
+ entry = {
43
+ count: 0,
44
+ resetTime: now + self.windowMs,
45
+ };
46
+ self.store.set(key, entry);
47
+ }
48
+
49
+ // Increment count
50
+ entry.count++;
51
+
52
+ // Set rate limit headers
53
+ res.headers = res.headers || {};
54
+ res.headers['X-RateLimit-Limit'] = self.max.toString();
55
+ res.headers['X-RateLimit-Remaining'] = Math.max(0, self.max - entry.count).toString();
56
+ res.headers['X-RateLimit-Reset'] = new Date(entry.resetTime).toISOString();
57
+
58
+ // Check if limit exceeded
59
+ if (entry.count > self.max) {
60
+ res.statusCode = 429;
61
+ res.body = {
62
+ error: 'Too many requests',
63
+ retryAfter: Math.ceil((entry.resetTime - now) / 1000),
64
+ };
65
+ return;
66
+ }
67
+
68
+ // Track response status for skip options
69
+ const originalStatusCode = res.statusCode;
70
+ const originalFinish = res.finish || (() => {});
71
+
72
+ // Wrap response finish to track status
73
+ res.finish = function(...args) {
74
+ const statusCode = res.statusCode || 200;
75
+
76
+ if (self.skipSuccessfulRequests && statusCode < 400) {
77
+ entry.count = Math.max(0, entry.count - 1);
78
+ }
79
+
80
+ if (self.skipFailedRequests && statusCode >= 400) {
81
+ entry.count = Math.max(0, entry.count - 1);
82
+ }
83
+
84
+ return originalFinish.apply(this, args);
85
+ };
86
+
87
+ next();
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Cleanup expired entries
93
+ * @private
94
+ */
95
+ _cleanup() {
96
+ const now = Date.now();
97
+ for (const [key, entry] of this.store.entries()) {
98
+ if (now - entry.resetTime > this.windowMs) {
99
+ this.store.delete(key);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Reset rate limit for a key
106
+ * @param {string} key - Rate limit key
107
+ */
108
+ reset(key) {
109
+ this.store.delete(key);
110
+ }
111
+
112
+ /**
113
+ * Get rate limit info for a key
114
+ * @param {string} key - Rate limit key
115
+ * @returns {Object|null} - Rate limit info
116
+ */
117
+ get(key) {
118
+ const entry = this.store.get(key);
119
+ if (!entry) return null;
120
+
121
+ const now = Date.now();
122
+ if (now - entry.resetTime > this.windowMs) {
123
+ this.store.delete(key);
124
+ return null;
125
+ }
126
+
127
+ return {
128
+ count: entry.count,
129
+ remaining: Math.max(0, this.max - entry.count),
130
+ resetTime: entry.resetTime,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Destroy rate limiter (cleanup)
136
+ */
137
+ destroy() {
138
+ if (this.cleanupInterval) {
139
+ clearInterval(this.cleanupInterval);
140
+ }
141
+ this.store.clear();
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create rate limit middleware
147
+ * @param {Object} options - Rate limit options
148
+ * @returns {Function} - Middleware function
149
+ */
150
+ function rateLimit(options = {}) {
151
+ const limiter = new RateLimiter(options);
152
+ return limiter.middleware();
153
+ }
154
+
155
+ module.exports = {
156
+ rateLimit,
157
+ RateLimiter,
158
+ };
159
+