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.
- package/README.md +66 -1
- package/examples/lambda-optimized.js +103 -0
- package/examples/lambda.js +30 -29
- package/examples/v4-features-demo.js +171 -0
- package/package.json +1 -1
- package/src/auth/authenticator.js +223 -0
- package/src/core/advanced-router.js +186 -0
- package/src/core/app.js +255 -176
- package/src/core/lambda-handler.js +130 -0
- package/src/errors/error-handler.js +157 -0
- package/src/index.js +62 -0
- package/src/middleware/cold-start-tracker.js +56 -0
- package/src/middleware/rate-limiter.js +159 -0
- package/src/utils/lazy-init.js +100 -0
- package/src/utils/service-client-pool.js +131 -0
- package/src/validation/validator.js +301 -0
package/README.md
CHANGED
|
@@ -112,6 +112,23 @@ navis metrics
|
|
|
112
112
|
- ✅ **Distributed tracing** - Trace and span management
|
|
113
113
|
- ✅ **Enhanced CLI** - Test and metrics commands
|
|
114
114
|
|
|
115
|
+
### v3.1
|
|
116
|
+
|
|
117
|
+
- ✅ **Lambda cold start optimization** - Connection pooling, lazy initialization
|
|
118
|
+
- ✅ **ServiceClientPool** - Reuse HTTP connections across invocations
|
|
119
|
+
- ✅ **LazyInit utility** - Defer heavy operations until needed
|
|
120
|
+
- ✅ **LambdaHandler** - Optimized handler with warm-up support
|
|
121
|
+
- ✅ **Cold start tracking** - Monitor and log cold start metrics
|
|
122
|
+
|
|
123
|
+
### v4 (Current)
|
|
124
|
+
|
|
125
|
+
- ✅ **Advanced routing** - Route parameters (`:id`), nested routes, PATCH method
|
|
126
|
+
- ✅ **Request validation** - Schema-based validation with comprehensive rules
|
|
127
|
+
- ✅ **Authentication** - JWT and API Key authentication
|
|
128
|
+
- ✅ **Authorization** - Role-based access control
|
|
129
|
+
- ✅ **Rate limiting** - In-memory rate limiting with configurable windows
|
|
130
|
+
- ✅ **Enhanced error handling** - Custom error classes and error handler middleware
|
|
131
|
+
|
|
115
132
|
## API Reference
|
|
116
133
|
|
|
117
134
|
### NavisApp
|
|
@@ -248,12 +265,55 @@ await nats.connect();
|
|
|
248
265
|
await nats.publish('user.created', { userId: 123 });
|
|
249
266
|
```
|
|
250
267
|
|
|
268
|
+
### Lambda Optimization (v3.1)
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
const {
|
|
272
|
+
NavisApp,
|
|
273
|
+
getPool,
|
|
274
|
+
LambdaHandler,
|
|
275
|
+
coldStartTracker,
|
|
276
|
+
LazyInit,
|
|
277
|
+
} = require('navis.js');
|
|
278
|
+
|
|
279
|
+
// Initialize app OUTSIDE handler (reused across invocations)
|
|
280
|
+
const app = new NavisApp();
|
|
281
|
+
app.use(coldStartTracker);
|
|
282
|
+
|
|
283
|
+
// Connection pooling - reuse HTTP connections
|
|
284
|
+
const client = getPool().get('http://api.example.com', {
|
|
285
|
+
timeout: 3000,
|
|
286
|
+
maxRetries: 2,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Lazy initialization - defer heavy operations
|
|
290
|
+
const dbConnection = new LazyInit();
|
|
291
|
+
app.get('/users', async (req, res) => {
|
|
292
|
+
const db = await dbConnection.init(async () => {
|
|
293
|
+
return await connectToDatabase(); // Only runs once
|
|
294
|
+
});
|
|
295
|
+
res.body = await db.query('SELECT * FROM users');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Optimized Lambda handler
|
|
299
|
+
const handler = new LambdaHandler(app, {
|
|
300
|
+
enableMetrics: true,
|
|
301
|
+
warmupPath: '/warmup',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
exports.handler = async (event, context) => {
|
|
305
|
+
return await handler.handle(event, context);
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
251
309
|
## Examples
|
|
252
310
|
|
|
253
311
|
See the `examples/` directory:
|
|
254
312
|
|
|
255
313
|
- `server.js` - Node.js HTTP server example
|
|
256
314
|
- `lambda.js` - AWS Lambda handler example
|
|
315
|
+
- `lambda-optimized.js` - Optimized Lambda handler with cold start optimizations (v3.1)
|
|
316
|
+
- `v4-features-demo.js` - v4 features demonstration (routing, validation, auth, rate limiting, etc.)
|
|
257
317
|
- `service-client-demo.js` - ServiceClient usage example
|
|
258
318
|
- `v2-features-demo.js` - v2 features demonstration (retry, circuit breaker, etc.)
|
|
259
319
|
- `v3-features-demo.js` - v3 features demonstration (messaging, observability, etc.)
|
|
@@ -266,13 +326,18 @@ Core functionality: routing, middleware, Lambda support, ServiceClient
|
|
|
266
326
|
### v2 ✅
|
|
267
327
|
Resilience patterns: retry, circuit breaker, service discovery, CLI generators
|
|
268
328
|
|
|
269
|
-
### v3 ✅
|
|
329
|
+
### v3 ✅
|
|
270
330
|
Advanced features: async messaging (SQS/Kafka/NATS), observability, enhanced CLI
|
|
271
331
|
|
|
332
|
+
### v4 ✅ (Current)
|
|
333
|
+
Production-ready: advanced routing, validation, authentication, rate limiting, error handling
|
|
334
|
+
|
|
272
335
|
## Documentation
|
|
273
336
|
|
|
274
337
|
- [V2 Features Guide](./V2_FEATURES.md) - Complete v2 features documentation
|
|
275
338
|
- [V3 Features Guide](./V3_FEATURES.md) - Complete v3 features documentation
|
|
339
|
+
- [V4 Features Guide](./V4_FEATURES.md) - Complete v4 features documentation
|
|
340
|
+
- [Lambda Optimization Guide](./LAMBDA_OPTIMIZATION.md) - Lambda cold start optimization guide (v3.1)
|
|
276
341
|
- [Verification Guide v2](./VERIFY_V2.md) - How to verify v2 features
|
|
277
342
|
- [Verification Guide v3](./VERIFY_V3.md) - How to verify v3 features
|
|
278
343
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimized Lambda Handler Example
|
|
3
|
+
* v3.1: Best practices for reducing cold start time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { NavisApp, response, getPool } = require('../src/index');
|
|
7
|
+
const LambdaHandler = require('../src/core/lambda-handler');
|
|
8
|
+
const { coldStartTracker } = require('../src/middleware/cold-start-tracker');
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// CRITICAL: Initialize app OUTSIDE handler
|
|
12
|
+
// This ensures the app is reused across invocations
|
|
13
|
+
// ============================================
|
|
14
|
+
const app = new NavisApp();
|
|
15
|
+
|
|
16
|
+
// Add cold start tracking middleware
|
|
17
|
+
app.use(coldStartTracker);
|
|
18
|
+
|
|
19
|
+
// ============================================
|
|
20
|
+
// Register routes at MODULE LEVEL (not in handler)
|
|
21
|
+
// This runs once per container, not per invocation
|
|
22
|
+
// ============================================
|
|
23
|
+
app.get('/', (req, res) => {
|
|
24
|
+
res.statusCode = 200;
|
|
25
|
+
res.body = {
|
|
26
|
+
message: 'Welcome to Navis.js Lambda (Optimized)!',
|
|
27
|
+
optimized: true,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.get('/health', (req, res) => {
|
|
32
|
+
res.statusCode = 200;
|
|
33
|
+
res.body = { status: 'ok' };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.get('/warmup', (req, res) => {
|
|
37
|
+
res.statusCode = 200;
|
|
38
|
+
res.body = { status: 'warmed' };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Example: Using ServiceClient with connection pooling
|
|
42
|
+
app.get('/external', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
// Get client from pool (reuses connections)
|
|
45
|
+
const client = getPool().get('http://api.example.com', {
|
|
46
|
+
timeout: 3000,
|
|
47
|
+
maxRetries: 2,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = await client.get('/data');
|
|
51
|
+
res.statusCode = 200;
|
|
52
|
+
res.body = { data: result.data };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
res.statusCode = 500;
|
|
55
|
+
res.body = { error: error.message };
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// OPTIMIZED HANDLER
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
// Create handler instance (reused across invocations)
|
|
64
|
+
const handler = new LambdaHandler(app, {
|
|
65
|
+
enableMetrics: true,
|
|
66
|
+
warmupPath: '/warmup',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Cache the handler function (V8 optimization)
|
|
70
|
+
let cachedHandler = null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Lambda handler - optimized for cold starts
|
|
74
|
+
* @param {Object} event - Lambda event
|
|
75
|
+
* @param {Object} context - Lambda context
|
|
76
|
+
*/
|
|
77
|
+
exports.handler = async (event, context) => {
|
|
78
|
+
// Reuse handler function (V8 JIT optimization)
|
|
79
|
+
if (!cachedHandler) {
|
|
80
|
+
cachedHandler = async (event, context) => {
|
|
81
|
+
return await handler.handle(event, context);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return await cachedHandler(event, context);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Optional: Pre-warm function
|
|
90
|
+
* Can be called during container initialization
|
|
91
|
+
*/
|
|
92
|
+
exports.preWarm = async () => {
|
|
93
|
+
// Pre-initialize any heavy operations
|
|
94
|
+
// This runs once per container
|
|
95
|
+
console.log('Pre-warming Lambda container...');
|
|
96
|
+
|
|
97
|
+
// Example: Pre-initialize service clients
|
|
98
|
+
const pool = getPool();
|
|
99
|
+
pool.get('http://api.example.com', { timeout: 3000 });
|
|
100
|
+
|
|
101
|
+
console.log('Pre-warming complete');
|
|
102
|
+
};
|
|
103
|
+
|
package/examples/lambda.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
const { NavisApp } = require('../src/index');
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
res.
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
res.
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
res.
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
const { NavisApp } = require('../src/index');
|
|
2
|
+
|
|
3
|
+
// Initialize app at module level (reused across invocations)
|
|
4
|
+
const app = new NavisApp();
|
|
5
|
+
|
|
6
|
+
// Middleware example
|
|
7
|
+
app.use((req, res, next) => {
|
|
8
|
+
console.log(`Lambda: ${req.method} ${req.path}`);
|
|
9
|
+
next();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Routes registered at module level (not in handler)
|
|
13
|
+
app.get('/', (req, res) => {
|
|
14
|
+
res.statusCode = 200;
|
|
15
|
+
res.body = { message: 'Welcome to Navis.js Lambda!' };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.get('/health', (req, res) => {
|
|
19
|
+
res.statusCode = 200;
|
|
20
|
+
res.body = { status: 'ok' };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
app.post('/echo', (req, res) => {
|
|
24
|
+
res.statusCode = 200;
|
|
25
|
+
res.body = { echo: req.body };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Lambda handler - optimized for reuse
|
|
29
|
+
exports.handler = async (event) => {
|
|
30
|
+
return await app.handleLambda(event);
|
|
30
31
|
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navis.js v4 Features Demo
|
|
3
|
+
* Demonstrates advanced routing, validation, auth, rate limiting, and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
NavisApp,
|
|
8
|
+
response,
|
|
9
|
+
validate,
|
|
10
|
+
authenticateJWT,
|
|
11
|
+
authorize,
|
|
12
|
+
rateLimit,
|
|
13
|
+
errorHandler,
|
|
14
|
+
asyncHandler,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
BadRequestError,
|
|
17
|
+
} = require('../src/index');
|
|
18
|
+
|
|
19
|
+
const app = new NavisApp();
|
|
20
|
+
|
|
21
|
+
// Set error handler
|
|
22
|
+
app.setErrorHandler(errorHandler({
|
|
23
|
+
includeStack: true,
|
|
24
|
+
logErrors: true,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Global rate limiting
|
|
28
|
+
app.use(rateLimit({
|
|
29
|
+
windowMs: 60000, // 1 minute
|
|
30
|
+
max: 100, // 100 requests per minute
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Example 1: Route Parameters
|
|
34
|
+
console.log('\n=== Route Parameters (v4) ===\n');
|
|
35
|
+
|
|
36
|
+
app.get('/users/:id', (req, res) => {
|
|
37
|
+
console.log('User ID:', req.params.id);
|
|
38
|
+
response.success(res, {
|
|
39
|
+
message: `Fetching user ${req.params.id}`,
|
|
40
|
+
userId: req.params.id,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.get('/users/:id/posts/:postId', (req, res) => {
|
|
45
|
+
console.log('User ID:', req.params.id);
|
|
46
|
+
console.log('Post ID:', req.params.postId);
|
|
47
|
+
response.success(res, {
|
|
48
|
+
userId: req.params.id,
|
|
49
|
+
postId: req.params.postId,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Example 2: Request Validation
|
|
54
|
+
console.log('\n=== Request Validation (v4) ===\n');
|
|
55
|
+
|
|
56
|
+
const createUserSchema = {
|
|
57
|
+
body: {
|
|
58
|
+
name: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
required: true,
|
|
61
|
+
minLength: 3,
|
|
62
|
+
maxLength: 50,
|
|
63
|
+
},
|
|
64
|
+
email: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
required: true,
|
|
67
|
+
format: 'email',
|
|
68
|
+
},
|
|
69
|
+
age: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
min: 18,
|
|
72
|
+
max: 100,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
app.post('/users', validate(createUserSchema), (req, res) => {
|
|
78
|
+
console.log('Validated body:', req.body);
|
|
79
|
+
response.success(res, {
|
|
80
|
+
message: 'User created',
|
|
81
|
+
user: req.body,
|
|
82
|
+
}, 201);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Example 3: Authentication (Mock JWT)
|
|
86
|
+
console.log('\n=== Authentication (v4) ===\n');
|
|
87
|
+
|
|
88
|
+
// Mock JWT secret (in production, use environment variable)
|
|
89
|
+
process.env.JWT_SECRET = 'your-secret-key';
|
|
90
|
+
|
|
91
|
+
// Protected route
|
|
92
|
+
app.get('/profile', authenticateJWT(), (req, res) => {
|
|
93
|
+
response.success(res, {
|
|
94
|
+
message: 'Protected route',
|
|
95
|
+
user: req.user,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Role-based authorization
|
|
100
|
+
app.get('/admin', authenticateJWT(), authorize(['admin']), (req, res) => {
|
|
101
|
+
response.success(res, {
|
|
102
|
+
message: 'Admin area',
|
|
103
|
+
user: req.user,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Example 4: Error Handling
|
|
108
|
+
console.log('\n=== Error Handling (v4) ===\n');
|
|
109
|
+
|
|
110
|
+
app.get('/error-test', asyncHandler(async (req, res) => {
|
|
111
|
+
throw new BadRequestError('This is a bad request');
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
app.get('/not-found-test', asyncHandler(async (req, res) => {
|
|
115
|
+
throw new NotFoundError('Resource not found');
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Example 5: Rate Limiting per Route
|
|
119
|
+
console.log('\n=== Rate Limiting (v4) ===\n');
|
|
120
|
+
|
|
121
|
+
app.post('/login', rateLimit({ max: 5, windowMs: 60000 }), (req, res) => {
|
|
122
|
+
response.success(res, {
|
|
123
|
+
message: 'Login endpoint (5 requests per minute)',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Example 6: Query Parameters
|
|
128
|
+
app.get('/search', (req, res) => {
|
|
129
|
+
console.log('Query params:', req.query);
|
|
130
|
+
response.success(res, {
|
|
131
|
+
query: req.query.q,
|
|
132
|
+
filters: req.query,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Example 7: PATCH Method (v4)
|
|
137
|
+
app.patch('/users/:id', validate({
|
|
138
|
+
body: {
|
|
139
|
+
name: { type: 'string', required: false },
|
|
140
|
+
email: { type: 'string', required: false, format: 'email' },
|
|
141
|
+
},
|
|
142
|
+
}), (req, res) => {
|
|
143
|
+
response.success(res, {
|
|
144
|
+
message: `Updating user ${req.params.id}`,
|
|
145
|
+
updates: req.body,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Start server
|
|
150
|
+
const PORT = 3000;
|
|
151
|
+
app.listen(PORT, () => {
|
|
152
|
+
console.log(`\n🚀 Navis.js v4 Features Demo Server`);
|
|
153
|
+
console.log(`📡 Listening on http://localhost:${PORT}\n`);
|
|
154
|
+
console.log('Available endpoints:');
|
|
155
|
+
console.log(' GET /users/:id');
|
|
156
|
+
console.log(' GET /users/:id/posts/:postId');
|
|
157
|
+
console.log(' POST /users (with validation)');
|
|
158
|
+
console.log(' GET /profile (requires JWT)');
|
|
159
|
+
console.log(' GET /admin (requires admin role)');
|
|
160
|
+
console.log(' GET /error-test');
|
|
161
|
+
console.log(' GET /not-found-test');
|
|
162
|
+
console.log(' POST /login (rate limited)');
|
|
163
|
+
console.log(' GET /search?q=test');
|
|
164
|
+
console.log(' PATCH /users/:id');
|
|
165
|
+
console.log('\n💡 Test with:');
|
|
166
|
+
console.log(' curl http://localhost:3000/users/123');
|
|
167
|
+
console.log(' curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d \'{"name":"John","email":"john@example.com","age":25}\'');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
module.exports = app;
|
|
171
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication and Authorization Middleware
|
|
3
|
+
* v4: JWT, API Key, and role-based access control
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
class AuthenticationError extends Error {
|
|
9
|
+
constructor(message, statusCode = 401) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'AuthenticationError';
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class AuthorizationError extends Error {
|
|
17
|
+
constructor(message, statusCode = 403) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'AuthorizationError';
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* JWT Authentication Middleware
|
|
26
|
+
* @param {Object} options - JWT options
|
|
27
|
+
* @returns {Function} - Middleware function
|
|
28
|
+
*/
|
|
29
|
+
function authenticateJWT(options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
secret = process.env.JWT_SECRET,
|
|
32
|
+
algorithms = ['HS256'],
|
|
33
|
+
header = 'authorization',
|
|
34
|
+
extractToken = (req) => {
|
|
35
|
+
const authHeader = req.headers[header] || req.headers[header.toLowerCase()];
|
|
36
|
+
if (!authHeader) return null;
|
|
37
|
+
|
|
38
|
+
// Support "Bearer <token>" format
|
|
39
|
+
const parts = authHeader.split(' ');
|
|
40
|
+
return parts.length === 2 && parts[0].toLowerCase() === 'bearer'
|
|
41
|
+
? parts[1]
|
|
42
|
+
: authHeader;
|
|
43
|
+
},
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
if (!secret) {
|
|
47
|
+
throw new Error('JWT secret is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return async (req, res, next) => {
|
|
51
|
+
try {
|
|
52
|
+
const token = extractToken(req);
|
|
53
|
+
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new AuthenticationError('No authentication token provided');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Simple JWT decode and verify (for HS256)
|
|
59
|
+
// In production, use a proper JWT library like jsonwebtoken
|
|
60
|
+
const decoded = verifyJWT(token, secret);
|
|
61
|
+
|
|
62
|
+
if (!decoded) {
|
|
63
|
+
throw new AuthenticationError('Invalid or expired token');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Attach user info to request
|
|
67
|
+
req.user = decoded;
|
|
68
|
+
req.token = token;
|
|
69
|
+
|
|
70
|
+
next();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof AuthenticationError) {
|
|
73
|
+
res.statusCode = error.statusCode;
|
|
74
|
+
res.body = { error: error.message };
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simple JWT verification (HS256 only)
|
|
84
|
+
* For production, use jsonwebtoken library
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
function verifyJWT(token, secret) {
|
|
88
|
+
try {
|
|
89
|
+
const parts = token.split('.');
|
|
90
|
+
if (parts.length !== 3) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
95
|
+
|
|
96
|
+
// Verify signature
|
|
97
|
+
const signature = Buffer.from(signatureB64, 'base64url').toString('hex');
|
|
98
|
+
const expectedSignature = crypto
|
|
99
|
+
.createHmac('sha256', secret)
|
|
100
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
101
|
+
.digest('hex');
|
|
102
|
+
|
|
103
|
+
if (signature !== expectedSignature) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Decode payload
|
|
108
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
109
|
+
|
|
110
|
+
// Check expiration
|
|
111
|
+
if (payload.exp && Date.now() >= payload.exp * 1000) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return payload;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* API Key Authentication Middleware
|
|
123
|
+
* @param {Object} options - API Key options
|
|
124
|
+
* @returns {Function} - Middleware function
|
|
125
|
+
*/
|
|
126
|
+
function authenticateAPIKey(options = {}) {
|
|
127
|
+
const {
|
|
128
|
+
header = 'x-api-key',
|
|
129
|
+
keys = process.env.API_KEYS ? process.env.API_KEYS.split(',') : [],
|
|
130
|
+
validateKey = (key) => keys.includes(key),
|
|
131
|
+
} = options;
|
|
132
|
+
|
|
133
|
+
return async (req, res, next) => {
|
|
134
|
+
try {
|
|
135
|
+
const apiKey = req.headers[header] || req.headers[header.toLowerCase()];
|
|
136
|
+
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
throw new AuthenticationError('API key is required');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isValid = await validateKey(apiKey);
|
|
142
|
+
|
|
143
|
+
if (!isValid) {
|
|
144
|
+
throw new AuthenticationError('Invalid API key');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
req.apiKey = apiKey;
|
|
148
|
+
next();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof AuthenticationError) {
|
|
151
|
+
res.statusCode = error.statusCode;
|
|
152
|
+
res.body = { error: error.message };
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Role-based Authorization Middleware
|
|
162
|
+
* @param {string|Array} allowedRoles - Allowed roles
|
|
163
|
+
* @returns {Function} - Middleware function
|
|
164
|
+
*/
|
|
165
|
+
function authorize(allowedRoles) {
|
|
166
|
+
const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
|
167
|
+
|
|
168
|
+
return async (req, res, next) => {
|
|
169
|
+
try {
|
|
170
|
+
if (!req.user) {
|
|
171
|
+
throw new AuthenticationError('Authentication required');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const userRoles = req.user.roles || req.user.role ? [req.user.role] : [];
|
|
175
|
+
|
|
176
|
+
const hasRole = roles.some(role => userRoles.includes(role));
|
|
177
|
+
|
|
178
|
+
if (!hasRole) {
|
|
179
|
+
throw new AuthorizationError('Insufficient permissions');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
next();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof AuthenticationError || error instanceof AuthorizationError) {
|
|
185
|
+
res.statusCode = error.statusCode;
|
|
186
|
+
res.body = { error: error.message };
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Optional authentication (doesn't fail if no token)
|
|
196
|
+
* @param {Object} options - JWT options
|
|
197
|
+
* @returns {Function} - Middleware function
|
|
198
|
+
*/
|
|
199
|
+
function optionalAuth(options = {}) {
|
|
200
|
+
const jwtAuth = authenticateJWT(options);
|
|
201
|
+
|
|
202
|
+
return async (req, res, next) => {
|
|
203
|
+
try {
|
|
204
|
+
await jwtAuth(req, res, () => {
|
|
205
|
+
// Continue even if auth fails
|
|
206
|
+
next();
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// If auth fails, continue without user
|
|
210
|
+
next();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
authenticateJWT,
|
|
217
|
+
authenticateAPIKey,
|
|
218
|
+
authorize,
|
|
219
|
+
optionalAuth,
|
|
220
|
+
AuthenticationError,
|
|
221
|
+
AuthorizationError,
|
|
222
|
+
};
|
|
223
|
+
|