lean-claudient-daemon 0.1.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 +39 -0
- package/dist/api.js +54 -0
- package/dist/bin/daemon.js +14 -0
- package/dist/metrics.js +31 -0
- package/dist/metricsStore.js +84 -0
- package/dist/middleware/rateLimiting.js +193 -0
- package/dist/middleware/securityHeaders.js +239 -0
- package/dist/middleware/validation.js +181 -0
- package/dist/security/envValidation.js +266 -0
- package/dist/service.js +47 -0
- package/dist/validation/schemas.js +157 -0
- package/package.json +49 -0
- package/src/api.ts +58 -0
- package/src/bin/daemon.ts +18 -0
- package/src/metrics.ts +45 -0
- package/src/metricsStore.ts +114 -0
- package/src/middleware/rateLimiting.ts +244 -0
- package/src/middleware/securityHeaders.ts +305 -0
- package/src/middleware/validation.ts +222 -0
- package/src/security/envValidation.ts +321 -0
- package/src/service.ts +62 -0
- package/src/validation/schemas.ts +190 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers Middleware
|
|
3
|
+
* Adds security-related HTTP headers to all responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Request, Response, NextFunction } from 'express';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Security headers configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface SecurityHeadersConfig {
|
|
12
|
+
// Strict Transport Security (HSTS)
|
|
13
|
+
hstsMaxAge?: number; // seconds, default 31536000 (1 year)
|
|
14
|
+
hstsIncludeSubdomains?: boolean;
|
|
15
|
+
hstsPreload?: boolean;
|
|
16
|
+
|
|
17
|
+
// Content Security Policy (CSP)
|
|
18
|
+
cspDirectives?: Record<string, string>;
|
|
19
|
+
|
|
20
|
+
// X-Content-Type-Options
|
|
21
|
+
noSniff?: boolean;
|
|
22
|
+
|
|
23
|
+
// X-Frame-Options
|
|
24
|
+
frameOptions?: 'DENY' | 'SAMEORIGIN' | 'ALLOW-FROM';
|
|
25
|
+
|
|
26
|
+
// X-XSS-Protection
|
|
27
|
+
xssProtection?: boolean;
|
|
28
|
+
|
|
29
|
+
// Referrer-Policy
|
|
30
|
+
referrerPolicy?: string;
|
|
31
|
+
|
|
32
|
+
// Permissions-Policy (Feature-Policy)
|
|
33
|
+
permissionsPolicy?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default security headers configuration
|
|
38
|
+
*/
|
|
39
|
+
const DEFAULT_CONFIG: SecurityHeadersConfig = {
|
|
40
|
+
hstsMaxAge: 31536000, // 1 year
|
|
41
|
+
hstsIncludeSubdomains: true,
|
|
42
|
+
hstsPreload: true,
|
|
43
|
+
noSniff: true,
|
|
44
|
+
frameOptions: 'DENY',
|
|
45
|
+
xssProtection: true,
|
|
46
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
47
|
+
cspDirectives: {
|
|
48
|
+
'default-src': "'none'",
|
|
49
|
+
'script-src': "'self'",
|
|
50
|
+
'style-src': "'self'",
|
|
51
|
+
'img-src': "'self'",
|
|
52
|
+
'font-src': "'self'",
|
|
53
|
+
'connect-src': "'self'",
|
|
54
|
+
'frame-ancestors': "'none'",
|
|
55
|
+
'base-uri': "'self'",
|
|
56
|
+
'form-action': "'self'",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create security headers middleware
|
|
62
|
+
*/
|
|
63
|
+
export function securityHeaders(config: SecurityHeadersConfig = {}) {
|
|
64
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
65
|
+
|
|
66
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
67
|
+
// Strict-Transport-Security (HSTS)
|
|
68
|
+
if (finalConfig.hstsMaxAge !== undefined) {
|
|
69
|
+
let hstsValue = `max-age=${finalConfig.hstsMaxAge}`;
|
|
70
|
+
if (finalConfig.hstsIncludeSubdomains) {
|
|
71
|
+
hstsValue += '; includeSubDomains';
|
|
72
|
+
}
|
|
73
|
+
if (finalConfig.hstsPreload) {
|
|
74
|
+
hstsValue += '; preload';
|
|
75
|
+
}
|
|
76
|
+
res.setHeader('Strict-Transport-Security', hstsValue);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// X-Content-Type-Options
|
|
80
|
+
if (finalConfig.noSniff !== false) {
|
|
81
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// X-Frame-Options
|
|
85
|
+
if (finalConfig.frameOptions) {
|
|
86
|
+
res.setHeader('X-Frame-Options', finalConfig.frameOptions);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// X-XSS-Protection
|
|
90
|
+
if (finalConfig.xssProtection !== false) {
|
|
91
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Referrer-Policy
|
|
95
|
+
if (finalConfig.referrerPolicy) {
|
|
96
|
+
res.setHeader('Referrer-Policy', finalConfig.referrerPolicy);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Content-Security-Policy (CSP)
|
|
100
|
+
if (finalConfig.cspDirectives) {
|
|
101
|
+
const cspValue = Object.entries(finalConfig.cspDirectives)
|
|
102
|
+
.map(([key, value]) => `${key} ${value}`)
|
|
103
|
+
.join('; ');
|
|
104
|
+
res.setHeader('Content-Security-Policy', cspValue);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Permissions-Policy
|
|
108
|
+
if (finalConfig.permissionsPolicy) {
|
|
109
|
+
const ppValue = Object.entries(finalConfig.permissionsPolicy)
|
|
110
|
+
.map(([key, value]) => `${key}=(${value})`)
|
|
111
|
+
.join(', ');
|
|
112
|
+
res.setHeader('Permissions-Policy', ppValue);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Additional security headers
|
|
116
|
+
res.setHeader('X-Powered-By', 'Lean Claudient');
|
|
117
|
+
res.setHeader('Server', 'Lean Claudient Daemon');
|
|
118
|
+
|
|
119
|
+
next();
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Middleware for API endpoints (stricter CSP)
|
|
125
|
+
*/
|
|
126
|
+
export function securityHeadersAPI(config: SecurityHeadersConfig = {}) {
|
|
127
|
+
const apiConfig: SecurityHeadersConfig = {
|
|
128
|
+
...DEFAULT_CONFIG,
|
|
129
|
+
cspDirectives: {
|
|
130
|
+
'default-src': "'none'",
|
|
131
|
+
'script-src': "'none'",
|
|
132
|
+
'style-src': "'none'",
|
|
133
|
+
'img-src': "'none'",
|
|
134
|
+
'font-src': "'none'",
|
|
135
|
+
'connect-src': "'none'",
|
|
136
|
+
'frame-ancestors': "'none'",
|
|
137
|
+
'base-uri': "'self'",
|
|
138
|
+
'form-action': "'none'",
|
|
139
|
+
},
|
|
140
|
+
...config,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return securityHeaders(apiConfig);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Remove potentially dangerous headers
|
|
148
|
+
*/
|
|
149
|
+
export function removeDangerousHeaders(req: Request, res: Response, next: NextFunction) {
|
|
150
|
+
// Remove headers that might leak information
|
|
151
|
+
const dangerous = ['X-Powered-By', 'Server', 'X-AspNet-Version', 'X-Runtime-Version'];
|
|
152
|
+
|
|
153
|
+
for (const header of dangerous) {
|
|
154
|
+
res.removeHeader(header);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Disable caching for sensitive data
|
|
158
|
+
if (req.path.includes('/api/')) {
|
|
159
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
160
|
+
res.setHeader('Pragma', 'no-cache');
|
|
161
|
+
res.setHeader('Expires', '0');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
next();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validate request headers
|
|
169
|
+
*/
|
|
170
|
+
export function validateRequestHeaders(req: Request, res: Response, next: NextFunction) {
|
|
171
|
+
const dangerousPatterns = [
|
|
172
|
+
/javascript:/i,
|
|
173
|
+
/on\w+=/i, // onclick, onload, etc
|
|
174
|
+
/eval\(/i,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// Check headers for suspicious content
|
|
178
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
179
|
+
if (typeof value !== 'string') continue;
|
|
180
|
+
|
|
181
|
+
for (const pattern of dangerousPatterns) {
|
|
182
|
+
if (pattern.test(value)) {
|
|
183
|
+
return res.status(400).json({
|
|
184
|
+
error: 'Invalid request header',
|
|
185
|
+
code: 'INVALID_HEADER',
|
|
186
|
+
details: {
|
|
187
|
+
header: key,
|
|
188
|
+
message: 'Header contains suspicious content',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
next();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Enforce HTTPS redirect for non-local environments
|
|
200
|
+
*/
|
|
201
|
+
export function enforceHTTPS(req: Request, res: Response, next: NextFunction) {
|
|
202
|
+
// Skip for localhost and internal IPs
|
|
203
|
+
if (
|
|
204
|
+
req.hostname === 'localhost' ||
|
|
205
|
+
req.hostname === '127.0.0.1' ||
|
|
206
|
+
req.ip === '::1' ||
|
|
207
|
+
req.ip?.startsWith('127.') ||
|
|
208
|
+
req.ip?.startsWith('192.168.') ||
|
|
209
|
+
req.ip?.startsWith('10.')
|
|
210
|
+
) {
|
|
211
|
+
return next();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if connection is secure
|
|
215
|
+
const isSecure = req.secure || req.get('X-Forwarded-Proto') === 'https';
|
|
216
|
+
|
|
217
|
+
if (!isSecure) {
|
|
218
|
+
return res.redirect(301, `https://${req.get('Host')}${req.url}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
next();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Add Content Security Policy meta tag (for responses with HTML)
|
|
226
|
+
*/
|
|
227
|
+
export function cspMetaTag(config: SecurityHeadersConfig = {}) {
|
|
228
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
229
|
+
|
|
230
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
231
|
+
// Store CSP config on response for template use
|
|
232
|
+
if (finalConfig.cspDirectives) {
|
|
233
|
+
const cspValue = Object.entries(finalConfig.cspDirectives)
|
|
234
|
+
.map(([key, value]) => `${key} ${value}`)
|
|
235
|
+
.join('; ');
|
|
236
|
+
(res as any).locals = { cspMetaTag: cspValue };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
next();
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Prevent information disclosure
|
|
245
|
+
*/
|
|
246
|
+
export function preventInformationDisclosure(req: Request, res: Response, next: NextFunction) {
|
|
247
|
+
const originalJson = res.json;
|
|
248
|
+
|
|
249
|
+
res.json = function (body: any) {
|
|
250
|
+
// Remove stack traces from error responses in production
|
|
251
|
+
if (process.env.NODE_ENV === 'production') {
|
|
252
|
+
if (body && typeof body === 'object') {
|
|
253
|
+
delete body.stack;
|
|
254
|
+
delete body.stackTrace;
|
|
255
|
+
delete body.internal;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return originalJson.call(this, body);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
next();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create all security headers middleware stack
|
|
267
|
+
*/
|
|
268
|
+
export function createSecurityStack(config: SecurityHeadersConfig = {}) {
|
|
269
|
+
return [
|
|
270
|
+
validateRequestHeaders,
|
|
271
|
+
removeDangerousHeaders,
|
|
272
|
+
preventInformationDisclosure,
|
|
273
|
+
securityHeaders(config),
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Strict security headers for admin endpoints
|
|
279
|
+
*/
|
|
280
|
+
export const strictSecurityHeaders = securityHeaders({
|
|
281
|
+
hstsMaxAge: 31536000,
|
|
282
|
+
frameOptions: 'DENY',
|
|
283
|
+
cspDirectives: {
|
|
284
|
+
'default-src': "'none'",
|
|
285
|
+
'script-src': "'self'",
|
|
286
|
+
'style-src': "'self'",
|
|
287
|
+
'img-src': "'self'",
|
|
288
|
+
'font-src': "'self'",
|
|
289
|
+
'connect-src': "'self'",
|
|
290
|
+
'frame-ancestors': "'none'",
|
|
291
|
+
'base-uri': "'self'",
|
|
292
|
+
'form-action': "'self'",
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Relaxed security headers for public APIs (JSON only)
|
|
298
|
+
*/
|
|
299
|
+
export const apiSecurityHeaders = securityHeaders({
|
|
300
|
+
cspDirectives: {
|
|
301
|
+
'default-src': "'none'",
|
|
302
|
+
'frame-ancestors': "'none'",
|
|
303
|
+
'base-uri': "'self'",
|
|
304
|
+
},
|
|
305
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Middleware
|
|
3
|
+
* Express middleware for validating request bodies and parameters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Request, Response, NextFunction } from 'express';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validation error response
|
|
11
|
+
*/
|
|
12
|
+
interface ValidationError {
|
|
13
|
+
field: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create validation middleware for a given schema
|
|
19
|
+
*/
|
|
20
|
+
export function createValidationMiddleware<T>(schema: z.ZodSchema<T>) {
|
|
21
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
22
|
+
try {
|
|
23
|
+
const validated = schema.parse(req.body);
|
|
24
|
+
(req as any).validatedBody = validated;
|
|
25
|
+
next();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof z.ZodError) {
|
|
28
|
+
const errors: ValidationError[] = error.errors.map((e) => ({
|
|
29
|
+
field: e.path.join('.'),
|
|
30
|
+
message: e.message,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
return res.status(400).json({
|
|
34
|
+
error: 'Validation failed',
|
|
35
|
+
code: 'VALIDATION_ERROR',
|
|
36
|
+
details: errors,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return res.status(400).json({
|
|
41
|
+
error: 'Invalid request',
|
|
42
|
+
code: 'INVALID_REQUEST',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate request body
|
|
50
|
+
*/
|
|
51
|
+
export function validateRequestBody<T>(
|
|
52
|
+
data: unknown,
|
|
53
|
+
schema: z.ZodSchema<T>
|
|
54
|
+
): { valid: true; data: T } | { valid: false; errors: ValidationError[] } {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = schema.parse(data);
|
|
57
|
+
return { valid: true, data: parsed };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof z.ZodError) {
|
|
60
|
+
const errors: ValidationError[] = error.errors.map((e) => ({
|
|
61
|
+
field: e.path.join('.'),
|
|
62
|
+
message: e.message,
|
|
63
|
+
}));
|
|
64
|
+
return { valid: false, errors };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
errors: [{ field: 'unknown', message: 'Validation failed' }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate UUID in path parameter
|
|
75
|
+
*/
|
|
76
|
+
export function validatePathUUID(
|
|
77
|
+
req: Request,
|
|
78
|
+
res: Response,
|
|
79
|
+
next: NextFunction,
|
|
80
|
+
id: string,
|
|
81
|
+
paramName: string = 'id'
|
|
82
|
+
) {
|
|
83
|
+
const uuidPattern =
|
|
84
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
85
|
+
|
|
86
|
+
if (!uuidPattern.test(id)) {
|
|
87
|
+
return res.status(400).json({
|
|
88
|
+
error: 'Invalid UUID format',
|
|
89
|
+
code: 'INVALID_UUID',
|
|
90
|
+
details: {
|
|
91
|
+
field: paramName,
|
|
92
|
+
message: 'Must be a valid UUID (36 characters with hyphens)',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
next();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Middleware to validate UUID parameter 'id'
|
|
102
|
+
*/
|
|
103
|
+
export function validateIdParam(req: Request, res: Response, next: NextFunction) {
|
|
104
|
+
const { id } = req.params;
|
|
105
|
+
if (!id) {
|
|
106
|
+
return res.status(400).json({
|
|
107
|
+
error: 'Missing id parameter',
|
|
108
|
+
code: 'MISSING_PARAM',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
validatePathUUID(req, res, next, id, 'id');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Middleware to validate UUID parameter 'sessionId'
|
|
116
|
+
*/
|
|
117
|
+
export function validateSessionIdParam(req: Request, res: Response, next: NextFunction) {
|
|
118
|
+
const { sessionId } = req.params;
|
|
119
|
+
if (!sessionId) {
|
|
120
|
+
return res.status(400).json({
|
|
121
|
+
error: 'Missing sessionId parameter',
|
|
122
|
+
code: 'MISSING_PARAM',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
validatePathUUID(req, res, next, sessionId, 'sessionId');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Middleware to ensure Content-Type is application/json
|
|
130
|
+
*/
|
|
131
|
+
export function validateContentType(req: Request, res: Response, next: NextFunction) {
|
|
132
|
+
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
|
133
|
+
const contentType = req.get('Content-Type');
|
|
134
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
135
|
+
return res.status(415).json({
|
|
136
|
+
error: 'Unsupported Media Type',
|
|
137
|
+
code: 'UNSUPPORTED_MEDIA_TYPE',
|
|
138
|
+
details: {
|
|
139
|
+
message: 'Content-Type must be application/json',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
next();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Middleware to limit request body size
|
|
149
|
+
*/
|
|
150
|
+
export function validateRequestSize(maxSize = '1mb') {
|
|
151
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
152
|
+
if (!req.get('Content-Length')) {
|
|
153
|
+
return next();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sizeBytes = parseSize(maxSize);
|
|
157
|
+
const contentLength = parseInt(req.get('Content-Length') || '0', 10);
|
|
158
|
+
|
|
159
|
+
if (contentLength > sizeBytes) {
|
|
160
|
+
return res.status(413).json({
|
|
161
|
+
error: 'Payload Too Large',
|
|
162
|
+
code: 'PAYLOAD_TOO_LARGE',
|
|
163
|
+
details: {
|
|
164
|
+
maxSize,
|
|
165
|
+
received: `${(contentLength / 1024).toFixed(2)}kb`,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
next();
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse size string to bytes
|
|
176
|
+
*/
|
|
177
|
+
function parseSize(size: string): number {
|
|
178
|
+
const units: Record<string, number> = {
|
|
179
|
+
b: 1,
|
|
180
|
+
kb: 1024,
|
|
181
|
+
mb: 1024 * 1024,
|
|
182
|
+
gb: 1024 * 1024 * 1024,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(kb|mb|gb|b)?$/);
|
|
186
|
+
if (!match) return 1024 * 1024; // default 1mb
|
|
187
|
+
|
|
188
|
+
const [, number, unit = 'b'] = match;
|
|
189
|
+
return Math.floor(parseFloat(number) * (units[unit] || 1));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Validate query parameters
|
|
194
|
+
*/
|
|
195
|
+
export function validateQueryParams(
|
|
196
|
+
data: unknown,
|
|
197
|
+
allowedKeys: string[]
|
|
198
|
+
): { valid: true; data: Record<string, any> } | { valid: false; errors: ValidationError[] } {
|
|
199
|
+
if (typeof data !== 'object' || data === null) {
|
|
200
|
+
return { valid: false, errors: [{ field: 'query', message: 'Invalid query parameters' }] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const errors: ValidationError[] = [];
|
|
204
|
+
const validated: Record<string, any> = {};
|
|
205
|
+
|
|
206
|
+
for (const [key, value] of Object.entries(data)) {
|
|
207
|
+
if (!allowedKeys.includes(key)) {
|
|
208
|
+
errors.push({
|
|
209
|
+
field: key,
|
|
210
|
+
message: `Unknown parameter: ${key}`,
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
validated[key] = value;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (errors.length > 0) {
|
|
218
|
+
return { valid: false, errors };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { valid: true, data: validated };
|
|
222
|
+
}
|