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,168 @@
1
+ /**
2
+ * Secrets Encryption at Rest
3
+ */
4
+
5
+ /**
6
+ * Generates a sealed secret
7
+ * @param {Object} options - Configuration options
8
+ * @param {string} options.name - Secret name
9
+ * @param {Object} options.data - Secret data (key-value pairs)
10
+ * @param {string} options.namespace - Target namespace (default: 'default')
11
+ * @param {Function} options.mockSeal - Mock seal function for testing
12
+ * @returns {Object} SealedSecret resource
13
+ */
14
+ export function generateSealedSecret({ name, data = {}, namespace = 'default', mockSeal } = {}) {
15
+ const encryptedData = {};
16
+
17
+ for (const [key, value] of Object.entries(data)) {
18
+ if (mockSeal) {
19
+ encryptedData[key] = mockSeal(value);
20
+ } else {
21
+ // In production, this would use kubeseal
22
+ encryptedData[key] = Buffer.from(value).toString('base64');
23
+ }
24
+ }
25
+
26
+ return {
27
+ apiVersion: 'bitnami.com/v1alpha1',
28
+ kind: 'SealedSecret',
29
+ metadata: {
30
+ name,
31
+ namespace
32
+ },
33
+ spec: {
34
+ encryptedData
35
+ }
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Configures encryption at rest
41
+ * @param {Object} options - Configuration options
42
+ * @param {string} options.provider - Encryption provider ('kms', 'aescbc', 'secretbox')
43
+ * @param {string} options.keyId - Key ID (for KMS)
44
+ * @returns {Object} Encryption configuration
45
+ */
46
+ export function configureEncryptionAtRest({ provider = 'kms', keyId } = {}) {
47
+ const config = {
48
+ kind: 'EncryptionConfiguration',
49
+ apiVersion: 'apiserver.config.k8s.io/v1',
50
+ resources: [{
51
+ resources: ['secrets'],
52
+ providers: []
53
+ }]
54
+ };
55
+
56
+ if (provider === 'kms') {
57
+ config.resources[0].providers.push({
58
+ kms: {
59
+ name: 'kms-provider',
60
+ endpoint: 'unix:///var/run/kms-provider.sock',
61
+ cachesize: 1000,
62
+ timeout: '3s'
63
+ }
64
+ });
65
+ if (keyId) {
66
+ config.resources[0].providers[0].kms.keyId = keyId;
67
+ }
68
+ } else if (provider === 'aescbc') {
69
+ config.resources[0].providers.push({
70
+ aescbc: {
71
+ keys: [{
72
+ name: 'key1',
73
+ secret: '<base64-encoded-secret>'
74
+ }]
75
+ }
76
+ });
77
+ }
78
+
79
+ // Always add identity provider as fallback for reading unencrypted secrets
80
+ config.resources[0].providers.push({ identity: {} });
81
+
82
+ return {
83
+ providers: config.resources[0].providers,
84
+ config
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Generates an ExternalSecret custom resource
90
+ * @param {Object} options - Configuration options
91
+ * @param {string} options.name - Secret name
92
+ * @param {string} options.store - Secret store name
93
+ * @param {Array} options.keys - Keys to fetch from store
94
+ * @param {string} options.namespace - Target namespace (default: 'default')
95
+ * @returns {Object} ExternalSecret resource
96
+ */
97
+ export function generateExternalSecret({ name, store, keys = [], namespace = 'default' } = {}) {
98
+ return {
99
+ apiVersion: 'external-secrets.io/v1beta1',
100
+ kind: 'ExternalSecret',
101
+ metadata: {
102
+ name,
103
+ namespace
104
+ },
105
+ spec: {
106
+ refreshInterval: '1h',
107
+ secretStoreRef: {
108
+ name: store,
109
+ kind: 'SecretStore'
110
+ },
111
+ target: {
112
+ name,
113
+ creationPolicy: 'Owner'
114
+ },
115
+ data: keys.map(key => ({
116
+ secretKey: key,
117
+ remoteRef: {
118
+ key: name,
119
+ property: key
120
+ }
121
+ }))
122
+ }
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Generates Vault integration configuration
128
+ * @param {Object} options - Configuration options
129
+ * @param {string} options.address - Vault server address
130
+ * @param {string} options.role - Vault role
131
+ * @param {string} options.authPath - Auth path (default: 'kubernetes')
132
+ * @returns {string} Vault configuration as string
133
+ */
134
+ export function generateVaultConfig({ address, role, authPath = 'kubernetes' } = {}) {
135
+ return `
136
+ vault:
137
+ address: "${address}"
138
+ auth:
139
+ method: kubernetes
140
+ path: ${authPath}
141
+ role: ${role}
142
+ secrets:
143
+ path: secret/data
144
+ `.trim();
145
+ }
146
+
147
+ /**
148
+ * Creates a secrets encryption manager
149
+ * @returns {Object} Manager with seal, rotate, and getLog methods
150
+ */
151
+ export function createSecretsEncryption() {
152
+ const log = [];
153
+
154
+ return {
155
+ seal: (options) => {
156
+ log.push(`Sealing secret: ${options.name}`);
157
+ return generateSealedSecret(options);
158
+ },
159
+ rotate: (options) => {
160
+ log.push(`Rotating encryption key`);
161
+ return { rotated: true, timestamp: new Date().toISOString() };
162
+ },
163
+ getLog: () => {
164
+ // Never include sensitive data in logs
165
+ return log.join('\n');
166
+ }
167
+ };
168
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Secrets Encryption Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { generateSealedSecret, configureEncryptionAtRest, generateExternalSecret, generateVaultConfig, createSecretsEncryption } from './secrets-encryption.js';
6
+
7
+ describe('secrets-encryption', () => {
8
+ describe('generateSealedSecret', () => {
9
+ it('generates sealed secret', () => {
10
+ const secret = generateSealedSecret({ name: 'db-creds', data: { password: 'secret' }, mockSeal: vi.fn().mockReturnValue('encrypted') });
11
+ expect(secret.kind).toBe('SealedSecret');
12
+ });
13
+ });
14
+
15
+ describe('configureEncryptionAtRest', () => {
16
+ it('configures KMS encryption', () => {
17
+ const config = configureEncryptionAtRest({ provider: 'kms', keyId: 'arn:aws:kms:...' });
18
+ expect(config.providers.some(p => p.kms !== undefined)).toBe(true);
19
+ });
20
+ });
21
+
22
+ describe('generateExternalSecret', () => {
23
+ it('generates ExternalSecret CR', () => {
24
+ const secret = generateExternalSecret({ name: 'db-creds', store: 'vault', keys: ['password'] });
25
+ expect(secret.kind).toBe('ExternalSecret');
26
+ });
27
+ });
28
+
29
+ describe('generateVaultConfig', () => {
30
+ it('generates Vault integration config', () => {
31
+ const config = generateVaultConfig({ address: 'https://vault.example.com', role: 'app' });
32
+ expect(config).toContain('vault.example.com');
33
+ });
34
+ });
35
+
36
+ describe('createSecretsEncryption', () => {
37
+ it('creates manager', () => {
38
+ const manager = createSecretsEncryption();
39
+ expect(manager.seal).toBeDefined();
40
+ expect(manager.rotate).toBeDefined();
41
+ });
42
+
43
+ it('never logs secret values', () => {
44
+ const manager = createSecretsEncryption();
45
+ const log = manager.getLog();
46
+ expect(log).not.toContain('password');
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Alert Manager
3
+ * Alert routing and notifications
4
+ */
5
+
6
+ export const ALERT_SEVERITY = {
7
+ CRITICAL: 'critical',
8
+ WARNING: 'warning',
9
+ INFO: 'info',
10
+ };
11
+
12
+ /**
13
+ * Generate a unique alert ID
14
+ * @returns {string} Unique ID
15
+ */
16
+ function generateId() {
17
+ return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
18
+ }
19
+
20
+ /**
21
+ * Generate a fingerprint for deduplication
22
+ * @param {Object} alert - Alert object
23
+ * @returns {string} Fingerprint
24
+ */
25
+ function generateFingerprint(alert) {
26
+ return `${alert.title}_${alert.severity}_${alert.source || 'unknown'}`;
27
+ }
28
+
29
+ /**
30
+ * Create an alert
31
+ * @param {Object} options - Alert options
32
+ * @param {string} options.title - Alert title
33
+ * @param {string} options.severity - Alert severity
34
+ * @param {string} options.description - Alert description
35
+ * @param {string} options.source - Alert source
36
+ * @returns {Object} Alert object
37
+ */
38
+ export function createAlert(options = {}) {
39
+ const { title, severity = ALERT_SEVERITY.INFO, description, source } = options;
40
+
41
+ const alert = {
42
+ id: generateId(),
43
+ title,
44
+ severity,
45
+ description,
46
+ source,
47
+ timestamp: new Date().toISOString(),
48
+ acknowledged: false,
49
+ };
50
+
51
+ alert.fingerprint = generateFingerprint(alert);
52
+
53
+ return alert;
54
+ }
55
+
56
+ /**
57
+ * Route an alert to appropriate channels
58
+ * @param {Object} alert - Alert object
59
+ * @param {Object} rules - Routing rules by severity
60
+ * @returns {Array} List of channels to send to
61
+ */
62
+ export function routeAlert(alert, rules = {}) {
63
+ const { severity } = alert;
64
+ return rules[severity] || [];
65
+ }
66
+
67
+ /**
68
+ * Send alert to PagerDuty
69
+ * @param {Object} alert - Alert object
70
+ * @param {Object} options - Configuration options
71
+ * @param {Function} options.post - Post function
72
+ * @param {string} options.routingKey - PagerDuty routing key
73
+ * @returns {Object} Response
74
+ */
75
+ export async function sendToPagerDuty(alert, options = {}) {
76
+ const { post, routingKey } = options;
77
+
78
+ if (!post) {
79
+ throw new Error('post function required');
80
+ }
81
+
82
+ const payload = {
83
+ routing_key: routingKey,
84
+ event_action: 'trigger',
85
+ dedup_key: alert.fingerprint || alert.id,
86
+ payload: {
87
+ summary: alert.title,
88
+ severity: alert.severity === ALERT_SEVERITY.CRITICAL ? 'critical' : 'warning',
89
+ source: alert.source || 'monitoring',
90
+ timestamp: alert.timestamp,
91
+ },
92
+ };
93
+
94
+ return post('https://events.pagerduty.com/v2/enqueue', payload);
95
+ }
96
+
97
+ /**
98
+ * Send alert to Slack
99
+ * @param {Object} alert - Alert object
100
+ * @param {Object} options - Configuration options
101
+ * @param {Function} options.post - Post function
102
+ * @param {string} options.webhookUrl - Slack webhook URL
103
+ * @returns {Object} Response
104
+ */
105
+ export async function sendToSlack(alert, options = {}) {
106
+ const { post, webhookUrl } = options;
107
+
108
+ if (!post) {
109
+ throw new Error('post function required');
110
+ }
111
+
112
+ const severityEmoji = {
113
+ [ALERT_SEVERITY.CRITICAL]: ':rotating_light:',
114
+ [ALERT_SEVERITY.WARNING]: ':warning:',
115
+ [ALERT_SEVERITY.INFO]: ':information_source:',
116
+ };
117
+
118
+ const payload = {
119
+ text: `${severityEmoji[alert.severity] || ''} ${alert.title}`,
120
+ attachments: [
121
+ {
122
+ color: alert.severity === ALERT_SEVERITY.CRITICAL ? 'danger' : 'warning',
123
+ fields: [
124
+ { title: 'Severity', value: alert.severity, short: true },
125
+ { title: 'Time', value: alert.timestamp, short: true },
126
+ ],
127
+ },
128
+ ],
129
+ };
130
+
131
+ if (alert.description) {
132
+ payload.attachments[0].text = alert.description;
133
+ }
134
+
135
+ return post(webhookUrl, payload);
136
+ }
137
+
138
+ /**
139
+ * Deduplicate alerts by fingerprint
140
+ * @param {Array} alerts - Array of alerts
141
+ * @returns {Array} Deduplicated alerts
142
+ */
143
+ export function deduplicateAlerts(alerts) {
144
+ const seen = new Map();
145
+
146
+ for (const alert of alerts) {
147
+ const key = alert.fingerprint;
148
+ if (!seen.has(key)) {
149
+ seen.set(key, alert);
150
+ }
151
+ }
152
+
153
+ return Array.from(seen.values());
154
+ }
155
+
156
+ /**
157
+ * Acknowledge an alert
158
+ * @param {Object} alert - Alert object
159
+ * @param {Object} options - Acknowledgment options
160
+ * @param {string} options.user - User acknowledging
161
+ * @param {string} options.note - Acknowledgment note
162
+ * @returns {Object} Acknowledged alert
163
+ */
164
+ export function acknowledgeAlert(alert, options = {}) {
165
+ const { user, note } = options;
166
+
167
+ return {
168
+ ...alert,
169
+ acknowledged: true,
170
+ acknowledgedBy: user,
171
+ acknowledgedAt: new Date().toISOString(),
172
+ acknowledgeNote: note,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Create an alert manager
178
+ * @param {Object} options - Configuration options
179
+ * @param {Object} options.escalation - Escalation rules
180
+ * @param {Object} options.routing - Routing rules
181
+ * @returns {Object} Alert manager
182
+ */
183
+ export function createAlertManager(options = {}) {
184
+ const { escalation, routing = {} } = options;
185
+
186
+ const alerts = new Map();
187
+ const config = {
188
+ escalation,
189
+ routing,
190
+ };
191
+
192
+ return {
193
+ async send(alertOptions) {
194
+ const alert = createAlert(alertOptions);
195
+ alerts.set(alert.id, alert);
196
+
197
+ const routes = routeAlert(alert, config.routing);
198
+ return { alert, routes };
199
+ },
200
+
201
+ acknowledge(alertId, ackOptions) {
202
+ const alert = alerts.get(alertId);
203
+ if (!alert) {
204
+ throw new Error(`Alert ${alertId} not found`);
205
+ }
206
+
207
+ const acked = acknowledgeAlert(alert, ackOptions);
208
+ alerts.set(alertId, acked);
209
+ return acked;
210
+ },
211
+
212
+ list(filter = {}) {
213
+ let result = Array.from(alerts.values());
214
+
215
+ if (filter.severity) {
216
+ result = result.filter((a) => a.severity === filter.severity);
217
+ }
218
+
219
+ if (filter.acknowledged !== undefined) {
220
+ result = result.filter((a) => a.acknowledged === filter.acknowledged);
221
+ }
222
+
223
+ return result;
224
+ },
225
+
226
+ get(alertId) {
227
+ return alerts.get(alertId);
228
+ },
229
+
230
+ configure(newConfig) {
231
+ Object.assign(config, newConfig);
232
+ },
233
+
234
+ getConfig() {
235
+ return { ...config };
236
+ },
237
+ };
238
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Alert Manager Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import {
6
+ createAlert,
7
+ routeAlert,
8
+ sendToPagerDuty,
9
+ sendToSlack,
10
+ deduplicateAlerts,
11
+ acknowledgeAlert,
12
+ ALERT_SEVERITY,
13
+ createAlertManager,
14
+ } from './alert-manager.js';
15
+
16
+ describe('alert-manager', () => {
17
+ describe('ALERT_SEVERITY', () => {
18
+ it('defines severity constants', () => {
19
+ expect(ALERT_SEVERITY.CRITICAL).toBe('critical');
20
+ expect(ALERT_SEVERITY.WARNING).toBe('warning');
21
+ expect(ALERT_SEVERITY.INFO).toBe('info');
22
+ });
23
+ });
24
+
25
+ describe('createAlert', () => {
26
+ it('creates alert with required fields', () => {
27
+ const alert = createAlert({ title: 'Test Alert', severity: 'critical' });
28
+ expect(alert.id).toBeDefined();
29
+ expect(alert.title).toBe('Test Alert');
30
+ expect(alert.severity).toBe('critical');
31
+ expect(alert.timestamp).toBeDefined();
32
+ });
33
+ });
34
+
35
+ describe('routeAlert', () => {
36
+ it('routes critical alerts to PagerDuty', () => {
37
+ const routes = routeAlert({ severity: 'critical' }, {
38
+ critical: ['pagerduty'],
39
+ warning: ['slack'],
40
+ });
41
+ expect(routes).toContain('pagerduty');
42
+ });
43
+
44
+ it('routes warning alerts to Slack', () => {
45
+ const routes = routeAlert({ severity: 'warning' }, {
46
+ critical: ['pagerduty'],
47
+ warning: ['slack'],
48
+ });
49
+ expect(routes).toContain('slack');
50
+ });
51
+ });
52
+
53
+ describe('sendToPagerDuty', () => {
54
+ it('sends alert to PagerDuty', async () => {
55
+ const mockPost = vi.fn().mockResolvedValue({ ok: true });
56
+ await sendToPagerDuty({ title: 'Test' }, { post: mockPost, routingKey: 'key' });
57
+ expect(mockPost).toHaveBeenCalled();
58
+ });
59
+ });
60
+
61
+ describe('sendToSlack', () => {
62
+ it('sends alert to Slack', async () => {
63
+ const mockPost = vi.fn().mockResolvedValue({ ok: true });
64
+ await sendToSlack({ title: 'Test' }, { post: mockPost, webhookUrl: 'url' });
65
+ expect(mockPost).toHaveBeenCalled();
66
+ });
67
+ });
68
+
69
+ describe('deduplicateAlerts', () => {
70
+ it('removes duplicate alerts', () => {
71
+ const alerts = [
72
+ { id: '1', fingerprint: 'abc', title: 'Alert 1' },
73
+ { id: '2', fingerprint: 'abc', title: 'Alert 1' },
74
+ { id: '3', fingerprint: 'def', title: 'Alert 2' },
75
+ ];
76
+ const deduped = deduplicateAlerts(alerts);
77
+ expect(deduped.length).toBe(2);
78
+ });
79
+ });
80
+
81
+ describe('acknowledgeAlert', () => {
82
+ it('marks alert as acknowledged', () => {
83
+ const alert = createAlert({ title: 'Test' });
84
+ const acked = acknowledgeAlert(alert, { user: 'admin' });
85
+ expect(acked.acknowledged).toBe(true);
86
+ expect(acked.acknowledgedBy).toBe('admin');
87
+ });
88
+ });
89
+
90
+ describe('createAlertManager', () => {
91
+ it('creates manager with methods', () => {
92
+ const manager = createAlertManager();
93
+ expect(manager.send).toBeDefined();
94
+ expect(manager.acknowledge).toBeDefined();
95
+ expect(manager.list).toBeDefined();
96
+ expect(manager.configure).toBeDefined();
97
+ });
98
+
99
+ it('configures escalation rules', () => {
100
+ const manager = createAlertManager({
101
+ escalation: { afterMinutes: 5, to: 'pagerduty' },
102
+ });
103
+ expect(manager.getConfig().escalation).toBeDefined();
104
+ });
105
+ });
106
+ });