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,307 @@
1
+ /**
2
+ * Deployment Audit Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import {
6
+ logDeploymentEvent,
7
+ queryAuditLog,
8
+ exportAuditLog,
9
+ AUDIT_EVENTS,
10
+ createDeploymentAudit,
11
+ } from './deployment-audit.js';
12
+
13
+ describe('deployment-audit', () => {
14
+ describe('AUDIT_EVENTS', () => {
15
+ it('defines all event types', () => {
16
+ expect(AUDIT_EVENTS.DEPLOYMENT_STARTED).toBe('deployment_started');
17
+ expect(AUDIT_EVENTS.DEPLOYMENT_COMPLETED).toBe('deployment_completed');
18
+ expect(AUDIT_EVENTS.DEPLOYMENT_FAILED).toBe('deployment_failed');
19
+ expect(AUDIT_EVENTS.APPROVAL_REQUESTED).toBe('approval_requested');
20
+ expect(AUDIT_EVENTS.APPROVAL_GRANTED).toBe('approval_granted');
21
+ expect(AUDIT_EVENTS.APPROVAL_DENIED).toBe('approval_denied');
22
+ expect(AUDIT_EVENTS.ROLLBACK_TRIGGERED).toBe('rollback_triggered');
23
+ expect(AUDIT_EVENTS.ROLLBACK_COMPLETED).toBe('rollback_completed');
24
+ expect(AUDIT_EVENTS.SECURITY_GATE_PASSED).toBe('security_gate_passed');
25
+ expect(AUDIT_EVENTS.SECURITY_GATE_FAILED).toBe('security_gate_failed');
26
+ });
27
+ });
28
+
29
+ describe('logDeploymentEvent', () => {
30
+ it('logs event with required fields', async () => {
31
+ const mockWrite = vi.fn().mockResolvedValue(true);
32
+
33
+ const entry = await logDeploymentEvent({
34
+ event: 'deployment_started',
35
+ deploymentId: 'deploy-123',
36
+ branch: 'main',
37
+ user: 'alice',
38
+ writeFn: mockWrite,
39
+ });
40
+
41
+ expect(entry.id).toBeDefined();
42
+ expect(entry.event).toBe('deployment_started');
43
+ expect(entry.deploymentId).toBe('deploy-123');
44
+ expect(entry.branch).toBe('main');
45
+ expect(entry.user).toBe('alice');
46
+ expect(entry.timestamp).toBeDefined();
47
+ });
48
+
49
+ it('includes metadata when provided', async () => {
50
+ const mockWrite = vi.fn().mockResolvedValue(true);
51
+
52
+ const entry = await logDeploymentEvent({
53
+ event: 'deployment_completed',
54
+ deploymentId: 'deploy-123',
55
+ branch: 'main',
56
+ user: 'alice',
57
+ metadata: {
58
+ duration: 120000,
59
+ commitSha: 'abc123',
60
+ },
61
+ writeFn: mockWrite,
62
+ });
63
+
64
+ expect(entry.metadata.duration).toBe(120000);
65
+ expect(entry.metadata.commitSha).toBe('abc123');
66
+ });
67
+
68
+ it('generates checksum for integrity', async () => {
69
+ const mockWrite = vi.fn().mockResolvedValue(true);
70
+
71
+ const entry = await logDeploymentEvent({
72
+ event: 'deployment_started',
73
+ deploymentId: 'deploy-123',
74
+ branch: 'main',
75
+ user: 'alice',
76
+ writeFn: mockWrite,
77
+ });
78
+
79
+ expect(entry.checksum).toBeDefined();
80
+ expect(entry.checksum).toMatch(/^[a-f0-9]{64}$/); // SHA-256
81
+ });
82
+
83
+ it('links to previous entry', async () => {
84
+ const mockWrite = vi.fn().mockResolvedValue(true);
85
+ const previousChecksum = 'abc123def456';
86
+
87
+ const entry = await logDeploymentEvent({
88
+ event: 'deployment_completed',
89
+ deploymentId: 'deploy-123',
90
+ branch: 'main',
91
+ user: 'alice',
92
+ previousChecksum,
93
+ writeFn: mockWrite,
94
+ });
95
+
96
+ expect(entry.previousChecksum).toBe(previousChecksum);
97
+ });
98
+ });
99
+
100
+ describe('queryAuditLog', () => {
101
+ const mockEntries = [
102
+ { id: '1', event: 'deployment_started', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:00:00Z' },
103
+ { id: '2', event: 'deployment_completed', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:05:00Z' },
104
+ { id: '3', event: 'deployment_started', branch: 'dev', user: 'bob', timestamp: '2024-01-01T11:00:00Z' },
105
+ { id: '4', event: 'deployment_failed', branch: 'dev', user: 'bob', timestamp: '2024-01-01T11:02:00Z' },
106
+ ];
107
+
108
+ it('queries by date range', async () => {
109
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
110
+
111
+ const results = await queryAuditLog({
112
+ startDate: '2024-01-01T10:00:00Z',
113
+ endDate: '2024-01-01T10:30:00Z',
114
+ queryFn: mockQuery,
115
+ });
116
+
117
+ expect(results).toHaveLength(2);
118
+ });
119
+
120
+ it('queries by user', async () => {
121
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
122
+
123
+ const results = await queryAuditLog({
124
+ user: 'bob',
125
+ queryFn: mockQuery,
126
+ });
127
+
128
+ expect(results).toHaveLength(2);
129
+ expect(results.every(e => e.user === 'bob')).toBe(true);
130
+ });
131
+
132
+ it('queries by branch', async () => {
133
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
134
+
135
+ const results = await queryAuditLog({
136
+ branch: 'main',
137
+ queryFn: mockQuery,
138
+ });
139
+
140
+ expect(results).toHaveLength(2);
141
+ expect(results.every(e => e.branch === 'main')).toBe(true);
142
+ });
143
+
144
+ it('queries by event type', async () => {
145
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
146
+
147
+ const results = await queryAuditLog({
148
+ event: 'deployment_failed',
149
+ queryFn: mockQuery,
150
+ });
151
+
152
+ expect(results).toHaveLength(1);
153
+ expect(results[0].event).toBe('deployment_failed');
154
+ });
155
+
156
+ it('combines filters', async () => {
157
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
158
+
159
+ const results = await queryAuditLog({
160
+ branch: 'dev',
161
+ user: 'bob',
162
+ queryFn: mockQuery,
163
+ });
164
+
165
+ expect(results).toHaveLength(2);
166
+ });
167
+
168
+ it('paginates results', async () => {
169
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
170
+
171
+ const results = await queryAuditLog({
172
+ limit: 2,
173
+ offset: 1,
174
+ queryFn: mockQuery,
175
+ });
176
+
177
+ expect(results).toHaveLength(2);
178
+ });
179
+ });
180
+
181
+ describe('exportAuditLog', () => {
182
+ const mockEntries = [
183
+ { id: '1', event: 'deployment_started', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:00:00Z' },
184
+ { id: '2', event: 'deployment_completed', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:05:00Z' },
185
+ ];
186
+
187
+ it('exports as JSON', async () => {
188
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
189
+
190
+ const result = await exportAuditLog({
191
+ format: 'json',
192
+ queryFn: mockQuery,
193
+ });
194
+
195
+ const parsed = JSON.parse(result);
196
+ expect(parsed.entries).toHaveLength(2);
197
+ expect(parsed.exportedAt).toBeDefined();
198
+ });
199
+
200
+ it('exports as CSV', async () => {
201
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
202
+
203
+ const result = await exportAuditLog({
204
+ format: 'csv',
205
+ queryFn: mockQuery,
206
+ });
207
+
208
+ expect(result).toContain('id,event,branch,user,timestamp');
209
+ expect(result).toContain('deployment_started');
210
+ });
211
+
212
+ it('exports as SIEM format (CEF)', async () => {
213
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
214
+
215
+ const result = await exportAuditLog({
216
+ format: 'cef',
217
+ queryFn: mockQuery,
218
+ });
219
+
220
+ expect(result).toContain('CEF:0');
221
+ expect(result).toContain('deployment_started');
222
+ });
223
+
224
+ it('includes metadata in export', async () => {
225
+ const mockQuery = vi.fn().mockResolvedValue(mockEntries);
226
+
227
+ const result = await exportAuditLog({
228
+ format: 'json',
229
+ includeMetadata: true,
230
+ queryFn: mockQuery,
231
+ });
232
+
233
+ const parsed = JSON.parse(result);
234
+ expect(parsed.totalEntries).toBe(2);
235
+ });
236
+ });
237
+
238
+ describe('createDeploymentAudit', () => {
239
+ it('creates audit logger', () => {
240
+ const audit = createDeploymentAudit();
241
+ expect(audit.log).toBeDefined();
242
+ expect(audit.query).toBeDefined();
243
+ expect(audit.export).toBeDefined();
244
+ });
245
+
246
+ it('logs events to storage', async () => {
247
+ const mockStorage = {
248
+ write: vi.fn().mockResolvedValue(true),
249
+ read: vi.fn().mockResolvedValue([]),
250
+ };
251
+
252
+ const audit = createDeploymentAudit({ storage: mockStorage });
253
+
254
+ await audit.log({
255
+ event: 'deployment_started',
256
+ deploymentId: 'deploy-123',
257
+ branch: 'main',
258
+ user: 'alice',
259
+ });
260
+
261
+ expect(mockStorage.write).toHaveBeenCalled();
262
+ });
263
+
264
+ it('maintains checksum chain', async () => {
265
+ const audit = createDeploymentAudit();
266
+
267
+ const entry1 = await audit.log({
268
+ event: 'deployment_started',
269
+ deploymentId: 'deploy-123',
270
+ branch: 'main',
271
+ user: 'alice',
272
+ });
273
+
274
+ const entry2 = await audit.log({
275
+ event: 'deployment_completed',
276
+ deploymentId: 'deploy-123',
277
+ branch: 'main',
278
+ user: 'alice',
279
+ });
280
+
281
+ expect(entry2.previousChecksum).toBe(entry1.checksum);
282
+ });
283
+
284
+ it('verifies log integrity', async () => {
285
+ const audit = createDeploymentAudit();
286
+
287
+ await audit.log({ event: 'deployment_started', deploymentId: 'd1', branch: 'main', user: 'alice' });
288
+ await audit.log({ event: 'deployment_completed', deploymentId: 'd1', branch: 'main', user: 'alice' });
289
+
290
+ const integrity = await audit.verifyIntegrity();
291
+ expect(integrity.valid).toBe(true);
292
+ });
293
+
294
+ it('detects tampered entries', async () => {
295
+ const audit = createDeploymentAudit();
296
+
297
+ await audit.log({ event: 'deployment_started', deploymentId: 'd1', branch: 'main', user: 'alice' });
298
+
299
+ // Simulate tampering
300
+ audit._tamperEntry(0, { user: 'mallory' });
301
+
302
+ const integrity = await audit.verifyIntegrity();
303
+ expect(integrity.valid).toBe(false);
304
+ expect(integrity.tamperedEntries).toHaveLength(1);
305
+ });
306
+ });
307
+ });
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Deployment Executor
3
+ *
4
+ * Handles deployment orchestration with support for multiple strategies:
5
+ * - Rolling: Gradual replacement of instances
6
+ * - Blue-Green: Deploy to inactive slot, then switch traffic
7
+ * - Canary: Gradual traffic shift to new version
8
+ * - Recreate: Stop old version, start new version
9
+ */
10
+
11
+ import { randomUUID } from 'node:crypto';
12
+
13
+ /**
14
+ * Deployment state constants
15
+ */
16
+ export const DEPLOYMENT_STATES = {
17
+ PENDING: 'pending',
18
+ BUILDING: 'building',
19
+ DEPLOYING: 'deploying',
20
+ HEALTH_CHECK: 'health_check',
21
+ SWITCHING: 'switching',
22
+ COMPLETED: 'completed',
23
+ FAILED: 'failed',
24
+ ROLLED_BACK: 'rolled_back',
25
+ };
26
+
27
+ /**
28
+ * Deployment strategy constants
29
+ */
30
+ export const DEPLOYMENT_STRATEGIES = {
31
+ ROLLING: 'rolling',
32
+ BLUE_GREEN: 'blue-green',
33
+ CANARY: 'canary',
34
+ RECREATE: 'recreate',
35
+ };
36
+
37
+ /**
38
+ * Create a new deployment object
39
+ * @param {Object} options - Deployment options
40
+ * @param {string} options.branch - Git branch name
41
+ * @param {string} options.commitSha - Git commit SHA
42
+ * @param {string} [options.strategy='rolling'] - Deployment strategy
43
+ * @returns {Object} Deployment object
44
+ */
45
+ export function createDeployment(options) {
46
+ const { branch, commitSha, strategy = DEPLOYMENT_STRATEGIES.ROLLING } = options;
47
+
48
+ const now = new Date().toISOString();
49
+
50
+ return {
51
+ id: randomUUID(),
52
+ branch,
53
+ commitSha,
54
+ strategy,
55
+ state: DEPLOYMENT_STATES.PENDING,
56
+ createdAt: now,
57
+ stateHistory: [{ state: DEPLOYMENT_STATES.PENDING, timestamp: now }],
58
+ error: null,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Transition deployment to a new state
64
+ * @param {Object} deployment - Deployment object
65
+ * @param {string} newState - New state
66
+ * @param {Function} [onStateChange] - State change callback
67
+ * @returns {Object} Updated deployment
68
+ */
69
+ function transitionState(deployment, newState, onStateChange) {
70
+ deployment.state = newState;
71
+ deployment.stateHistory.push({
72
+ state: newState,
73
+ timestamp: new Date().toISOString(),
74
+ });
75
+
76
+ if (onStateChange) {
77
+ // Pass a snapshot to preserve state at time of callback
78
+ onStateChange({ ...deployment, stateHistory: [...deployment.stateHistory] });
79
+ }
80
+
81
+ return deployment;
82
+ }
83
+
84
+ /**
85
+ * Execute a deployment
86
+ * @param {Object} deployment - Deployment object
87
+ * @param {Object} options - Execution options
88
+ * @param {Function} options.build - Build function
89
+ * @param {Function} [options.deploy] - Deploy function
90
+ * @param {Function} [options.healthCheck] - Health check function
91
+ * @param {Function} [options.switchTraffic] - Traffic switch function (for blue-green)
92
+ * @param {Function} [options.onStateChange] - State change callback
93
+ * @returns {Promise<Object>} Updated deployment
94
+ */
95
+ export async function executeDeployment(deployment, options) {
96
+ const { build, deploy, healthCheck, switchTraffic: switchTrafficFn, onStateChange } = options;
97
+
98
+ try {
99
+ // Building phase
100
+ transitionState(deployment, DEPLOYMENT_STATES.BUILDING, onStateChange);
101
+ const buildResult = await build(deployment);
102
+
103
+ if (!buildResult.success) {
104
+ deployment.error = buildResult.error || 'Build failed';
105
+ return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
106
+ }
107
+
108
+ // Deploying phase
109
+ if (deploy) {
110
+ transitionState(deployment, DEPLOYMENT_STATES.DEPLOYING, onStateChange);
111
+ const deployResult = await deploy(deployment);
112
+
113
+ if (!deployResult.success) {
114
+ deployment.error = deployResult.error || 'Deploy failed';
115
+ return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
116
+ }
117
+
118
+ deployment.slot = deployResult.slot;
119
+ }
120
+
121
+ // Health check phase
122
+ if (healthCheck) {
123
+ transitionState(deployment, DEPLOYMENT_STATES.HEALTH_CHECK, onStateChange);
124
+ const healthResult = await healthCheck(deployment);
125
+
126
+ if (!healthResult.healthy) {
127
+ deployment.error = healthResult.reason || 'Health check failed';
128
+ return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
129
+ }
130
+ }
131
+
132
+ // Traffic switching phase (for blue-green deployments)
133
+ if (deployment.strategy === DEPLOYMENT_STRATEGIES.BLUE_GREEN && switchTrafficFn) {
134
+ transitionState(deployment, DEPLOYMENT_STATES.SWITCHING, onStateChange);
135
+ const switchResult = await switchTrafficFn(deployment);
136
+
137
+ if (!switchResult.success) {
138
+ deployment.error = switchResult.error || 'Traffic switch failed';
139
+ return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
140
+ }
141
+ }
142
+
143
+ // Completed
144
+ return transitionState(deployment, DEPLOYMENT_STATES.COMPLETED, onStateChange);
145
+ } catch (error) {
146
+ deployment.error = error.message;
147
+ return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Run health checks against multiple endpoints
153
+ * @param {Object} options - Health check options
154
+ * @param {string[]} options.endpoints - URLs to check
155
+ * @param {Function} options.fetch - Fetch function
156
+ * @param {number} [options.retries=1] - Number of retry attempts
157
+ * @param {number} [options.retryDelay=1000] - Delay between retries in ms
158
+ * @param {number} [options.timeout=30000] - Timeout in ms
159
+ * @returns {Promise<Object>} Health check result
160
+ */
161
+ export async function runHealthChecks(options) {
162
+ const { endpoints, fetch: fetchFn, retries = 1, retryDelay = 1000, timeout = 30000 } = options;
163
+
164
+ /**
165
+ * Check a single endpoint with timeout
166
+ */
167
+ async function checkEndpoint(url) {
168
+ return Promise.race([
169
+ fetchFn(url),
170
+ new Promise((_, reject) =>
171
+ setTimeout(() => reject(new Error('timeout')), timeout)
172
+ ),
173
+ ]);
174
+ }
175
+
176
+ /**
177
+ * Check endpoint with retries
178
+ */
179
+ async function checkWithRetries(url, attemptsLeft) {
180
+ try {
181
+ const response = await checkEndpoint(url);
182
+ if (response.ok) {
183
+ return { healthy: true, url };
184
+ }
185
+
186
+ if (attemptsLeft > 1) {
187
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
188
+ return checkWithRetries(url, attemptsLeft - 1);
189
+ }
190
+
191
+ return { healthy: false, url, reason: `Status ${response.status}` };
192
+ } catch (error) {
193
+ if (attemptsLeft > 1) {
194
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
195
+ return checkWithRetries(url, attemptsLeft - 1);
196
+ }
197
+
198
+ return { healthy: false, url, reason: error.message };
199
+ }
200
+ }
201
+
202
+ try {
203
+ const results = await Promise.all(
204
+ endpoints.map((endpoint) => checkWithRetries(endpoint, retries))
205
+ );
206
+
207
+ const unhealthy = results.filter((r) => !r.healthy);
208
+
209
+ if (unhealthy.length > 0) {
210
+ return {
211
+ healthy: false,
212
+ reason: unhealthy.map((u) => `${u.url}: ${u.reason}`).join(', '),
213
+ results,
214
+ };
215
+ }
216
+
217
+ return { healthy: true, results };
218
+ } catch (error) {
219
+ return { healthy: false, reason: error.message };
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Switch traffic between deployment slots
225
+ * @param {Object} options - Switch options
226
+ * @param {string} options.fromSlot - Source slot
227
+ * @param {string} options.toSlot - Target slot
228
+ * @param {Function} options.switchFn - Switch function
229
+ * @returns {Promise<Object>} Switch result
230
+ */
231
+ export async function switchTraffic(options) {
232
+ const { fromSlot, toSlot, switchFn } = options;
233
+
234
+ try {
235
+ const result = await switchFn(fromSlot, toSlot);
236
+ return { success: true, ...result };
237
+ } catch (error) {
238
+ return { success: false, error: error.message };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Cleanup old deployment resources
244
+ * @param {Object} options - Cleanup options
245
+ * @param {string} options.slot - Slot to cleanup
246
+ * @param {Function} options.cleanupFn - Cleanup function
247
+ * @returns {Promise<Object>} Cleanup result
248
+ */
249
+ export async function cleanupOldDeployment(options) {
250
+ const { slot, cleanupFn } = options;
251
+
252
+ try {
253
+ await cleanupFn(slot);
254
+ return { success: true };
255
+ } catch (error) {
256
+ // Log warning but don't throw - cleanup failures are not critical
257
+ console.warn(`Cleanup failed for slot ${slot}:`, error.message);
258
+ return { success: false, error: error.message };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Create a deployment executor instance
264
+ * @param {Object} [config={}] - Executor configuration
265
+ * @param {Function} [config.build] - Build function
266
+ * @param {Function} [config.deploy] - Deploy function
267
+ * @param {Function} [config.healthCheck] - Health check function
268
+ * @param {Function} [config.switchTraffic] - Traffic switch function
269
+ * @returns {Object} Deployment executor
270
+ */
271
+ export function createDeploymentExecutor(config = {}) {
272
+ const deployments = new Map();
273
+
274
+ return {
275
+ /**
276
+ * Execute an existing deployment
277
+ * @param {Object} deployment - Deployment to execute
278
+ * @param {Object} [options] - Additional options
279
+ * @returns {Promise<Object>} Execution result
280
+ */
281
+ async execute(deployment, options = {}) {
282
+ return executeDeployment(deployment, { ...config, ...options });
283
+ },
284
+
285
+ /**
286
+ * Start a new deployment
287
+ * @param {Object} options - Deployment options
288
+ * @returns {Promise<Object>} Created and executed deployment
289
+ */
290
+ async start(options) {
291
+ const deployment = createDeployment(options);
292
+ deployments.set(deployment.id, deployment);
293
+
294
+ // Execute in background, don't await
295
+ executeDeployment(deployment, config).catch((error) => {
296
+ deployment.error = error.message;
297
+ deployment.state = DEPLOYMENT_STATES.FAILED;
298
+ });
299
+
300
+ return deployment;
301
+ },
302
+
303
+ /**
304
+ * Get a deployment by ID
305
+ * @param {string} id - Deployment ID
306
+ * @returns {Object|undefined} Deployment or undefined
307
+ */
308
+ getDeployment(id) {
309
+ return deployments.get(id);
310
+ },
311
+
312
+ /**
313
+ * List all deployments
314
+ * @returns {Object[]} Array of deployments
315
+ */
316
+ list() {
317
+ return Array.from(deployments.values());
318
+ },
319
+
320
+ /**
321
+ * Cancel a deployment
322
+ * @param {string} id - Deployment ID
323
+ * @returns {boolean} True if cancelled
324
+ */
325
+ cancel(id) {
326
+ const deployment = deployments.get(id);
327
+ if (deployment && deployment.state !== DEPLOYMENT_STATES.COMPLETED) {
328
+ deployment.state = DEPLOYMENT_STATES.FAILED;
329
+ deployment.error = 'Cancelled';
330
+ return true;
331
+ }
332
+ return false;
333
+ },
334
+ };
335
+ }