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,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
+