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,234 @@
1
+ /**
2
+ * Notes API Module
3
+ * PROJECT.md and BUGS.md management
4
+ */
5
+ import { promises as defaultFs } from 'fs';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * Get notes (PROJECT.md) content
10
+ * @param {Object} options - Options
11
+ * @returns {Promise<Object>} Notes data
12
+ */
13
+ export async function getNotes(options = {}) {
14
+ const fs = options.fs || defaultFs;
15
+ const basePath = options.basePath || process.cwd();
16
+
17
+ const filePath = path.join(basePath, 'PROJECT.md');
18
+
19
+ let content = '';
20
+ let exists = true;
21
+ let lastModified = null;
22
+
23
+ try {
24
+ content = await fs.readFile(filePath, 'utf-8');
25
+ } catch {
26
+ content = '';
27
+ exists = false;
28
+ }
29
+
30
+ try {
31
+ if (fs.stat) {
32
+ const stats = await fs.stat(filePath);
33
+ lastModified = stats.mtime;
34
+ }
35
+ } catch {
36
+ // Ignore stat errors
37
+ }
38
+
39
+ return {
40
+ content,
41
+ type: 'project',
42
+ exists,
43
+ lastModified
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Update notes (PROJECT.md)
49
+ * @param {string} content - New content
50
+ * @param {Object} options - Options
51
+ * @returns {Promise<void>}
52
+ */
53
+ export async function updateNotes(content, options = {}) {
54
+ const fs = options.fs || defaultFs;
55
+ const basePath = options.basePath || process.cwd();
56
+ const backup = options.backup || false;
57
+ const validate = options.validate || false;
58
+
59
+ if (validate && (!content || !content.trim())) {
60
+ throw new Error('Content cannot be empty');
61
+ }
62
+
63
+ const filePath = path.join(basePath, 'PROJECT.md');
64
+
65
+ // Create backup if requested
66
+ if (backup) {
67
+ try {
68
+ const oldContent = await fs.readFile(filePath, 'utf-8');
69
+ const backupPath = path.join(basePath, `PROJECT.md.backup.${Date.now()}`);
70
+ await fs.writeFile(backupPath, oldContent);
71
+ } catch {
72
+ // Ignore backup errors for missing files
73
+ }
74
+ }
75
+
76
+ await fs.writeFile(filePath, content);
77
+ }
78
+
79
+ /**
80
+ * Get bugs (BUGS.md) content
81
+ * @param {Object} options - Options
82
+ * @returns {Promise<Object>} Bugs data
83
+ */
84
+ export async function getBugs(options = {}) {
85
+ const fs = options.fs || defaultFs;
86
+ const basePath = options.basePath || process.cwd();
87
+ const parse = options.parse || false;
88
+
89
+ const filePath = path.join(basePath, '.planning', 'BUGS.md');
90
+
91
+ let content = '';
92
+ let exists = true;
93
+
94
+ try {
95
+ content = await fs.readFile(filePath, 'utf-8');
96
+ } catch {
97
+ content = '';
98
+ exists = false;
99
+ }
100
+
101
+ const result = {
102
+ content,
103
+ exists
104
+ };
105
+
106
+ if (parse) {
107
+ result.entries = parseBugEntries(content);
108
+ }
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Parse bug entries from BUGS.md content
115
+ * @param {string} content - BUGS.md content
116
+ * @returns {Array} Bug entries
117
+ */
118
+ function parseBugEntries(content) {
119
+ const entries = [];
120
+ const lines = content.split('\n');
121
+
122
+ let currentBug = null;
123
+
124
+ for (const line of lines) {
125
+ // Match bug header: ## Bug: Title
126
+ const bugMatch = line.match(/^##\s+Bug:\s*(.+)$/);
127
+ if (bugMatch) {
128
+ if (currentBug) {
129
+ entries.push(currentBug);
130
+ }
131
+ currentBug = {
132
+ title: bugMatch[1].trim(),
133
+ severity: null,
134
+ status: null
135
+ };
136
+ continue;
137
+ }
138
+
139
+ if (currentBug) {
140
+ // Match severity: **Severity:** Value
141
+ const severityMatch = line.match(/^\*\*Severity:\*\*\s*(.+)$/);
142
+ if (severityMatch) {
143
+ currentBug.severity = severityMatch[1].trim();
144
+ }
145
+
146
+ // Match status: **Status:** Value
147
+ const statusMatch = line.match(/^\*\*Status:\*\*\s*(.+)$/);
148
+ if (statusMatch) {
149
+ currentBug.status = statusMatch[1].trim();
150
+ }
151
+ }
152
+ }
153
+
154
+ if (currentBug) {
155
+ entries.push(currentBug);
156
+ }
157
+
158
+ return entries;
159
+ }
160
+
161
+ /**
162
+ * Add a bug to BUGS.md
163
+ * @param {Object} bugData - Bug data
164
+ * @param {Object} options - Options
165
+ * @returns {Promise<Object>} Created bug
166
+ */
167
+ export async function addBug(bugData, options = {}) {
168
+ if (!bugData.title) {
169
+ throw new Error('Title is required');
170
+ }
171
+
172
+ const fs = options.fs || defaultFs;
173
+ const basePath = options.basePath || process.cwd();
174
+
175
+ const filePath = path.join(basePath, '.planning', 'BUGS.md');
176
+
177
+ let content = '';
178
+ try {
179
+ content = await fs.readFile(filePath, 'utf-8');
180
+ } catch {
181
+ content = '# Bugs\n\n';
182
+ }
183
+
184
+ // Create bug entry
185
+ const timestamp = new Date().toISOString().split('T')[0];
186
+ const severity = bugData.severity || 'Medium';
187
+ const description = bugData.description || '';
188
+
189
+ const bugEntry = `
190
+ ## Bug: ${bugData.title}
191
+ **Severity:** ${severity}
192
+ **Status:** Open
193
+ **Date:** ${timestamp}
194
+
195
+ ${description}
196
+ `;
197
+
198
+ const newContent = content + bugEntry;
199
+ await fs.writeFile(filePath, newContent);
200
+
201
+ return {
202
+ title: bugData.title,
203
+ severity,
204
+ status: 'Open',
205
+ date: timestamp
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Create Notes API handlers
211
+ * @param {Object} options - Options
212
+ * @returns {Object} API handlers
213
+ */
214
+ export function createNotesApi(options = {}) {
215
+ const { basePath = process.cwd(), fs: fileSystem = defaultFs } = options;
216
+
217
+ return {
218
+ async getNotes(opts = {}) {
219
+ return getNotes({ fs: fileSystem, basePath, ...opts });
220
+ },
221
+
222
+ async updateNotes(content, opts = {}) {
223
+ return updateNotes(content, { fs: fileSystem, basePath, ...opts });
224
+ },
225
+
226
+ async getBugs(opts = {}) {
227
+ return getBugs({ fs: fileSystem, basePath, ...opts });
228
+ },
229
+
230
+ async addBug(bugData, opts = {}) {
231
+ return addBug(bugData, { fs: fileSystem, basePath, ...opts });
232
+ }
233
+ };
234
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Notes API Module Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { getNotes, updateNotes, getBugs, addBug, createNotesApi } from './notes-api.js';
6
+
7
+ describe('notes-api', () => {
8
+ describe('getNotes', () => {
9
+ it('returns PROJECT.md content', async () => {
10
+ const mockFs = {
11
+ readFile: vi.fn().mockResolvedValue('# Project\n\nDescription')
12
+ };
13
+ const notes = await getNotes({ fs: mockFs, basePath: '/test' });
14
+ expect(notes.content).toContain('Project');
15
+ expect(notes.type).toBe('project');
16
+ });
17
+
18
+ it('includes last modified time', async () => {
19
+ const mockFs = {
20
+ readFile: vi.fn().mockResolvedValue('content'),
21
+ stat: vi.fn().mockResolvedValue({ mtime: new Date() })
22
+ };
23
+ const notes = await getNotes({ fs: mockFs, basePath: '/test' });
24
+ expect(notes.lastModified).toBeDefined();
25
+ });
26
+
27
+ it('handles missing file', async () => {
28
+ const mockFs = {
29
+ readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
30
+ };
31
+ const notes = await getNotes({ fs: mockFs, basePath: '/test' });
32
+ expect(notes.content).toBe('');
33
+ expect(notes.exists).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe('updateNotes', () => {
38
+ it('writes PROJECT.md content', async () => {
39
+ const mockFs = {
40
+ writeFile: vi.fn().mockResolvedValue(undefined)
41
+ };
42
+ await updateNotes('# Updated', { fs: mockFs, basePath: '/test' });
43
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
44
+ expect.stringContaining('PROJECT.md'),
45
+ '# Updated'
46
+ );
47
+ });
48
+
49
+ it('creates backup before update', async () => {
50
+ const mockFs = {
51
+ readFile: vi.fn().mockResolvedValue('old content'),
52
+ writeFile: vi.fn().mockResolvedValue(undefined)
53
+ };
54
+ await updateNotes('new content', { fs: mockFs, basePath: '/test', backup: true });
55
+ expect(mockFs.writeFile).toHaveBeenCalledTimes(2); // backup + main
56
+ });
57
+
58
+ it('validates markdown format', async () => {
59
+ const mockFs = { writeFile: vi.fn() };
60
+ await expect(updateNotes('', { fs: mockFs, validate: true }))
61
+ .rejects.toThrow(/empty/i);
62
+ });
63
+ });
64
+
65
+ describe('getBugs', () => {
66
+ it('returns BUGS.md content', async () => {
67
+ const mockFs = {
68
+ readFile: vi.fn().mockResolvedValue('## Bug 1\nDescription')
69
+ };
70
+ const bugs = await getBugs({ fs: mockFs, basePath: '/test' });
71
+ expect(bugs.content).toContain('Bug 1');
72
+ });
73
+
74
+ it('parses bug entries', async () => {
75
+ const mockFs = {
76
+ readFile: vi.fn().mockResolvedValue(`
77
+ ## Bug: Login fails
78
+ **Severity:** High
79
+ **Status:** Open
80
+
81
+ ## Bug: Slow load
82
+ **Severity:** Low
83
+ **Status:** Fixed
84
+ `)
85
+ };
86
+ const bugs = await getBugs({ fs: mockFs, basePath: '/test', parse: true });
87
+ expect(bugs.entries.length).toBe(2);
88
+ expect(bugs.entries[0].title).toBe('Login fails');
89
+ expect(bugs.entries[0].severity).toBe('High');
90
+ });
91
+ });
92
+
93
+ describe('addBug', () => {
94
+ it('appends bug to BUGS.md', async () => {
95
+ const mockFs = {
96
+ readFile: vi.fn().mockResolvedValue('# Bugs\n'),
97
+ writeFile: vi.fn().mockResolvedValue(undefined)
98
+ };
99
+ await addBug({
100
+ title: 'New bug',
101
+ description: 'Bug description',
102
+ severity: 'High'
103
+ }, { fs: mockFs, basePath: '/test' });
104
+ const content = mockFs.writeFile.mock.calls[0][1];
105
+ expect(content).toContain('New bug');
106
+ expect(content).toContain('High');
107
+ });
108
+
109
+ it('validates required fields', async () => {
110
+ await expect(addBug({ description: 'No title' }, {}))
111
+ .rejects.toThrow(/title.*required/i);
112
+ });
113
+
114
+ it('adds timestamp', async () => {
115
+ const mockFs = {
116
+ readFile: vi.fn().mockResolvedValue(''),
117
+ writeFile: vi.fn().mockResolvedValue(undefined)
118
+ };
119
+ await addBug({ title: 'Bug', severity: 'Low' }, { fs: mockFs, basePath: '/test' });
120
+ const content = mockFs.writeFile.mock.calls[0][1];
121
+ expect(content).toMatch(/\d{4}-\d{2}-\d{2}/); // date format
122
+ });
123
+ });
124
+
125
+ describe('createNotesApi', () => {
126
+ it('creates API handlers', () => {
127
+ const api = createNotesApi({ basePath: '/test' });
128
+ expect(api.getNotes).toBeDefined();
129
+ expect(api.updateNotes).toBeDefined();
130
+ expect(api.getBugs).toBeDefined();
131
+ expect(api.addBug).toBeDefined();
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Router Status API
3
+ * Multi-LLM router status API
4
+ */
5
+
6
+ /**
7
+ * Get router status
8
+ * @param {Object} options - Options
9
+ * @returns {Promise<Object>} Router status
10
+ */
11
+ export async function getRouterStatus(options = {}) {
12
+ const { router } = options;
13
+
14
+ if (!router) {
15
+ return {
16
+ providers: [],
17
+ overall: 'unknown'
18
+ };
19
+ }
20
+
21
+ const providers = router.getProviders();
22
+
23
+ // Determine overall status
24
+ const hasError = providers.some(p => p.status === 'error');
25
+ const allActive = providers.every(p => p.status === 'active');
26
+
27
+ let overall;
28
+ if (providers.length === 0) {
29
+ overall = 'unknown';
30
+ } else if (allActive) {
31
+ overall = 'healthy';
32
+ } else if (hasError) {
33
+ overall = 'degraded';
34
+ } else {
35
+ overall = 'healthy';
36
+ }
37
+
38
+ return {
39
+ providers,
40
+ overall,
41
+ timestamp: new Date().toISOString()
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get provider statistics
47
+ * @param {Object} data - Data containing requests
48
+ * @returns {Object} Provider stats
49
+ */
50
+ export function getProviderStats(data = {}) {
51
+ const { requests = [] } = data;
52
+ const stats = {};
53
+
54
+ // Group requests by provider
55
+ const byProvider = {};
56
+ for (const req of requests) {
57
+ if (!byProvider[req.provider]) {
58
+ byProvider[req.provider] = [];
59
+ }
60
+ byProvider[req.provider].push(req);
61
+ }
62
+
63
+ // Calculate stats for each provider
64
+ for (const [provider, providerReqs] of Object.entries(byProvider)) {
65
+ const requestCount = providerReqs.length;
66
+
67
+ // Calculate error rate
68
+ const errors = providerReqs.filter(r => r.error === true).length;
69
+ const errorRate = requestCount > 0 ? errors / requestCount : 0;
70
+
71
+ // Calculate average latency
72
+ const latencies = providerReqs.filter(r => r.latency !== undefined).map(r => r.latency);
73
+ const avgLatency = latencies.length > 0
74
+ ? latencies.reduce((sum, l) => sum + l, 0) / latencies.length
75
+ : 0;
76
+
77
+ stats[provider] = {
78
+ requests: requestCount,
79
+ errorRate,
80
+ avgLatency
81
+ };
82
+ }
83
+
84
+ return stats;
85
+ }
86
+
87
+ /**
88
+ * Calculate costs from requests
89
+ * @param {Array} requests - Request array
90
+ * @param {Object} pricing - Pricing per provider
91
+ * @returns {Object} Cost breakdown
92
+ */
93
+ export function calculateCosts(requests, pricing = {}) {
94
+ const byProvider = {};
95
+ let total = 0;
96
+
97
+ for (const req of requests) {
98
+ const provider = req.provider;
99
+ const providerPricing = pricing[provider];
100
+
101
+ if (!byProvider[provider]) {
102
+ byProvider[provider] = 0;
103
+ }
104
+
105
+ if (providerPricing) {
106
+ const inputCost = (req.inputTokens || 0) * (providerPricing.input || 0) / 1000;
107
+ const outputCost = (req.outputTokens || 0) * (providerPricing.output || 0) / 1000;
108
+ const cost = inputCost + outputCost;
109
+ byProvider[provider] += cost;
110
+ total += cost;
111
+ } else {
112
+ // No pricing available
113
+ byProvider[provider] = 0;
114
+ }
115
+ }
116
+
117
+ return {
118
+ total,
119
+ byProvider
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Filter requests by time range
125
+ * @param {Array} requests - Requests to filter
126
+ * @param {Object} range - Time range with start and optional end
127
+ * @returns {Array} Filtered requests
128
+ */
129
+ export function filterByTimeRange(requests, range = {}) {
130
+ const { start, end } = range;
131
+
132
+ return requests.filter(req => {
133
+ const timestamp = req.timestamp;
134
+
135
+ if (start !== undefined && timestamp < start) {
136
+ return false;
137
+ }
138
+
139
+ if (end !== undefined && timestamp > end) {
140
+ return false;
141
+ }
142
+
143
+ return true;
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Create Router API handler
149
+ * @param {Object} options - Options
150
+ * @returns {Object} API handlers
151
+ */
152
+ export function createRouterApi(options = {}) {
153
+ const { router, requestStore } = options;
154
+
155
+ return {
156
+ async getStatus() {
157
+ return getRouterStatus({ router });
158
+ },
159
+
160
+ getStats(timeRange) {
161
+ let requests = requestStore?.getRequests() || [];
162
+ if (timeRange) {
163
+ requests = filterByTimeRange(requests, timeRange);
164
+ }
165
+ return getProviderStats({ requests });
166
+ },
167
+
168
+ getCosts(pricing, timeRange) {
169
+ let requests = requestStore?.getRequests() || [];
170
+ if (timeRange) {
171
+ requests = filterByTimeRange(requests, timeRange);
172
+ }
173
+ return calculateCosts(requests, pricing);
174
+ }
175
+ };
176
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Router Status API Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { getRouterStatus, getProviderStats, calculateCosts, filterByTimeRange, createRouterApi } from './router-api.js';
6
+
7
+ describe('router-api', () => {
8
+ describe('getRouterStatus', () => {
9
+ it('returns all provider statuses', async () => {
10
+ const mockRouter = {
11
+ getProviders: vi.fn().mockReturnValue([
12
+ { name: 'openai', status: 'active' },
13
+ { name: 'anthropic', status: 'active' }
14
+ ])
15
+ };
16
+ const status = await getRouterStatus({ router: mockRouter });
17
+ expect(status.providers.length).toBe(2);
18
+ expect(status.providers[0].name).toBe('openai');
19
+ });
20
+
21
+ it('includes overall status', async () => {
22
+ const mockRouter = {
23
+ getProviders: vi.fn().mockReturnValue([
24
+ { name: 'openai', status: 'active' }
25
+ ])
26
+ };
27
+ const status = await getRouterStatus({ router: mockRouter });
28
+ expect(status.overall).toBe('healthy');
29
+ });
30
+
31
+ it('reports degraded when providers down', async () => {
32
+ const mockRouter = {
33
+ getProviders: vi.fn().mockReturnValue([
34
+ { name: 'openai', status: 'error' },
35
+ { name: 'anthropic', status: 'active' }
36
+ ])
37
+ };
38
+ const status = await getRouterStatus({ router: mockRouter });
39
+ expect(status.overall).toBe('degraded');
40
+ });
41
+ });
42
+
43
+ describe('getProviderStats', () => {
44
+ it('returns request counts', () => {
45
+ const stats = getProviderStats({
46
+ requests: [
47
+ { provider: 'openai', timestamp: Date.now() },
48
+ { provider: 'openai', timestamp: Date.now() },
49
+ { provider: 'anthropic', timestamp: Date.now() }
50
+ ]
51
+ });
52
+ expect(stats.openai.requests).toBe(2);
53
+ expect(stats.anthropic.requests).toBe(1);
54
+ });
55
+
56
+ it('calculates error rates', () => {
57
+ const stats = getProviderStats({
58
+ requests: [
59
+ { provider: 'openai', error: false },
60
+ { provider: 'openai', error: true }
61
+ ]
62
+ });
63
+ expect(stats.openai.errorRate).toBe(0.5);
64
+ });
65
+
66
+ it('tracks latency', () => {
67
+ const stats = getProviderStats({
68
+ requests: [
69
+ { provider: 'openai', latency: 100 },
70
+ { provider: 'openai', latency: 200 }
71
+ ]
72
+ });
73
+ expect(stats.openai.avgLatency).toBe(150);
74
+ });
75
+ });
76
+
77
+ describe('calculateCosts', () => {
78
+ it('calculates total costs', () => {
79
+ const requests = [
80
+ { provider: 'openai', inputTokens: 1000, outputTokens: 500 },
81
+ { provider: 'anthropic', inputTokens: 2000, outputTokens: 1000 }
82
+ ];
83
+ const costs = calculateCosts(requests, {
84
+ openai: { input: 0.01, output: 0.03 },
85
+ anthropic: { input: 0.008, output: 0.024 }
86
+ });
87
+ expect(costs.total).toBeGreaterThan(0);
88
+ expect(costs.byProvider.openai).toBeDefined();
89
+ });
90
+
91
+ it('handles missing pricing', () => {
92
+ const requests = [{ provider: 'unknown', inputTokens: 1000 }];
93
+ const costs = calculateCosts(requests, {});
94
+ expect(costs.byProvider.unknown).toBe(0);
95
+ });
96
+ });
97
+
98
+ describe('filterByTimeRange', () => {
99
+ it('filters by date range', () => {
100
+ const now = Date.now();
101
+ const requests = [
102
+ { timestamp: now - 1000 },
103
+ { timestamp: now - 100000 },
104
+ { timestamp: now - 1000000 }
105
+ ];
106
+ const filtered = filterByTimeRange(requests, { start: now - 50000 });
107
+ expect(filtered.length).toBe(1);
108
+ });
109
+
110
+ it('supports end date', () => {
111
+ const now = Date.now();
112
+ const requests = [
113
+ { timestamp: now - 1000 },
114
+ { timestamp: now - 5000 }
115
+ ];
116
+ const filtered = filterByTimeRange(requests, {
117
+ start: now - 10000,
118
+ end: now - 3000
119
+ });
120
+ expect(filtered.length).toBe(1);
121
+ });
122
+ });
123
+
124
+ describe('createRouterApi', () => {
125
+ it('creates API handler', () => {
126
+ const api = createRouterApi({});
127
+ expect(api.getStatus).toBeDefined();
128
+ expect(api.getStats).toBeDefined();
129
+ expect(api.getCosts).toBeDefined();
130
+ });
131
+ });
132
+ });