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,331 @@
1
+ /**
2
+ * Error Sanitizer Tests
3
+ *
4
+ * Tests for sanitizing error messages for production.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ sanitizeError,
10
+ createErrorSanitizer,
11
+ formatErrorResponse,
12
+ isOperationalError,
13
+ } from './error-sanitizer.js';
14
+
15
+ describe('error-sanitizer', () => {
16
+ describe('sanitizeError', () => {
17
+ it('removes stack trace in production', () => {
18
+ const error = new Error('Something went wrong');
19
+ error.stack = 'Error: Something went wrong\n at /app/src/handler.js:42:15';
20
+
21
+ const result = sanitizeError(error, { production: true });
22
+
23
+ expect(result.stack).toBeUndefined();
24
+ });
25
+
26
+ it('preserves stack trace in development', () => {
27
+ const error = new Error('Something went wrong');
28
+ error.stack = 'Error: Something went wrong\n at /app/src/handler.js:42:15';
29
+
30
+ const result = sanitizeError(error, { production: false });
31
+
32
+ expect(result.stack).toBeDefined();
33
+ expect(result.stack).toContain('handler.js');
34
+ });
35
+
36
+ it('removes file paths from error message', () => {
37
+ const error = new Error('Failed to read /home/user/app/secrets/config.json');
38
+
39
+ const result = sanitizeError(error, { production: true });
40
+
41
+ expect(result.message).not.toContain('/home/user');
42
+ expect(result.message).not.toContain('secrets');
43
+ });
44
+
45
+ it('removes Windows file paths', () => {
46
+ const error = new Error('Cannot find C:\\Users\\admin\\app\\config.json');
47
+
48
+ const result = sanitizeError(error, { production: true });
49
+
50
+ expect(result.message).not.toContain('C:\\Users');
51
+ });
52
+
53
+ it('genericizes database errors', () => {
54
+ const error = new Error('ECONNREFUSED 127.0.0.1:5432 - PostgreSQL connection failed');
55
+
56
+ const result = sanitizeError(error, { production: true });
57
+
58
+ expect(result.message).toBe('A database error occurred');
59
+ expect(result.message).not.toContain('127.0.0.1');
60
+ expect(result.message).not.toContain('5432');
61
+ });
62
+
63
+ it('genericizes SQL syntax errors', () => {
64
+ const error = new Error("syntax error at or near 'SELECT' at position 42");
65
+
66
+ const result = sanitizeError(error, { production: true });
67
+
68
+ expect(result.message).toBe('A database error occurred');
69
+ });
70
+
71
+ it('preserves user-friendly message', () => {
72
+ const error = new Error('Invalid email address');
73
+ error.isUserFriendly = true;
74
+
75
+ const result = sanitizeError(error, { production: true });
76
+
77
+ expect(result.message).toBe('Invalid email address');
78
+ });
79
+
80
+ it('returns error ID for support reference', () => {
81
+ const error = new Error('Internal error');
82
+
83
+ const result = sanitizeError(error, { production: true });
84
+
85
+ expect(result.id).toBeDefined();
86
+ expect(result.id).toMatch(/^[a-z0-9-]+$/);
87
+ });
88
+
89
+ it('handles nested error.cause', () => {
90
+ const cause = new Error('Database timeout at /app/db/pool.js:123');
91
+ const error = new Error('Request failed');
92
+ error.cause = cause;
93
+
94
+ const result = sanitizeError(error, { production: true });
95
+
96
+ expect(result.message).not.toContain('/app/db');
97
+ expect(result.cause).toBeUndefined(); // Cause should be removed in production
98
+ });
99
+
100
+ it('preserves cause in development', () => {
101
+ const cause = new Error('Database timeout');
102
+ const error = new Error('Request failed');
103
+ error.cause = cause;
104
+
105
+ const result = sanitizeError(error, { production: false });
106
+
107
+ expect(result.cause).toBeDefined();
108
+ });
109
+
110
+ it('handles circular references', () => {
111
+ const error = new Error('Circular error');
112
+ error.circular = error;
113
+
114
+ expect(() => {
115
+ sanitizeError(error, { production: true });
116
+ }).not.toThrow();
117
+ });
118
+
119
+ it('removes sensitive property names', () => {
120
+ const error = new Error('Auth failed');
121
+ error.password = 'secret123';
122
+ error.apiKey = 'sk_live_xxx';
123
+ error.token = 'jwt_token_here';
124
+
125
+ const result = sanitizeError(error, { production: true });
126
+
127
+ expect(result.password).toBeUndefined();
128
+ expect(result.apiKey).toBeUndefined();
129
+ expect(result.token).toBeUndefined();
130
+ });
131
+ });
132
+
133
+ describe('createErrorSanitizer', () => {
134
+ it('creates sanitizer with custom patterns', () => {
135
+ const sanitizer = createErrorSanitizer({
136
+ production: true,
137
+ redactPatterns: [/SECRET_\w+/gi],
138
+ });
139
+
140
+ const error = new Error('Key is SECRET_ABC123');
141
+ const result = sanitizer.sanitize(error);
142
+
143
+ expect(result.message).not.toContain('SECRET_ABC123');
144
+ });
145
+
146
+ it('creates sanitizer with custom generic messages', () => {
147
+ const sanitizer = createErrorSanitizer({
148
+ production: true,
149
+ genericMessages: {
150
+ database: 'Database is temporarily unavailable',
151
+ auth: 'Authentication error',
152
+ },
153
+ });
154
+
155
+ const error = new Error('ECONNREFUSED PostgreSQL');
156
+ const result = sanitizer.sanitize(error);
157
+
158
+ expect(result.message).toBe('Database is temporarily unavailable');
159
+ });
160
+
161
+ it('logs original error before sanitization', () => {
162
+ const logger = vi.fn();
163
+ const sanitizer = createErrorSanitizer({
164
+ production: true,
165
+ logger,
166
+ });
167
+
168
+ const error = new Error('Sensitive internal error');
169
+ sanitizer.sanitize(error);
170
+
171
+ expect(logger).toHaveBeenCalledWith(expect.objectContaining({
172
+ originalMessage: 'Sensitive internal error',
173
+ }));
174
+ });
175
+ });
176
+
177
+ describe('formatErrorResponse', () => {
178
+ it('formats error for HTTP response', () => {
179
+ const error = new Error('Not found');
180
+ error.statusCode = 404;
181
+
182
+ const response = formatErrorResponse(error, { production: true });
183
+
184
+ expect(response).toEqual({
185
+ error: {
186
+ message: 'Not found',
187
+ code: 'NOT_FOUND',
188
+ id: expect.any(String),
189
+ },
190
+ });
191
+ });
192
+
193
+ it('includes status code', () => {
194
+ const error = new Error('Bad request');
195
+ error.statusCode = 400;
196
+
197
+ const response = formatErrorResponse(error, {
198
+ production: true,
199
+ includeStatus: true,
200
+ });
201
+
202
+ expect(response.error.status).toBe(400);
203
+ });
204
+
205
+ it('maps error codes correctly', () => {
206
+ const testCases = [
207
+ { statusCode: 400, expectedCode: 'BAD_REQUEST' },
208
+ { statusCode: 401, expectedCode: 'UNAUTHORIZED' },
209
+ { statusCode: 403, expectedCode: 'FORBIDDEN' },
210
+ { statusCode: 404, expectedCode: 'NOT_FOUND' },
211
+ { statusCode: 409, expectedCode: 'CONFLICT' },
212
+ { statusCode: 422, expectedCode: 'UNPROCESSABLE_ENTITY' },
213
+ { statusCode: 429, expectedCode: 'TOO_MANY_REQUESTS' },
214
+ { statusCode: 500, expectedCode: 'INTERNAL_ERROR' },
215
+ { statusCode: 502, expectedCode: 'BAD_GATEWAY' },
216
+ { statusCode: 503, expectedCode: 'SERVICE_UNAVAILABLE' },
217
+ ];
218
+
219
+ for (const { statusCode, expectedCode } of testCases) {
220
+ const error = new Error('Error');
221
+ error.statusCode = statusCode;
222
+
223
+ const response = formatErrorResponse(error, { production: true });
224
+
225
+ expect(response.error.code).toBe(expectedCode);
226
+ }
227
+ });
228
+
229
+ it('includes validation errors for 400', () => {
230
+ const error = new Error('Validation failed');
231
+ error.statusCode = 400;
232
+ error.validationErrors = [
233
+ { field: 'email', message: 'Invalid email format' },
234
+ { field: 'age', message: 'Must be positive' },
235
+ ];
236
+
237
+ const response = formatErrorResponse(error, { production: true });
238
+
239
+ expect(response.error.details).toEqual([
240
+ { field: 'email', message: 'Invalid email format' },
241
+ { field: 'age', message: 'Must be positive' },
242
+ ]);
243
+ });
244
+
245
+ it('includes stack in development', () => {
246
+ const error = new Error('Dev error');
247
+ error.stack = 'Error: Dev error\n at test.js:1';
248
+
249
+ const response = formatErrorResponse(error, { production: false });
250
+
251
+ expect(response.error.stack).toBeDefined();
252
+ });
253
+ });
254
+
255
+ describe('isOperationalError', () => {
256
+ it('identifies validation errors as operational', () => {
257
+ const error = new Error('Invalid input');
258
+ error.statusCode = 400;
259
+
260
+ expect(isOperationalError(error)).toBe(true);
261
+ });
262
+
263
+ it('identifies auth errors as operational', () => {
264
+ const error = new Error('Unauthorized');
265
+ error.statusCode = 401;
266
+
267
+ expect(isOperationalError(error)).toBe(true);
268
+ });
269
+
270
+ it('identifies 5xx errors as non-operational', () => {
271
+ const error = new Error('Server error');
272
+ error.statusCode = 500;
273
+
274
+ expect(isOperationalError(error)).toBe(false);
275
+ });
276
+
277
+ it('identifies programmer errors as non-operational', () => {
278
+ const error = new TypeError('Cannot read property of undefined');
279
+
280
+ expect(isOperationalError(error)).toBe(false);
281
+ });
282
+
283
+ it('identifies custom operational errors', () => {
284
+ const error = new Error('Business rule violation');
285
+ error.isOperational = true;
286
+
287
+ expect(isOperationalError(error)).toBe(true);
288
+ });
289
+ });
290
+
291
+ describe('edge cases', () => {
292
+ it('handles null error', () => {
293
+ const result = sanitizeError(null, { production: true });
294
+
295
+ expect(result.message).toBe('An unexpected error occurred');
296
+ });
297
+
298
+ it('handles undefined error', () => {
299
+ const result = sanitizeError(undefined, { production: true });
300
+
301
+ expect(result.message).toBe('An unexpected error occurred');
302
+ });
303
+
304
+ it('handles non-Error objects', () => {
305
+ const result = sanitizeError('string error', { production: true });
306
+
307
+ expect(result.message).toBe('An error occurred');
308
+ });
309
+
310
+ it('handles error with no message', () => {
311
+ const error = new Error();
312
+
313
+ const result = sanitizeError(error, { production: true });
314
+
315
+ expect(result.message).toBe('An unexpected error occurred');
316
+ });
317
+
318
+ it('handles deeply nested errors', () => {
319
+ let error = new Error('Deep error');
320
+ for (let i = 0; i < 10; i++) {
321
+ const wrapper = new Error(`Wrapper ${i}`);
322
+ wrapper.cause = error;
323
+ error = wrapper;
324
+ }
325
+
326
+ expect(() => {
327
+ sanitizeError(error, { production: true });
328
+ }).not.toThrow();
329
+ });
330
+ });
331
+ });
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Security Headers Generator Module
3
+ *
4
+ * Generates secure HTTP headers to prevent common attacks.
5
+ * Addresses OWASP A05: Security Misconfiguration
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+
10
+ /**
11
+ * Valid CSP directives
12
+ */
13
+ const VALID_CSP_DIRECTIVES = new Set([
14
+ 'default-src', 'script-src', 'style-src', 'img-src', 'font-src',
15
+ 'connect-src', 'media-src', 'object-src', 'frame-src', 'child-src',
16
+ 'worker-src', 'frame-ancestors', 'form-action', 'base-uri', 'manifest-src',
17
+ 'upgrade-insecure-requests', 'block-all-mixed-content', 'report-uri', 'report-to',
18
+ 'require-trusted-types-for', 'trusted-types', 'sandbox',
19
+ ]);
20
+
21
+ /**
22
+ * Valid Permissions-Policy features
23
+ */
24
+ const VALID_PERMISSIONS_FEATURES = new Set([
25
+ 'accelerometer', 'ambient-light-sensor', 'autoplay', 'battery', 'camera',
26
+ 'display-capture', 'document-domain', 'encrypted-media', 'fullscreen',
27
+ 'geolocation', 'gyroscope', 'layout-animations', 'legacy-image-formats',
28
+ 'magnetometer', 'microphone', 'midi', 'oversized-images', 'payment',
29
+ 'picture-in-picture', 'publickey-credentials-get', 'sync-xhr', 'usb',
30
+ 'wake-lock', 'xr-spatial-tracking', 'interest-cohort',
31
+ ]);
32
+
33
+ /**
34
+ * Default CSP directives (strict)
35
+ */
36
+ const DEFAULT_CSP = {
37
+ 'default-src': ["'self'"],
38
+ 'script-src': ["'self'"],
39
+ 'style-src': ["'self'"],
40
+ 'img-src': ["'self'", 'data:'],
41
+ 'font-src': ["'self'"],
42
+ 'connect-src': ["'self'"],
43
+ 'frame-ancestors': ["'none'"],
44
+ 'base-uri': ["'self'"],
45
+ 'form-action': ["'self'"],
46
+ 'upgrade-insecure-requests': [],
47
+ 'block-all-mixed-content': [],
48
+ };
49
+
50
+ /**
51
+ * Generate Content Security Policy header value
52
+ * @param {Object} options - CSP options
53
+ * @returns {string} CSP header value
54
+ */
55
+ export function generateCsp(options = {}) {
56
+ const {
57
+ useNonce = false,
58
+ nonce = null,
59
+ scriptSrc = null,
60
+ styleSrc = null,
61
+ imgSrc = null,
62
+ connectSrc = null,
63
+ frameAncestors = null,
64
+ reportUri = null,
65
+ reportTo = null,
66
+ mode = null,
67
+ reportOnly = false, // Consumed here, not passed to CSP string
68
+ ...customDirectives
69
+ } = options;
70
+
71
+ // Validate custom directive names
72
+ for (const directive of Object.keys(customDirectives)) {
73
+ if (!VALID_CSP_DIRECTIVES.has(directive)) {
74
+ throw new Error(`Invalid CSP directive: ${directive}`);
75
+ }
76
+ }
77
+
78
+ // Start with defaults
79
+ const directives = { ...DEFAULT_CSP };
80
+
81
+ // Apply script-src
82
+ if (scriptSrc && scriptSrc.length > 0) {
83
+ directives['script-src'] = ["'self'", ...scriptSrc];
84
+ }
85
+
86
+ // Apply style-src
87
+ if (styleSrc && styleSrc.length > 0) {
88
+ directives['style-src'] = ["'self'", ...styleSrc];
89
+ }
90
+
91
+ // Apply img-src
92
+ if (imgSrc && imgSrc.length > 0) {
93
+ directives['img-src'] = ["'self'", ...imgSrc];
94
+ }
95
+
96
+ // Apply connect-src
97
+ if (connectSrc && connectSrc.length > 0) {
98
+ directives['connect-src'] = ["'self'", ...connectSrc];
99
+ }
100
+
101
+ // Apply frame-ancestors
102
+ if (frameAncestors) {
103
+ directives['frame-ancestors'] = frameAncestors;
104
+ }
105
+
106
+ // Add nonce if configured
107
+ if (useNonce && nonce) {
108
+ directives['script-src'] = [...(directives['script-src'] || ["'self'"]), `'nonce-${nonce}'`];
109
+
110
+ // SPA mode adds strict-dynamic
111
+ if (mode === 'spa') {
112
+ directives['script-src'].push("'strict-dynamic'");
113
+ }
114
+ }
115
+
116
+ // Build CSP string
117
+ const parts = [];
118
+
119
+ for (const [directive, values] of Object.entries(directives)) {
120
+ if (values && values.length > 0) {
121
+ parts.push(`${directive} ${values.join(' ')}`);
122
+ } else if (directive === 'upgrade-insecure-requests' || directive === 'block-all-mixed-content') {
123
+ parts.push(directive);
124
+ }
125
+ }
126
+
127
+ // Add reporting
128
+ if (reportUri) {
129
+ parts.push(`report-uri ${reportUri}`);
130
+ }
131
+ if (reportTo) {
132
+ parts.push(`report-to ${reportTo}`);
133
+ }
134
+
135
+ return parts.join('; ');
136
+ }
137
+
138
+ /**
139
+ * Generate HSTS header value
140
+ * @param {Object} options - HSTS options
141
+ * @returns {string} HSTS header value
142
+ */
143
+ export function generateHsts(options = {}) {
144
+ const {
145
+ maxAge = 31536000, // 1 year
146
+ includeSubDomains = true,
147
+ preload = false,
148
+ } = options;
149
+
150
+ // Preload requires at least 1 year (31536000 seconds)
151
+ if (preload && maxAge < 31536000) {
152
+ throw new Error('HSTS preload requires max-age of at least 31536000 seconds (1 year)');
153
+ }
154
+
155
+ let hsts = `max-age=${maxAge}`;
156
+
157
+ if (includeSubDomains) {
158
+ hsts += '; includeSubDomains';
159
+ }
160
+
161
+ if (preload) {
162
+ hsts += '; preload';
163
+ }
164
+
165
+ return hsts;
166
+ }
167
+
168
+ /**
169
+ * Generate Permissions-Policy header value
170
+ * @param {Object} policies - Feature policies
171
+ * @returns {string} Permissions-Policy value
172
+ */
173
+ export function generatePermissionsPolicy(policies = {}) {
174
+ // Validate feature names
175
+ for (const feature of Object.keys(policies)) {
176
+ if (!VALID_PERMISSIONS_FEATURES.has(feature)) {
177
+ throw new Error(`Invalid Permissions-Policy feature: ${feature}`);
178
+ }
179
+ }
180
+
181
+ const defaultPolicies = {
182
+ accelerometer: [],
183
+ 'ambient-light-sensor': [],
184
+ autoplay: [],
185
+ battery: [],
186
+ camera: [],
187
+ 'display-capture': [],
188
+ 'encrypted-media': [],
189
+ fullscreen: ['self'],
190
+ geolocation: [],
191
+ gyroscope: [],
192
+ magnetometer: [],
193
+ microphone: [],
194
+ midi: [],
195
+ payment: [],
196
+ 'picture-in-picture': [],
197
+ 'publickey-credentials-get': [],
198
+ 'sync-xhr': [],
199
+ usb: [],
200
+ 'interest-cohort': [],
201
+ };
202
+
203
+ const merged = { ...defaultPolicies, ...policies };
204
+
205
+ return Object.entries(merged)
206
+ .map(([feature, allowList]) => {
207
+ if (allowList.length === 0) {
208
+ return `${feature}=()`;
209
+ }
210
+ const formatted = allowList.map((item) => {
211
+ if (item === 'self') return 'self';
212
+ if (item === '*') return '*';
213
+ return `"${item}"`;
214
+ }).join(' ');
215
+ return `${feature}=(${formatted})`;
216
+ })
217
+ .join(', ');
218
+ }
219
+
220
+ /**
221
+ * Generate all security headers
222
+ * @param {Object} options - Header options
223
+ * @returns {Object} Security headers
224
+ */
225
+ export function generateSecurityHeaders(options = {}) {
226
+ const {
227
+ csp = {},
228
+ frameOptions = 'DENY',
229
+ contentTypeOptions = true,
230
+ referrerPolicy = 'strict-origin-when-cross-origin',
231
+ hsts = {},
232
+ permissionsPolicy = {},
233
+ } = options;
234
+
235
+ const headers = {};
236
+
237
+ // Content Security Policy
238
+ headers['Content-Security-Policy'] = generateCsp(csp);
239
+
240
+ // X-Frame-Options (clickjacking protection)
241
+ if (frameOptions) {
242
+ headers['X-Frame-Options'] = frameOptions;
243
+ }
244
+
245
+ // X-Content-Type-Options (MIME sniffing protection)
246
+ if (contentTypeOptions) {
247
+ headers['X-Content-Type-Options'] = 'nosniff';
248
+ }
249
+
250
+ // Referrer-Policy
251
+ if (referrerPolicy) {
252
+ headers['Referrer-Policy'] = referrerPolicy;
253
+ }
254
+
255
+ // Strict-Transport-Security
256
+ headers['Strict-Transport-Security'] = generateHsts(hsts);
257
+
258
+ // Permissions-Policy
259
+ headers['Permissions-Policy'] = generatePermissionsPolicy(permissionsPolicy);
260
+
261
+ // Cross-Origin headers
262
+ headers['Cross-Origin-Opener-Policy'] = 'same-origin';
263
+ headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
264
+
265
+ return headers;
266
+ }
267
+
268
+ /**
269
+ * Create a headers generator with preset configuration
270
+ * @param {Object} config - Generator configuration
271
+ * @returns {Object} Headers generator instance
272
+ */
273
+ export function createHeadersGenerator(config = {}) {
274
+ const {
275
+ csp = {},
276
+ cspReportOnly = null,
277
+ hsts = {},
278
+ permissionsPolicy = {},
279
+ } = config;
280
+
281
+ return {
282
+ /**
283
+ * Generate headers
284
+ * @param {Object} options - Per-request options
285
+ * @returns {Object} Security headers
286
+ */
287
+ generate(options = {}) {
288
+ const { overrides = {}, route } = options;
289
+
290
+ // Merge CSP options
291
+ let cspOptions = { ...csp };
292
+ if (overrides.csp) {
293
+ cspOptions = { ...cspOptions, ...overrides.csp };
294
+ }
295
+
296
+ // Auto-generate nonce if useNonce is configured
297
+ if (cspOptions.useNonce && !cspOptions.nonce) {
298
+ cspOptions.nonce = crypto.randomBytes(16).toString('base64');
299
+ }
300
+
301
+ const headers = {};
302
+
303
+ // Generate main CSP
304
+ if (csp.reportOnly) {
305
+ headers['Content-Security-Policy-Report-Only'] = generateCsp(cspOptions);
306
+ } else {
307
+ headers['Content-Security-Policy'] = generateCsp(cspOptions);
308
+ }
309
+
310
+ // Generate report-only CSP if configured separately
311
+ if (cspReportOnly) {
312
+ headers['Content-Security-Policy-Report-Only'] = generateCsp(cspReportOnly);
313
+ }
314
+
315
+ // HSTS
316
+ const hstsOptions = overrides.hsts ? { ...hsts, ...overrides.hsts } : hsts;
317
+ headers['Strict-Transport-Security'] = generateHsts(hstsOptions);
318
+
319
+ // Other headers
320
+ headers['X-Frame-Options'] = 'DENY';
321
+ headers['X-Content-Type-Options'] = 'nosniff';
322
+ headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
323
+ headers['Permissions-Policy'] = generatePermissionsPolicy(permissionsPolicy);
324
+ headers['Cross-Origin-Opener-Policy'] = 'same-origin';
325
+ headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
326
+
327
+ return headers;
328
+ },
329
+
330
+ /**
331
+ * Generate headers with nonce
332
+ * @returns {{headers: Object, nonce: string}} Headers and nonce
333
+ */
334
+ generateWithNonce() {
335
+ const nonce = crypto.randomBytes(16).toString('base64');
336
+ const cspOptions = { ...csp, useNonce: true, nonce };
337
+
338
+ const headers = {};
339
+ headers['Content-Security-Policy'] = generateCsp(cspOptions);
340
+
341
+ // HSTS
342
+ headers['Strict-Transport-Security'] = generateHsts(hsts);
343
+
344
+ // Other headers
345
+ headers['X-Frame-Options'] = 'DENY';
346
+ headers['X-Content-Type-Options'] = 'nosniff';
347
+ headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
348
+ headers['Permissions-Policy'] = generatePermissionsPolicy(permissionsPolicy);
349
+ headers['Cross-Origin-Opener-Policy'] = 'same-origin';
350
+ headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
351
+
352
+ return { headers, nonce };
353
+ },
354
+
355
+ /**
356
+ * Express middleware
357
+ */
358
+ middleware() {
359
+ return (req, res, next) => {
360
+ const headers = this.generate();
361
+ Object.entries(headers).forEach(([key, value]) => {
362
+ res.setHeader(key, value);
363
+ });
364
+ next();
365
+ };
366
+ },
367
+ };
368
+ }