tlc-claude-code 1.4.8 → 1.4.9

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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Request Validator
3
+ * Validates request size, content type, JSON depth, and prevents path traversal
4
+ */
5
+
6
+ export const REQUEST_LIMITS = {
7
+ MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB
8
+ MAX_JSON_DEPTH: 10,
9
+ MAX_QUERY_LENGTH: 2048,
10
+ MAX_HEADER_SIZE: 8192,
11
+ MAX_PARAMS: 100,
12
+ };
13
+
14
+ /**
15
+ * Validate request size against limit
16
+ */
17
+ export function validateRequestSize(options) {
18
+ const { contentLength, maxSize = REQUEST_LIMITS.MAX_BODY_SIZE } = options;
19
+
20
+ if (contentLength === undefined || contentLength === null) {
21
+ return {
22
+ valid: true,
23
+ warning: 'Content-Length header not provided',
24
+ };
25
+ }
26
+
27
+ if (contentLength > maxSize) {
28
+ return {
29
+ valid: false,
30
+ error: `Request size ${contentLength} exceeds maximum allowed size of ${maxSize}`,
31
+ };
32
+ }
33
+
34
+ return { valid: true };
35
+ }
36
+
37
+ /**
38
+ * Validate content type against allowed types
39
+ */
40
+ export function validateContentType(options) {
41
+ const { contentType, allowedTypes } = options;
42
+
43
+ if (!contentType) {
44
+ return {
45
+ valid: false,
46
+ error: 'Content-Type header is required',
47
+ };
48
+ }
49
+
50
+ // Extract base content type (without charset, etc.)
51
+ const baseType = contentType.split(';')[0].trim().toLowerCase();
52
+
53
+ const isAllowed = allowedTypes.some((allowed) => {
54
+ const allowedLower = allowed.toLowerCase();
55
+
56
+ // Handle wildcard types (e.g., image/*)
57
+ if (allowedLower.endsWith('/*')) {
58
+ const prefix = allowedLower.slice(0, -1); // Remove *
59
+ return baseType.startsWith(prefix);
60
+ }
61
+
62
+ return baseType === allowedLower;
63
+ });
64
+
65
+ if (!isAllowed) {
66
+ return {
67
+ valid: false,
68
+ error: `Content-Type '${baseType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,
69
+ };
70
+ }
71
+
72
+ return { valid: true };
73
+ }
74
+
75
+ /**
76
+ * Calculate depth of a JSON object/array
77
+ */
78
+ function calculateDepth(obj, currentDepth = 0) {
79
+ if (obj === null || typeof obj !== 'object') {
80
+ return currentDepth;
81
+ }
82
+
83
+ currentDepth++;
84
+
85
+ if (Array.isArray(obj)) {
86
+ if (obj.length === 0) return currentDepth;
87
+ return Math.max(...obj.map((item) => calculateDepth(item, currentDepth)));
88
+ }
89
+
90
+ const values = Object.values(obj);
91
+ if (values.length === 0) return currentDepth;
92
+ return Math.max(...values.map((value) => calculateDepth(value, currentDepth)));
93
+ }
94
+
95
+ /**
96
+ * Validate JSON depth against limit
97
+ */
98
+ export function validateJsonDepth(options) {
99
+ const { json, maxDepth = REQUEST_LIMITS.MAX_JSON_DEPTH } = options;
100
+
101
+ const depth = calculateDepth(json);
102
+
103
+ if (depth > maxDepth) {
104
+ return {
105
+ valid: false,
106
+ error: `JSON depth ${depth} exceeds maximum allowed depth of ${maxDepth}`,
107
+ depth,
108
+ };
109
+ }
110
+
111
+ return { valid: true, depth };
112
+ }
113
+
114
+ /**
115
+ * Validate query string length and parameters
116
+ */
117
+ export function validateQueryString(options) {
118
+ const {
119
+ queryString,
120
+ maxLength = REQUEST_LIMITS.MAX_QUERY_LENGTH,
121
+ maxParams = REQUEST_LIMITS.MAX_PARAMS,
122
+ allowDuplicates = true,
123
+ } = options;
124
+
125
+ if (!queryString) {
126
+ return { valid: true };
127
+ }
128
+
129
+ // Check length
130
+ if (queryString.length > maxLength) {
131
+ return {
132
+ valid: false,
133
+ error: `query string length ${queryString.length} exceeds maximum of ${maxLength}`,
134
+ };
135
+ }
136
+
137
+ // Parse parameters
138
+ const params = queryString.split('&').filter((p) => p.length > 0);
139
+
140
+ // Check parameter count
141
+ if (params.length > maxParams) {
142
+ return {
143
+ valid: false,
144
+ error: `Query string has ${params.length} parameters, exceeds maximum of ${maxParams}`,
145
+ };
146
+ }
147
+
148
+ // Check for duplicates
149
+ if (!allowDuplicates) {
150
+ const names = params.map((p) => p.split('=')[0]);
151
+ const uniqueNames = new Set(names);
152
+ if (uniqueNames.size !== names.length) {
153
+ return {
154
+ valid: false,
155
+ error: 'Query string contains duplicate parameter names',
156
+ };
157
+ }
158
+ }
159
+
160
+ return { valid: true };
161
+ }
162
+
163
+ /**
164
+ * Validate headers size and format
165
+ */
166
+ export function validateHeaders(options) {
167
+ const {
168
+ headers,
169
+ maxSize = REQUEST_LIMITS.MAX_HEADER_SIZE,
170
+ maxHeaderSize,
171
+ validateNames = false,
172
+ } = options;
173
+
174
+ // Calculate total headers size
175
+ let totalSize = 0;
176
+ for (const [name, value] of Object.entries(headers)) {
177
+ const headerSize = name.length + String(value).length;
178
+ totalSize += headerSize;
179
+
180
+ // Check individual header size
181
+ if (maxHeaderSize && headerSize > maxHeaderSize) {
182
+ return {
183
+ valid: false,
184
+ error: `Header '${name}' size ${headerSize} exceeds maximum of ${maxHeaderSize}`,
185
+ };
186
+ }
187
+
188
+ // Validate header name format (RFC 7230)
189
+ if (validateNames) {
190
+ const validHeaderName = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
191
+ if (!validHeaderName.test(name)) {
192
+ return {
193
+ valid: false,
194
+ error: `Header name '${name}' contains invalid characters`,
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ if (totalSize > maxSize) {
201
+ return {
202
+ valid: false,
203
+ error: `Total header size ${totalSize} exceeds maximum of ${maxSize}`,
204
+ };
205
+ }
206
+
207
+ return { valid: true };
208
+ }
209
+
210
+ /**
211
+ * Validate path for security issues
212
+ */
213
+ export function validatePath(options) {
214
+ const { path, allowedPaths } = options;
215
+
216
+ // Decode the path to catch encoded attacks
217
+ let decodedPath;
218
+ try {
219
+ // Double decode to catch double-encoding attacks
220
+ decodedPath = decodeURIComponent(decodeURIComponent(path));
221
+ } catch {
222
+ // If decoding fails, use single decode or original
223
+ try {
224
+ decodedPath = decodeURIComponent(path);
225
+ } catch {
226
+ decodedPath = path;
227
+ }
228
+ }
229
+
230
+ // Check for null bytes
231
+ if (path.includes('%00') || path.includes('\0') || decodedPath.includes('\0')) {
232
+ return {
233
+ valid: false,
234
+ error: 'Path contains null byte injection attempt',
235
+ };
236
+ }
237
+
238
+ // Check for path traversal
239
+ if (
240
+ decodedPath.includes('..') ||
241
+ decodedPath.includes('..\\') ||
242
+ path.includes('%2e%2e') ||
243
+ path.includes('%252e')
244
+ ) {
245
+ return {
246
+ valid: false,
247
+ error: 'Path contains traversal attempt',
248
+ };
249
+ }
250
+
251
+ // Check against allowed paths
252
+ if (allowedPaths && allowedPaths.length > 0) {
253
+ const isAllowed = allowedPaths.some((allowed) => {
254
+ if (allowed.endsWith('/*')) {
255
+ const prefix = allowed.slice(0, -1);
256
+ return path.startsWith(prefix);
257
+ }
258
+ return path === allowed || path.startsWith(allowed + '/');
259
+ });
260
+
261
+ if (!isAllowed) {
262
+ return {
263
+ valid: false,
264
+ error: `Path '${path}' is not in allowed paths`,
265
+ };
266
+ }
267
+ }
268
+
269
+ return { valid: true };
270
+ }
271
+
272
+ /**
273
+ * Create a request validator instance
274
+ */
275
+ export function createRequestValidator(config = {}) {
276
+ const {
277
+ maxBodySize = REQUEST_LIMITS.MAX_BODY_SIZE,
278
+ maxJsonDepth = REQUEST_LIMITS.MAX_JSON_DEPTH,
279
+ allowedContentTypes = ['application/json'],
280
+ allowedPaths,
281
+ } = config;
282
+
283
+ return {
284
+ validate(request) {
285
+ const errors = [];
286
+
287
+ // Validate size
288
+ const sizeResult = validateRequestSize({
289
+ contentLength: request.contentLength,
290
+ maxSize: maxBodySize,
291
+ });
292
+ if (!sizeResult.valid) {
293
+ errors.push(sizeResult.error);
294
+ }
295
+
296
+ // Validate content type
297
+ if (request.contentType) {
298
+ const contentTypeResult = validateContentType({
299
+ contentType: request.contentType,
300
+ allowedTypes: allowedContentTypes,
301
+ });
302
+ if (!contentTypeResult.valid) {
303
+ errors.push(contentTypeResult.error);
304
+ }
305
+ }
306
+
307
+ // Validate path
308
+ if (request.path) {
309
+ const pathResult = validatePath({
310
+ path: request.path,
311
+ allowedPaths,
312
+ });
313
+ if (!pathResult.valid) {
314
+ errors.push(pathResult.error);
315
+ }
316
+ }
317
+
318
+ // Validate JSON depth
319
+ if (request.body && typeof request.body === 'object') {
320
+ const jsonResult = validateJsonDepth({
321
+ json: request.body,
322
+ maxDepth: maxJsonDepth,
323
+ });
324
+ if (!jsonResult.valid) {
325
+ errors.push(jsonResult.error);
326
+ }
327
+ }
328
+
329
+ return {
330
+ valid: errors.length === 0,
331
+ errors,
332
+ };
333
+ },
334
+
335
+ validateSize(options) {
336
+ return validateRequestSize({ ...options, maxSize: maxBodySize });
337
+ },
338
+
339
+ validateContentType(options) {
340
+ return validateContentType({ ...options, allowedTypes: allowedContentTypes });
341
+ },
342
+
343
+ validateJson(json) {
344
+ return validateJsonDepth({ json, maxDepth: maxJsonDepth });
345
+ },
346
+
347
+ validatePath(path) {
348
+ return validatePath({ path, allowedPaths });
349
+ },
350
+ };
351
+ }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Request Validator Tests
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ validateRequestSize,
7
+ validateContentType,
8
+ validateJsonDepth,
9
+ validateQueryString,
10
+ validateHeaders,
11
+ validatePath,
12
+ REQUEST_LIMITS,
13
+ createRequestValidator,
14
+ } from './request-validator.js';
15
+
16
+ describe('request-validator', () => {
17
+ describe('REQUEST_LIMITS', () => {
18
+ it('defines default limits', () => {
19
+ expect(REQUEST_LIMITS.MAX_BODY_SIZE).toBeDefined();
20
+ expect(REQUEST_LIMITS.MAX_JSON_DEPTH).toBeDefined();
21
+ expect(REQUEST_LIMITS.MAX_QUERY_LENGTH).toBeDefined();
22
+ expect(REQUEST_LIMITS.MAX_HEADER_SIZE).toBeDefined();
23
+ });
24
+ });
25
+
26
+ describe('validateRequestSize', () => {
27
+ it('accepts requests under limit', () => {
28
+ const result = validateRequestSize({
29
+ contentLength: 1000,
30
+ maxSize: 10000,
31
+ });
32
+
33
+ expect(result.valid).toBe(true);
34
+ });
35
+
36
+ it('rejects oversized requests', () => {
37
+ const result = validateRequestSize({
38
+ contentLength: 20000,
39
+ maxSize: 10000,
40
+ });
41
+
42
+ expect(result.valid).toBe(false);
43
+ expect(result.error).toContain('size');
44
+ });
45
+
46
+ it('uses default limit when not specified', () => {
47
+ const result = validateRequestSize({
48
+ contentLength: 1000,
49
+ });
50
+
51
+ expect(result.valid).toBe(true);
52
+ });
53
+
54
+ it('handles missing content-length', () => {
55
+ const result = validateRequestSize({
56
+ maxSize: 10000,
57
+ });
58
+
59
+ expect(result.valid).toBe(true);
60
+ expect(result.warning).toContain('Content-Length');
61
+ });
62
+ });
63
+
64
+ describe('validateContentType', () => {
65
+ it('accepts valid JSON content type', () => {
66
+ const result = validateContentType({
67
+ contentType: 'application/json',
68
+ allowedTypes: ['application/json'],
69
+ });
70
+
71
+ expect(result.valid).toBe(true);
72
+ });
73
+
74
+ it('accepts content type with charset', () => {
75
+ const result = validateContentType({
76
+ contentType: 'application/json; charset=utf-8',
77
+ allowedTypes: ['application/json'],
78
+ });
79
+
80
+ expect(result.valid).toBe(true);
81
+ });
82
+
83
+ it('rejects invalid content type', () => {
84
+ const result = validateContentType({
85
+ contentType: 'text/plain',
86
+ allowedTypes: ['application/json'],
87
+ });
88
+
89
+ expect(result.valid).toBe(false);
90
+ expect(result.error).toContain('Content-Type');
91
+ });
92
+
93
+ it('supports multiple allowed types', () => {
94
+ const result = validateContentType({
95
+ contentType: 'application/xml',
96
+ allowedTypes: ['application/json', 'application/xml'],
97
+ });
98
+
99
+ expect(result.valid).toBe(true);
100
+ });
101
+
102
+ it('handles wildcard types', () => {
103
+ const result = validateContentType({
104
+ contentType: 'image/png',
105
+ allowedTypes: ['image/*'],
106
+ });
107
+
108
+ expect(result.valid).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe('validateJsonDepth', () => {
113
+ it('accepts JSON within depth limit', () => {
114
+ const json = { a: { b: { c: 'value' } } };
115
+ const result = validateJsonDepth({
116
+ json,
117
+ maxDepth: 5,
118
+ });
119
+
120
+ expect(result.valid).toBe(true);
121
+ });
122
+
123
+ it('rejects deeply nested JSON', () => {
124
+ const json = { a: { b: { c: { d: { e: { f: 'value' } } } } } };
125
+ const result = validateJsonDepth({
126
+ json,
127
+ maxDepth: 3,
128
+ });
129
+
130
+ expect(result.valid).toBe(false);
131
+ expect(result.error).toContain('depth');
132
+ });
133
+
134
+ it('handles arrays in depth calculation', () => {
135
+ const json = { a: [{ b: [{ c: 'value' }] }] };
136
+ const result = validateJsonDepth({
137
+ json,
138
+ maxDepth: 5,
139
+ });
140
+
141
+ expect(result.valid).toBe(true);
142
+ expect(result.depth).toBe(5);
143
+ });
144
+
145
+ it('returns actual depth', () => {
146
+ const json = { a: { b: 'value' } };
147
+ const result = validateJsonDepth({
148
+ json,
149
+ maxDepth: 10,
150
+ });
151
+
152
+ expect(result.depth).toBe(2);
153
+ });
154
+ });
155
+
156
+ describe('validateQueryString', () => {
157
+ it('accepts query string under length limit', () => {
158
+ const result = validateQueryString({
159
+ queryString: 'foo=bar&baz=qux',
160
+ maxLength: 1000,
161
+ });
162
+
163
+ expect(result.valid).toBe(true);
164
+ });
165
+
166
+ it('rejects query string over length limit', () => {
167
+ const result = validateQueryString({
168
+ queryString: 'a'.repeat(2000),
169
+ maxLength: 1000,
170
+ });
171
+
172
+ expect(result.valid).toBe(false);
173
+ expect(result.error).toContain('query');
174
+ });
175
+
176
+ it('limits parameter count', () => {
177
+ const params = Array.from({ length: 200 }, (_, i) => `p${i}=v${i}`).join('&');
178
+ const result = validateQueryString({
179
+ queryString: params,
180
+ maxParams: 100,
181
+ });
182
+
183
+ expect(result.valid).toBe(false);
184
+ expect(result.error).toContain('parameters');
185
+ });
186
+
187
+ it('detects duplicate parameters', () => {
188
+ const result = validateQueryString({
189
+ queryString: 'foo=bar&foo=baz',
190
+ allowDuplicates: false,
191
+ });
192
+
193
+ expect(result.valid).toBe(false);
194
+ expect(result.error).toContain('duplicate');
195
+ });
196
+ });
197
+
198
+ describe('validateHeaders', () => {
199
+ it('accepts headers under size limit', () => {
200
+ const result = validateHeaders({
201
+ headers: {
202
+ 'Content-Type': 'application/json',
203
+ 'Authorization': 'Bearer token123',
204
+ },
205
+ maxSize: 8000,
206
+ });
207
+
208
+ expect(result.valid).toBe(true);
209
+ });
210
+
211
+ it('rejects headers over size limit', () => {
212
+ const result = validateHeaders({
213
+ headers: {
214
+ 'X-Large-Header': 'a'.repeat(10000),
215
+ },
216
+ maxSize: 8000,
217
+ });
218
+
219
+ expect(result.valid).toBe(false);
220
+ expect(result.error).toContain('header');
221
+ });
222
+
223
+ it('limits individual header size', () => {
224
+ const result = validateHeaders({
225
+ headers: {
226
+ 'X-Large-Header': 'a'.repeat(5000),
227
+ },
228
+ maxHeaderSize: 4000,
229
+ });
230
+
231
+ expect(result.valid).toBe(false);
232
+ });
233
+
234
+ it('validates header name format', () => {
235
+ const result = validateHeaders({
236
+ headers: {
237
+ 'Invalid Header Name': 'value',
238
+ },
239
+ validateNames: true,
240
+ });
241
+
242
+ expect(result.valid).toBe(false);
243
+ expect(result.error).toContain('invalid');
244
+ });
245
+ });
246
+
247
+ describe('validatePath', () => {
248
+ it('accepts valid paths', () => {
249
+ const result = validatePath({
250
+ path: '/api/users/123',
251
+ });
252
+
253
+ expect(result.valid).toBe(true);
254
+ });
255
+
256
+ it('blocks path traversal attempts', () => {
257
+ const result = validatePath({
258
+ path: '/api/files/../../../etc/passwd',
259
+ });
260
+
261
+ expect(result.valid).toBe(false);
262
+ expect(result.error).toContain('traversal');
263
+ });
264
+
265
+ it('blocks encoded path traversal', () => {
266
+ const result = validatePath({
267
+ path: '/api/files/%2e%2e%2f%2e%2e%2fetc/passwd',
268
+ });
269
+
270
+ expect(result.valid).toBe(false);
271
+ });
272
+
273
+ it('blocks double-encoded traversal', () => {
274
+ const result = validatePath({
275
+ path: '/api/files/%252e%252e%252f',
276
+ });
277
+
278
+ expect(result.valid).toBe(false);
279
+ });
280
+
281
+ it('blocks null bytes', () => {
282
+ const result = validatePath({
283
+ path: '/api/files/test%00.txt',
284
+ });
285
+
286
+ expect(result.valid).toBe(false);
287
+ expect(result.error).toContain('null');
288
+ });
289
+
290
+ it('validates against allowed paths', () => {
291
+ const result = validatePath({
292
+ path: '/admin/secret',
293
+ allowedPaths: ['/api/*', '/public/*'],
294
+ });
295
+
296
+ expect(result.valid).toBe(false);
297
+ });
298
+ });
299
+
300
+ describe('createRequestValidator', () => {
301
+ it('creates validator with methods', () => {
302
+ const validator = createRequestValidator();
303
+
304
+ expect(validator.validate).toBeDefined();
305
+ expect(validator.validateSize).toBeDefined();
306
+ expect(validator.validateContentType).toBeDefined();
307
+ expect(validator.validateJson).toBeDefined();
308
+ expect(validator.validatePath).toBeDefined();
309
+ });
310
+
311
+ it('validates full request', () => {
312
+ const validator = createRequestValidator({
313
+ maxBodySize: 10000,
314
+ maxJsonDepth: 5,
315
+ allowedContentTypes: ['application/json'],
316
+ });
317
+
318
+ const result = validator.validate({
319
+ contentLength: 1000,
320
+ contentType: 'application/json',
321
+ path: '/api/users',
322
+ body: { name: 'test' },
323
+ });
324
+
325
+ expect(result.valid).toBe(true);
326
+ });
327
+
328
+ it('returns all validation errors', () => {
329
+ const validator = createRequestValidator({
330
+ maxBodySize: 100,
331
+ maxJsonDepth: 2,
332
+ });
333
+
334
+ const result = validator.validate({
335
+ contentLength: 10000,
336
+ contentType: 'application/json',
337
+ path: '/api/../etc/passwd',
338
+ body: { a: { b: { c: { d: 'value' } } } },
339
+ });
340
+
341
+ expect(result.valid).toBe(false);
342
+ expect(result.errors.length).toBeGreaterThan(1);
343
+ });
344
+ });
345
+ });