navis.js 3.0.2 → 4.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,130 @@
1
+ /**
2
+ * Optimized Lambda Handler
3
+ * v3.1: Enhanced Lambda handler with cold start optimizations
4
+ */
5
+
6
+ class LambdaHandler {
7
+ constructor(app, options = {}) {
8
+ this.app = app;
9
+ this.isWarm = false;
10
+ this.initTime = Date.now();
11
+ this.invocationCount = 0;
12
+ this.coldStartCount = 0;
13
+ this.enableMetrics = options.enableMetrics !== false;
14
+ this.warmupPath = options.warmupPath || '/warmup';
15
+ }
16
+
17
+ /**
18
+ * Handle Lambda invocation
19
+ * @param {Object} event - Lambda event
20
+ * @param {Object} context - Lambda context
21
+ * @returns {Promise<Object>} - Lambda response
22
+ */
23
+ async handle(event, context) {
24
+ this.invocationCount++;
25
+
26
+ // Detect warm-up events
27
+ if (this.isWarmupEvent(event)) {
28
+ return this.handleWarmup();
29
+ }
30
+
31
+ // Track cold start
32
+ const isColdStart = !this.isWarm;
33
+ if (isColdStart) {
34
+ this.isWarm = true;
35
+ this.coldStartCount++;
36
+
37
+ if (this.enableMetrics) {
38
+ const coldStartDuration = Date.now() - this.initTime;
39
+ console.log(JSON.stringify({
40
+ type: 'cold-start',
41
+ duration: coldStartDuration,
42
+ memoryLimit: context.memoryLimitInMB,
43
+ requestId: context.requestId,
44
+ }));
45
+ }
46
+ }
47
+
48
+ // Add cold start headers to response
49
+ const response = await this.app.handleLambda(event);
50
+
51
+ if (isColdStart && response.headers) {
52
+ response.headers['X-Cold-Start'] = 'true';
53
+ response.headers['X-Init-Time'] = (Date.now() - this.initTime).toString();
54
+ }
55
+
56
+ return response;
57
+ }
58
+
59
+ /**
60
+ * Check if event is a warm-up event
61
+ * @param {Object} event - Lambda event
62
+ * @returns {boolean} - True if warm-up event
63
+ */
64
+ isWarmupEvent(event) {
65
+ // Check various warm-up event formats
66
+ if (event.source === 'serverless-plugin-warmup') {
67
+ return true;
68
+ }
69
+
70
+ if (event.warmup === true || event['serverless-plugin-warmup']) {
71
+ return true;
72
+ }
73
+
74
+ // Check if it's a warm-up HTTP request
75
+ if (event.httpMethod === 'GET' &&
76
+ (event.path === this.warmupPath || event.rawPath === this.warmupPath)) {
77
+ return true;
78
+ }
79
+
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Handle warm-up event
85
+ * @returns {Object} - Warm-up response
86
+ */
87
+ handleWarmup() {
88
+ // Mark as warm
89
+ this.isWarm = true;
90
+
91
+ return {
92
+ statusCode: 200,
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ body: JSON.stringify({
97
+ status: 'warmed',
98
+ invocationCount: this.invocationCount,
99
+ coldStartCount: this.coldStartCount,
100
+ }),
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Get handler statistics
106
+ * @returns {Object} - Handler statistics
107
+ */
108
+ getStats() {
109
+ return {
110
+ isWarm: this.isWarm,
111
+ invocationCount: this.invocationCount,
112
+ coldStartCount: this.coldStartCount,
113
+ initTime: this.initTime,
114
+ uptime: Date.now() - this.initTime,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Reset handler state (useful for testing)
120
+ */
121
+ reset() {
122
+ this.isWarm = false;
123
+ this.invocationCount = 0;
124
+ this.coldStartCount = 0;
125
+ this.initTime = Date.now();
126
+ }
127
+ }
128
+
129
+ module.exports = LambdaHandler;
130
+
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Enhanced Error Handling
3
+ * v4: Custom error classes and error handling middleware
4
+ */
5
+
6
+ class AppError extends Error {
7
+ constructor(message, statusCode = 500, code = null) {
8
+ super(message);
9
+ this.name = this.constructor.name;
10
+ this.statusCode = statusCode;
11
+ this.code = code || this.constructor.name.toUpperCase();
12
+ this.isOperational = true;
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+ }
16
+
17
+ class NotFoundError extends AppError {
18
+ constructor(message = 'Resource not found') {
19
+ super(message, 404, 'NOT_FOUND');
20
+ }
21
+ }
22
+
23
+ class BadRequestError extends AppError {
24
+ constructor(message = 'Bad request') {
25
+ super(message, 400, 'BAD_REQUEST');
26
+ }
27
+ }
28
+
29
+ class UnauthorizedError extends AppError {
30
+ constructor(message = 'Unauthorized') {
31
+ super(message, 401, 'UNAUTHORIZED');
32
+ }
33
+ }
34
+
35
+ class ForbiddenError extends AppError {
36
+ constructor(message = 'Forbidden') {
37
+ super(message, 403, 'FORBIDDEN');
38
+ }
39
+ }
40
+
41
+ class ConflictError extends AppError {
42
+ constructor(message = 'Conflict') {
43
+ super(message, 409, 'CONFLICT');
44
+ }
45
+ }
46
+
47
+ class InternalServerError extends AppError {
48
+ constructor(message = 'Internal server error') {
49
+ super(message, 500, 'INTERNAL_SERVER_ERROR');
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Error handler middleware
55
+ * @param {Object} options - Error handler options
56
+ * @returns {Function} - Error handling middleware
57
+ */
58
+ function errorHandler(options = {}) {
59
+ const {
60
+ format = 'json',
61
+ includeStack = process.env.NODE_ENV === 'development',
62
+ logErrors = true,
63
+ logger = console.error,
64
+ } = options;
65
+
66
+ return async (err, req, res, next) => {
67
+ // Determine status code
68
+ const statusCode = err.statusCode || err.status || 500;
69
+ const code = err.code || 'INTERNAL_SERVER_ERROR';
70
+ const message = err.message || 'Internal server error';
71
+
72
+ // Log error
73
+ if (logErrors) {
74
+ logger('Error:', {
75
+ message,
76
+ statusCode,
77
+ code,
78
+ stack: includeStack ? err.stack : undefined,
79
+ path: req.path,
80
+ method: req.method,
81
+ });
82
+ }
83
+
84
+ // Format error response
85
+ const errorResponse = {
86
+ error: {
87
+ message,
88
+ code,
89
+ statusCode,
90
+ },
91
+ };
92
+
93
+ // Include stack trace in development
94
+ if (includeStack && err.stack) {
95
+ errorResponse.error.stack = err.stack.split('\n');
96
+ }
97
+
98
+ // Include validation errors if present
99
+ if (err.errors && Array.isArray(err.errors)) {
100
+ errorResponse.error.errors = err.errors;
101
+ }
102
+
103
+ // Set response
104
+ res.statusCode = statusCode;
105
+
106
+ if (format === 'json') {
107
+ res.headers = res.headers || {};
108
+ res.headers['Content-Type'] = 'application/json';
109
+ res.body = errorResponse;
110
+ } else {
111
+ res.body = `${statusCode} ${message}`;
112
+ }
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Async error wrapper
118
+ * Wraps async route handlers to catch errors
119
+ * @param {Function} fn - Async function
120
+ * @returns {Function} - Wrapped function
121
+ */
122
+ function asyncHandler(fn) {
123
+ return (req, res, next) => {
124
+ Promise.resolve(fn(req, res, next)).catch(next);
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Not found handler
130
+ * @returns {Function} - Middleware function
131
+ */
132
+ function notFoundHandler() {
133
+ return (req, res) => {
134
+ res.statusCode = 404;
135
+ res.body = {
136
+ error: {
137
+ message: `Route ${req.method} ${req.path} not found`,
138
+ code: 'NOT_FOUND',
139
+ statusCode: 404,
140
+ },
141
+ };
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ AppError,
147
+ NotFoundError,
148
+ BadRequestError,
149
+ UnauthorizedError,
150
+ ForbiddenError,
151
+ ConflictError,
152
+ InternalServerError,
153
+ errorHandler,
154
+ asyncHandler,
155
+ notFoundHandler,
156
+ };
157
+
package/src/index.js CHANGED
@@ -16,6 +16,37 @@ const Logger = require('./observability/logger');
16
16
  const Metrics = require('./observability/metrics');
17
17
  const Tracer = require('./observability/tracer');
18
18
 
19
+ // v3.1: Lambda Optimizations
20
+ const { getPool, ServiceClientPool } = require('./utils/service-client-pool');
21
+ const { LazyInit, createLazyInit } = require('./utils/lazy-init');
22
+ const LambdaHandler = require('./core/lambda-handler');
23
+ const { coldStartTracker } = require('./middleware/cold-start-tracker');
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
+
19
50
  module.exports = {
20
51
  // Core
21
52
  NavisApp,
@@ -38,6 +69,37 @@ module.exports = {
38
69
  Metrics,
39
70
  Tracer,
40
71
 
72
+ // v3.1: Lambda Optimizations
73
+ ServiceClientPool,
74
+ getPool,
75
+ LazyInit,
76
+ createLazyInit,
77
+ LambdaHandler,
78
+ coldStartTracker,
79
+
80
+ // v4: Advanced Features
81
+ AdvancedRouter,
82
+ validate,
83
+ ValidationError,
84
+ authenticateJWT,
85
+ authenticateAPIKey,
86
+ authorize,
87
+ optionalAuth,
88
+ AuthenticationError,
89
+ AuthorizationError,
90
+ rateLimit,
91
+ RateLimiter,
92
+ AppError,
93
+ NotFoundError,
94
+ BadRequestError,
95
+ UnauthorizedError,
96
+ ForbiddenError,
97
+ ConflictError,
98
+ InternalServerError,
99
+ errorHandler,
100
+ asyncHandler,
101
+ notFoundHandler,
102
+
41
103
  // Utilities
42
104
  response: {
43
105
  success,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cold Start Tracker Middleware
3
+ * v3.1: Track and log cold start metrics
4
+ */
5
+
6
+ let isFirstInvocation = true;
7
+ let initTime = Date.now();
8
+
9
+ /**
10
+ * Cold start tracking middleware
11
+ * Tracks cold starts and adds headers to response
12
+ * @param {Object} req - Request object
13
+ * @param {Object} res - Response object
14
+ * @param {Function} next - Next middleware
15
+ */
16
+ function coldStartTracker(req, res, next) {
17
+ if (isFirstInvocation) {
18
+ const coldStartDuration = Date.now() - initTime;
19
+
20
+ // Add cold start info to response headers
21
+ if (res.headers) {
22
+ res.headers['X-Cold-Start'] = 'true';
23
+ res.headers['X-Cold-Start-Duration'] = coldStartDuration.toString();
24
+ }
25
+
26
+ // Log cold start (structured logging)
27
+ console.log(JSON.stringify({
28
+ type: 'cold-start',
29
+ duration: coldStartDuration,
30
+ path: req.path || req.url,
31
+ method: req.method,
32
+ }));
33
+
34
+ isFirstInvocation = false;
35
+ } else {
36
+ if (res.headers) {
37
+ res.headers['X-Cold-Start'] = 'false';
38
+ }
39
+ }
40
+
41
+ next();
42
+ }
43
+
44
+ /**
45
+ * Reset cold start tracker (useful for testing)
46
+ */
47
+ function resetColdStartTracker() {
48
+ isFirstInvocation = true;
49
+ initTime = Date.now();
50
+ }
51
+
52
+ module.exports = {
53
+ coldStartTracker,
54
+ resetColdStartTracker,
55
+ };
56
+
@@ -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
+
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Lazy Initialization Utility
3
+ * v3.1: Defer heavy initialization until needed (reduces cold start time)
4
+ */
5
+
6
+ class LazyInit {
7
+ constructor(options = {}) {
8
+ this.initialized = false;
9
+ this.initPromise = null;
10
+ this.initFn = null;
11
+ this.autoInit = options.autoInit !== false; // Auto-init on first access
12
+ this.cacheResult = options.cacheResult !== false; // Cache initialization result
13
+ this.cachedResult = null;
14
+ }
15
+
16
+ /**
17
+ * Initialize with a function
18
+ * @param {Function} initFn - Initialization function (can be async)
19
+ * @returns {Promise} - Initialization promise
20
+ */
21
+ async init(initFn) {
22
+ if (this.initialized && this.cacheResult) {
23
+ return this.cachedResult;
24
+ }
25
+
26
+ if (!this.initPromise) {
27
+ this.initFn = initFn;
28
+ this.initPromise = Promise.resolve(initFn()).then(result => {
29
+ this.initialized = true;
30
+ if (this.cacheResult) {
31
+ this.cachedResult = result;
32
+ }
33
+ return result;
34
+ }).catch(error => {
35
+ // Reset on error so it can be retried
36
+ this.initPromise = null;
37
+ throw error;
38
+ });
39
+ }
40
+
41
+ return this.initPromise;
42
+ }
43
+
44
+ /**
45
+ * Check if initialized
46
+ * @returns {boolean} - True if initialized
47
+ */
48
+ isInitialized() {
49
+ return this.initialized;
50
+ }
51
+
52
+ /**
53
+ * Get cached result (if available)
54
+ * @returns {*} - Cached initialization result
55
+ */
56
+ getCached() {
57
+ return this.cachedResult;
58
+ }
59
+
60
+ /**
61
+ * Reset initialization state
62
+ */
63
+ reset() {
64
+ this.initialized = false;
65
+ this.initPromise = null;
66
+ this.cachedResult = null;
67
+ }
68
+
69
+ /**
70
+ * Execute function with lazy initialization
71
+ * @param {Function} fn - Function to execute after initialization
72
+ * @returns {Promise} - Result of function execution
73
+ */
74
+ async withInit(fn) {
75
+ if (!this.initialized && this.initFn) {
76
+ await this.init(this.initFn);
77
+ }
78
+ return await fn(this.cachedResult);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create a lazy initializer
84
+ * @param {Function} initFn - Initialization function
85
+ * @param {Object} options - Options
86
+ * @returns {LazyInit} - LazyInit instance
87
+ */
88
+ function createLazyInit(initFn, options = {}) {
89
+ const lazy = new LazyInit(options);
90
+ if (initFn) {
91
+ lazy.initFn = initFn;
92
+ }
93
+ return lazy;
94
+ }
95
+
96
+ module.exports = {
97
+ LazyInit,
98
+ createLazyInit,
99
+ };
100
+