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,278 @@
1
+ /**
2
+ * CORS Validator Module
3
+ *
4
+ * Strict CORS configuration to prevent cross-origin attacks.
5
+ * Addresses OWASP A05: Security Misconfiguration
6
+ */
7
+
8
+ /**
9
+ * Custom error for CORS security violations
10
+ */
11
+ export class CorsSecurityError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'CorsSecurityError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Simple headers that don't need explicit CORS allowance
20
+ */
21
+ const SIMPLE_HEADERS = ['accept', 'accept-language', 'content-language', 'content-type'];
22
+
23
+ /**
24
+ * Validate an origin against allowed origins
25
+ * @param {string|null} origin - Origin to validate
26
+ * @param {Object} options - Validation options
27
+ * @returns {Object} Validation result
28
+ */
29
+ export function validateOrigin(origin, options = {}) {
30
+ const {
31
+ allowedOrigins = [],
32
+ production = false,
33
+ allowNull = false,
34
+ } = options;
35
+
36
+ // Check wildcard in production
37
+ if (allowedOrigins.includes('*') && production) {
38
+ throw new CorsSecurityError('Wildcard origin (*) not allowed in production');
39
+ }
40
+
41
+ // Handle null origin
42
+ if (origin === null) {
43
+ return allowNull
44
+ ? { allowed: true }
45
+ : { allowed: false, reason: 'Null origin not allowed' };
46
+ }
47
+
48
+ // Wildcard in development
49
+ if (allowedOrigins.includes('*') && !production) {
50
+ return { allowed: true };
51
+ }
52
+
53
+ // Validate origin format - reject credentials in URL
54
+ if (origin && origin.includes('@')) {
55
+ return { allowed: false, reason: 'Origin contains credentials' };
56
+ }
57
+
58
+ // Reject trailing slashes
59
+ if (origin && origin.endsWith('/')) {
60
+ return { allowed: false, reason: 'Origin has trailing slash' };
61
+ }
62
+
63
+ // Reject paths
64
+ try {
65
+ const url = new URL(origin);
66
+ if (url.pathname !== '/' && url.pathname !== '') {
67
+ return { allowed: false, reason: 'Origin contains path' };
68
+ }
69
+ } catch {
70
+ return { allowed: false, reason: 'Invalid origin format' };
71
+ }
72
+
73
+ // Normalize for comparison
74
+ const normalizedOrigin = origin.toLowerCase();
75
+
76
+ for (const allowed of allowedOrigins) {
77
+ // Exact match (case insensitive)
78
+ if (allowed.toLowerCase() === normalizedOrigin) {
79
+ return { allowed: true };
80
+ }
81
+
82
+ // Subdomain wildcard pattern
83
+ if (allowed.includes('*')) {
84
+ const pattern = allowed
85
+ .replace(/\./g, '\\.')
86
+ .replace(/\*/g, '[a-z0-9-]+');
87
+ const regex = new RegExp(`^${pattern}$`, 'i');
88
+
89
+ // Limit pattern length to prevent ReDoS
90
+ if (origin.length <= 100 && regex.test(origin)) {
91
+ return { allowed: true };
92
+ }
93
+ }
94
+ }
95
+
96
+ return { allowed: false, reason: 'Origin not in whitelist' };
97
+ }
98
+
99
+ /**
100
+ * Generate CORS headers for a response
101
+ * @param {Object} options - Header options
102
+ * @returns {Object} CORS headers
103
+ */
104
+ export function generateCorsHeaders(options = {}) {
105
+ const {
106
+ origin,
107
+ allowedOrigins = [],
108
+ credentials = false,
109
+ exposeHeaders = [],
110
+ production = false,
111
+ } = options;
112
+
113
+ const validation = validateOrigin(origin, { allowedOrigins, production });
114
+
115
+ if (!validation.allowed) {
116
+ return {};
117
+ }
118
+
119
+ const headers = {
120
+ 'Access-Control-Allow-Origin': origin,
121
+ 'Vary': 'Origin',
122
+ };
123
+
124
+ if (credentials) {
125
+ headers['Access-Control-Allow-Credentials'] = 'true';
126
+ }
127
+
128
+ if (exposeHeaders.length > 0) {
129
+ headers['Access-Control-Expose-Headers'] = exposeHeaders.join(', ');
130
+ }
131
+
132
+ return headers;
133
+ }
134
+
135
+ /**
136
+ * Handle preflight OPTIONS request
137
+ * @param {Object} options - Preflight options
138
+ * @returns {Object} Preflight response headers
139
+ */
140
+ export function handlePreflight(options = {}) {
141
+ const {
142
+ origin,
143
+ requestMethod,
144
+ requestHeaders = [],
145
+ allowedOrigins = [],
146
+ allowedMethods = ['GET', 'POST'],
147
+ allowedHeaders = [],
148
+ credentials = false,
149
+ maxAge = 86400,
150
+ production = false,
151
+ } = options;
152
+
153
+ const validation = validateOrigin(origin, { allowedOrigins, production });
154
+
155
+ if (!validation.allowed) {
156
+ return {};
157
+ }
158
+
159
+ const headers = {
160
+ 'Access-Control-Allow-Origin': origin,
161
+ 'Vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers',
162
+ };
163
+
164
+ // Filter allowed methods
165
+ const allowedMethodsList = allowedMethods.filter((m) =>
166
+ requestMethod ? allowedMethods.includes(requestMethod.toUpperCase()) : true
167
+ );
168
+ headers['Access-Control-Allow-Methods'] = allowedMethodsList.join(', ');
169
+
170
+ // Filter allowed headers (always allow simple headers)
171
+ const normalizedAllowedHeaders = [
172
+ ...allowedHeaders.map((h) => h.toLowerCase()),
173
+ ...SIMPLE_HEADERS,
174
+ ];
175
+ const normalizedRequestHeaders = requestHeaders.map((h) => h.toLowerCase());
176
+
177
+ const filteredHeaders = normalizedRequestHeaders.filter((h) =>
178
+ normalizedAllowedHeaders.includes(h) || SIMPLE_HEADERS.includes(h)
179
+ );
180
+
181
+ if (filteredHeaders.length > 0 || allowedHeaders.length > 0) {
182
+ headers['Access-Control-Allow-Headers'] = [...new Set([
183
+ ...allowedHeaders,
184
+ ...filteredHeaders.filter((h) => SIMPLE_HEADERS.includes(h)),
185
+ ])].join(', ') || 'Accept, Content-Type';
186
+ }
187
+
188
+ if (credentials) {
189
+ headers['Access-Control-Allow-Credentials'] = 'true';
190
+ }
191
+
192
+ if (maxAge) {
193
+ headers['Access-Control-Max-Age'] = String(maxAge);
194
+ }
195
+
196
+ return headers;
197
+ }
198
+
199
+ /**
200
+ * Create a reusable CORS validator
201
+ * @param {Object} config - CORS configuration
202
+ * @returns {Object} CORS validator instance
203
+ */
204
+ export function createCorsValidator(config = {}) {
205
+ const {
206
+ allowedOrigins = [],
207
+ allowedMethods = ['GET', 'POST', 'PUT', 'DELETE'],
208
+ allowedHeaders = ['Content-Type', 'Authorization'],
209
+ credentials = false,
210
+ maxAge = 86400,
211
+ production = false,
212
+ } = config;
213
+
214
+ return {
215
+ validate(origin) {
216
+ return validateOrigin(origin, { allowedOrigins, production });
217
+ },
218
+
219
+ isMethodAllowed(method) {
220
+ return allowedMethods.map((m) => m.toUpperCase()).includes(method.toUpperCase());
221
+ },
222
+
223
+ isHeaderAllowed(header) {
224
+ const normalized = header.toLowerCase();
225
+ return (
226
+ SIMPLE_HEADERS.includes(normalized) ||
227
+ allowedHeaders.map((h) => h.toLowerCase()).includes(normalized)
228
+ );
229
+ },
230
+
231
+ getHeaders(origin) {
232
+ return generateCorsHeaders({
233
+ origin,
234
+ allowedOrigins,
235
+ credentials,
236
+ production,
237
+ });
238
+ },
239
+
240
+ handlePreflight(origin, requestMethod, requestHeaders) {
241
+ return handlePreflight({
242
+ origin,
243
+ requestMethod,
244
+ requestHeaders,
245
+ allowedOrigins,
246
+ allowedMethods,
247
+ allowedHeaders,
248
+ credentials,
249
+ maxAge,
250
+ production,
251
+ });
252
+ },
253
+
254
+ middleware() {
255
+ return (req, res, next) => {
256
+ const origin = req.headers.origin;
257
+ const headers = this.getHeaders(origin);
258
+ Object.entries(headers).forEach(([key, value]) => {
259
+ res.setHeader(key, value);
260
+ });
261
+ if (req.method === 'OPTIONS') {
262
+ const preflightHeaders = this.handlePreflight(
263
+ origin,
264
+ req.headers['access-control-request-method'],
265
+ (req.headers['access-control-request-headers'] || '').split(',').map((h) => h.trim())
266
+ );
267
+ Object.entries(preflightHeaders).forEach(([key, value]) => {
268
+ res.setHeader(key, value);
269
+ });
270
+ res.statusCode = 204;
271
+ res.end();
272
+ return;
273
+ }
274
+ next();
275
+ };
276
+ },
277
+ };
278
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * CORS Validator Tests
3
+ *
4
+ * Tests for strict CORS configuration.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ validateOrigin,
10
+ generateCorsHeaders,
11
+ handlePreflight,
12
+ createCorsValidator,
13
+ CorsSecurityError,
14
+ } from './cors-validator.js';
15
+
16
+ describe('cors-validator', () => {
17
+ describe('validateOrigin', () => {
18
+ it('allows whitelisted origin', () => {
19
+ const result = validateOrigin('https://example.com', {
20
+ allowedOrigins: ['https://example.com', 'https://app.example.com'],
21
+ });
22
+
23
+ expect(result.allowed).toBe(true);
24
+ });
25
+
26
+ it('rejects non-whitelisted origin', () => {
27
+ const result = validateOrigin('https://evil.com', {
28
+ allowedOrigins: ['https://example.com'],
29
+ });
30
+
31
+ expect(result.allowed).toBe(false);
32
+ expect(result.reason).toContain('not in whitelist');
33
+ });
34
+
35
+ it('rejects wildcard (*) in production mode', () => {
36
+ expect(() => {
37
+ validateOrigin('https://example.com', {
38
+ allowedOrigins: ['*'],
39
+ production: true,
40
+ });
41
+ }).toThrow(CorsSecurityError);
42
+ });
43
+
44
+ it('allows wildcard in development mode', () => {
45
+ const result = validateOrigin('https://anything.com', {
46
+ allowedOrigins: ['*'],
47
+ production: false,
48
+ });
49
+
50
+ expect(result.allowed).toBe(true);
51
+ });
52
+
53
+ it('supports pattern matching for subdomains', () => {
54
+ const result = validateOrigin('https://api.example.com', {
55
+ allowedOrigins: ['https://*.example.com'],
56
+ });
57
+
58
+ expect(result.allowed).toBe(true);
59
+ });
60
+
61
+ it('rejects null origin by default', () => {
62
+ const result = validateOrigin(null, {
63
+ allowedOrigins: ['https://example.com'],
64
+ });
65
+
66
+ expect(result.allowed).toBe(false);
67
+ });
68
+
69
+ it('allows null origin when explicitly configured', () => {
70
+ const result = validateOrigin(null, {
71
+ allowedOrigins: ['https://example.com'],
72
+ allowNull: true,
73
+ });
74
+
75
+ expect(result.allowed).toBe(true);
76
+ });
77
+
78
+ it('validates origin protocol', () => {
79
+ const result = validateOrigin('http://example.com', {
80
+ allowedOrigins: ['https://example.com'],
81
+ });
82
+
83
+ expect(result.allowed).toBe(false);
84
+ });
85
+
86
+ it('validates origin port', () => {
87
+ const result = validateOrigin('https://example.com:8080', {
88
+ allowedOrigins: ['https://example.com'],
89
+ });
90
+
91
+ expect(result.allowed).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe('generateCorsHeaders', () => {
96
+ it('generates Access-Control-Allow-Origin header', () => {
97
+ const headers = generateCorsHeaders({
98
+ origin: 'https://example.com',
99
+ allowedOrigins: ['https://example.com'],
100
+ });
101
+
102
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
103
+ });
104
+
105
+ it('generates Vary: Origin header', () => {
106
+ const headers = generateCorsHeaders({
107
+ origin: 'https://example.com',
108
+ allowedOrigins: ['https://example.com'],
109
+ });
110
+
111
+ expect(headers['Vary']).toContain('Origin');
112
+ });
113
+
114
+ it('sets Access-Control-Allow-Credentials when configured', () => {
115
+ const headers = generateCorsHeaders({
116
+ origin: 'https://example.com',
117
+ allowedOrigins: ['https://example.com'],
118
+ credentials: true,
119
+ });
120
+
121
+ expect(headers['Access-Control-Allow-Credentials']).toBe('true');
122
+ });
123
+
124
+ it('does not set credentials header when not configured', () => {
125
+ const headers = generateCorsHeaders({
126
+ origin: 'https://example.com',
127
+ allowedOrigins: ['https://example.com'],
128
+ credentials: false,
129
+ });
130
+
131
+ expect(headers['Access-Control-Allow-Credentials']).toBeUndefined();
132
+ });
133
+
134
+ it('sets Access-Control-Expose-Headers', () => {
135
+ const headers = generateCorsHeaders({
136
+ origin: 'https://example.com',
137
+ allowedOrigins: ['https://example.com'],
138
+ exposeHeaders: ['X-Request-Id', 'X-RateLimit-Remaining'],
139
+ });
140
+
141
+ expect(headers['Access-Control-Expose-Headers']).toContain('X-Request-Id');
142
+ expect(headers['Access-Control-Expose-Headers']).toContain('X-RateLimit-Remaining');
143
+ });
144
+
145
+ it('returns empty object for disallowed origin', () => {
146
+ const headers = generateCorsHeaders({
147
+ origin: 'https://evil.com',
148
+ allowedOrigins: ['https://example.com'],
149
+ });
150
+
151
+ expect(headers['Access-Control-Allow-Origin']).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe('handlePreflight', () => {
156
+ it('returns correct headers for OPTIONS request', () => {
157
+ const headers = handlePreflight({
158
+ origin: 'https://example.com',
159
+ requestMethod: 'POST',
160
+ requestHeaders: ['Content-Type', 'Authorization'],
161
+ allowedOrigins: ['https://example.com'],
162
+ allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
163
+ allowedHeaders: ['Content-Type', 'Authorization'],
164
+ });
165
+
166
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
167
+ expect(headers['Access-Control-Allow-Methods']).toContain('POST');
168
+ expect(headers['Access-Control-Allow-Headers']).toContain('Content-Type');
169
+ });
170
+
171
+ it('sets Access-Control-Max-Age', () => {
172
+ const headers = handlePreflight({
173
+ origin: 'https://example.com',
174
+ requestMethod: 'POST',
175
+ allowedOrigins: ['https://example.com'],
176
+ allowedMethods: ['POST'],
177
+ maxAge: 86400,
178
+ });
179
+
180
+ expect(headers['Access-Control-Max-Age']).toBe('86400');
181
+ });
182
+
183
+ it('rejects disallowed method', () => {
184
+ const headers = handlePreflight({
185
+ origin: 'https://example.com',
186
+ requestMethod: 'DELETE',
187
+ allowedOrigins: ['https://example.com'],
188
+ allowedMethods: ['GET', 'POST'],
189
+ });
190
+
191
+ expect(headers['Access-Control-Allow-Methods']).not.toContain('DELETE');
192
+ });
193
+
194
+ it('rejects disallowed headers', () => {
195
+ const headers = handlePreflight({
196
+ origin: 'https://example.com',
197
+ requestMethod: 'POST',
198
+ requestHeaders: ['X-Custom-Header'],
199
+ allowedOrigins: ['https://example.com'],
200
+ allowedMethods: ['POST'],
201
+ allowedHeaders: ['Content-Type'],
202
+ });
203
+
204
+ expect(headers['Access-Control-Allow-Headers']).not.toContain('X-Custom-Header');
205
+ });
206
+
207
+ it('always allows simple headers', () => {
208
+ const headers = handlePreflight({
209
+ origin: 'https://example.com',
210
+ requestMethod: 'POST',
211
+ requestHeaders: ['Accept', 'Content-Language'],
212
+ allowedOrigins: ['https://example.com'],
213
+ allowedMethods: ['POST'],
214
+ allowedHeaders: [],
215
+ });
216
+
217
+ // Simple headers should be implicitly allowed
218
+ expect(headers['Access-Control-Allow-Headers']).toBeDefined();
219
+ });
220
+ });
221
+
222
+ describe('createCorsValidator', () => {
223
+ it('creates reusable validator with config', () => {
224
+ const cors = createCorsValidator({
225
+ allowedOrigins: ['https://example.com', 'https://app.example.com'],
226
+ allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
227
+ allowedHeaders: ['Content-Type', 'Authorization'],
228
+ credentials: true,
229
+ maxAge: 86400,
230
+ });
231
+
232
+ const result = cors.validate('https://example.com');
233
+ expect(result.allowed).toBe(true);
234
+ });
235
+
236
+ it('validates methods', () => {
237
+ const cors = createCorsValidator({
238
+ allowedOrigins: ['https://example.com'],
239
+ allowedMethods: ['GET', 'POST'],
240
+ });
241
+
242
+ expect(cors.isMethodAllowed('GET')).toBe(true);
243
+ expect(cors.isMethodAllowed('DELETE')).toBe(false);
244
+ });
245
+
246
+ it('validates headers', () => {
247
+ const cors = createCorsValidator({
248
+ allowedOrigins: ['https://example.com'],
249
+ allowedHeaders: ['Content-Type', 'Authorization'],
250
+ });
251
+
252
+ expect(cors.isHeaderAllowed('Content-Type')).toBe(true);
253
+ expect(cors.isHeaderAllowed('X-Custom')).toBe(false);
254
+ });
255
+
256
+ it('generates middleware function', () => {
257
+ const cors = createCorsValidator({
258
+ allowedOrigins: ['https://example.com'],
259
+ });
260
+
261
+ const middleware = cors.middleware();
262
+ expect(typeof middleware).toBe('function');
263
+ });
264
+ });
265
+
266
+ describe('security edge cases', () => {
267
+ it('rejects origins with credentials in URL', () => {
268
+ const result = validateOrigin('https://user:pass@example.com', {
269
+ allowedOrigins: ['https://example.com'],
270
+ });
271
+
272
+ expect(result.allowed).toBe(false);
273
+ });
274
+
275
+ it('handles case-insensitive origin matching', () => {
276
+ const result = validateOrigin('https://EXAMPLE.COM', {
277
+ allowedOrigins: ['https://example.com'],
278
+ });
279
+
280
+ expect(result.allowed).toBe(true);
281
+ });
282
+
283
+ it('rejects origins with trailing slashes', () => {
284
+ const result = validateOrigin('https://example.com/', {
285
+ allowedOrigins: ['https://example.com'],
286
+ });
287
+
288
+ // Origins should not have trailing slashes
289
+ expect(result.allowed).toBe(false);
290
+ });
291
+
292
+ it('rejects origins with path components', () => {
293
+ const result = validateOrigin('https://example.com/path', {
294
+ allowedOrigins: ['https://example.com'],
295
+ });
296
+
297
+ expect(result.allowed).toBe(false);
298
+ });
299
+
300
+ it('validates against regex patterns safely', () => {
301
+ // Ensure regex patterns don't cause ReDoS
302
+ const result = validateOrigin('https://a'.repeat(1000) + '.example.com', {
303
+ allowedOrigins: ['https://*.example.com'],
304
+ });
305
+
306
+ // Should complete quickly and reject
307
+ expect(result.allowed).toBe(false);
308
+ });
309
+ });
310
+ });