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.
- package/README.md +30 -2
- package/examples/v4-features-demo.js +171 -0
- package/examples/v5-features-demo.js +167 -0
- package/package.json +1 -1
- package/src/auth/authenticator.js +223 -0
- package/src/cache/cache.js +157 -0
- package/src/cache/redis-cache.js +174 -0
- package/src/core/advanced-router.js +186 -0
- package/src/core/app.js +264 -176
- package/src/core/graceful-shutdown.js +77 -0
- package/src/errors/error-handler.js +157 -0
- package/src/health/health-checker.js +120 -0
- package/src/index.js +69 -0
- package/src/middleware/cache-middleware.js +105 -0
- package/src/middleware/compression.js +97 -0
- package/src/middleware/cors.js +86 -0
- package/src/middleware/rate-limiter.js +159 -0
- package/src/middleware/security.js +107 -0
- package/src/validation/validator.js +301 -0
|
@@ -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
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Validation Middleware
|
|
3
|
+
* v4: Schema-based request validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ValidationError extends Error {
|
|
7
|
+
constructor(message, errors = []) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ValidationError';
|
|
10
|
+
this.statusCode = 400;
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate request against schema
|
|
17
|
+
* @param {Object} schema - Validation schema
|
|
18
|
+
* @returns {Function} - Middleware function
|
|
19
|
+
*/
|
|
20
|
+
function validate(schema) {
|
|
21
|
+
return async (req, res, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const errors = [];
|
|
24
|
+
|
|
25
|
+
// Validate body
|
|
26
|
+
if (schema.body) {
|
|
27
|
+
const bodyErrors = validateObject(req.body || {}, schema.body);
|
|
28
|
+
if (bodyErrors.length > 0) {
|
|
29
|
+
errors.push(...bodyErrors.map(e => ({ field: `body.${e.field}`, ...e })));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate query parameters
|
|
34
|
+
if (schema.query) {
|
|
35
|
+
const queryErrors = validateObject(req.query || {}, schema.query);
|
|
36
|
+
if (queryErrors.length > 0) {
|
|
37
|
+
errors.push(...queryErrors.map(e => ({ field: `query.${e.field}`, ...e })));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate path parameters
|
|
42
|
+
if (schema.params) {
|
|
43
|
+
const paramsErrors = validateObject(req.params || {}, schema.params);
|
|
44
|
+
if (paramsErrors.length > 0) {
|
|
45
|
+
errors.push(...paramsErrors.map(e => ({ field: `params.${e.field}`, ...e })));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate headers
|
|
50
|
+
if (schema.headers) {
|
|
51
|
+
const headersErrors = validateObject(req.headers || {}, schema.headers);
|
|
52
|
+
if (headersErrors.length > 0) {
|
|
53
|
+
errors.push(...headersErrors.map(e => ({ field: `headers.${e.field}`, ...e })));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
throw new ValidationError('Validation failed', errors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
next();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof ValidationError) {
|
|
64
|
+
res.statusCode = error.statusCode;
|
|
65
|
+
res.body = {
|
|
66
|
+
error: error.message,
|
|
67
|
+
errors: error.errors,
|
|
68
|
+
};
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate object against schema
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
function validateObject(obj, schema) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
|
|
83
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
84
|
+
const value = obj[field];
|
|
85
|
+
const fieldErrors = validateField(field, value, rules);
|
|
86
|
+
errors.push(...fieldErrors);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a single field
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
function validateField(field, value, rules) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
|
|
99
|
+
// Required check
|
|
100
|
+
if (rules.required && (value === undefined || value === null || value === '')) {
|
|
101
|
+
errors.push({
|
|
102
|
+
field,
|
|
103
|
+
message: `${field} is required`,
|
|
104
|
+
code: 'REQUIRED',
|
|
105
|
+
});
|
|
106
|
+
return errors; // Don't check other rules if required fails
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip other validations if value is optional and not provided
|
|
110
|
+
if (!rules.required && (value === undefined || value === null)) {
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Type check
|
|
115
|
+
if (rules.type) {
|
|
116
|
+
const typeError = checkType(field, value, rules.type);
|
|
117
|
+
if (typeError) {
|
|
118
|
+
errors.push(typeError);
|
|
119
|
+
return errors; // Don't check other rules if type fails
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// String validations
|
|
124
|
+
if (rules.type === 'string') {
|
|
125
|
+
if (rules.minLength !== undefined && value.length < rules.minLength) {
|
|
126
|
+
errors.push({
|
|
127
|
+
field,
|
|
128
|
+
message: `${field} must be at least ${rules.minLength} characters`,
|
|
129
|
+
code: 'MIN_LENGTH',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
|
|
133
|
+
errors.push({
|
|
134
|
+
field,
|
|
135
|
+
message: `${field} must be at most ${rules.maxLength} characters`,
|
|
136
|
+
code: 'MAX_LENGTH',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
|
|
140
|
+
errors.push({
|
|
141
|
+
field,
|
|
142
|
+
message: `${field} does not match required pattern`,
|
|
143
|
+
code: 'PATTERN',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (rules.format === 'email' && !isValidEmail(value)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
field,
|
|
149
|
+
message: `${field} must be a valid email address`,
|
|
150
|
+
code: 'EMAIL_FORMAT',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (rules.format === 'uuid' && !isValidUUID(value)) {
|
|
154
|
+
errors.push({
|
|
155
|
+
field,
|
|
156
|
+
message: `${field} must be a valid UUID`,
|
|
157
|
+
code: 'UUID_FORMAT',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Number validations
|
|
163
|
+
if (rules.type === 'number') {
|
|
164
|
+
if (rules.min !== undefined && value < rules.min) {
|
|
165
|
+
errors.push({
|
|
166
|
+
field,
|
|
167
|
+
message: `${field} must be at least ${rules.min}`,
|
|
168
|
+
code: 'MIN',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (rules.max !== undefined && value > rules.max) {
|
|
172
|
+
errors.push({
|
|
173
|
+
field,
|
|
174
|
+
message: `${field} must be at most ${rules.max}`,
|
|
175
|
+
code: 'MAX',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Array validations
|
|
181
|
+
if (rules.type === 'array') {
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
errors.push({
|
|
184
|
+
field,
|
|
185
|
+
message: `${field} must be an array`,
|
|
186
|
+
code: 'TYPE',
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
190
|
+
errors.push({
|
|
191
|
+
field,
|
|
192
|
+
message: `${field} must have at least ${rules.minItems} items`,
|
|
193
|
+
code: 'MIN_ITEMS',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (rules.maxItems !== undefined && value.length > rules.maxItems) {
|
|
197
|
+
errors.push({
|
|
198
|
+
field,
|
|
199
|
+
message: `${field} must have at most ${rules.maxItems} items`,
|
|
200
|
+
code: 'MAX_ITEMS',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Custom validator
|
|
207
|
+
if (rules.validator && typeof rules.validator === 'function') {
|
|
208
|
+
try {
|
|
209
|
+
const result = rules.validator(value);
|
|
210
|
+
if (result !== true && typeof result === 'string') {
|
|
211
|
+
errors.push({
|
|
212
|
+
field,
|
|
213
|
+
message: result,
|
|
214
|
+
code: 'CUSTOM',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
errors.push({
|
|
219
|
+
field,
|
|
220
|
+
message: error.message || 'Custom validation failed',
|
|
221
|
+
code: 'CUSTOM',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return errors;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if value matches expected type
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
function checkType(field, value, expectedType) {
|
|
234
|
+
const actualType = typeof value;
|
|
235
|
+
|
|
236
|
+
if (expectedType === 'string' && actualType !== 'string') {
|
|
237
|
+
return {
|
|
238
|
+
field,
|
|
239
|
+
message: `${field} must be a string`,
|
|
240
|
+
code: 'TYPE',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (expectedType === 'number' && actualType !== 'number') {
|
|
245
|
+
return {
|
|
246
|
+
field,
|
|
247
|
+
message: `${field} must be a number`,
|
|
248
|
+
code: 'TYPE',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (expectedType === 'boolean' && actualType !== 'boolean') {
|
|
253
|
+
return {
|
|
254
|
+
field,
|
|
255
|
+
message: `${field} must be a boolean`,
|
|
256
|
+
code: 'TYPE',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
261
|
+
return {
|
|
262
|
+
field,
|
|
263
|
+
message: `${field} must be an array`,
|
|
264
|
+
code: 'TYPE',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value) || value === null)) {
|
|
269
|
+
return {
|
|
270
|
+
field,
|
|
271
|
+
message: `${field} must be an object`,
|
|
272
|
+
code: 'TYPE',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate email format
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
function isValidEmail(email) {
|
|
284
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
285
|
+
return emailRegex.test(email);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate UUID format
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
function isValidUUID(uuid) {
|
|
293
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
294
|
+
return uuidRegex.test(uuid);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
validate,
|
|
299
|
+
ValidationError,
|
|
300
|
+
};
|
|
301
|
+
|