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.
@@ -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
+ }