lean-claudient-daemon 0.1.0
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.
- package/README.md +39 -0
- package/dist/api.js +54 -0
- package/dist/bin/daemon.js +14 -0
- package/dist/metrics.js +31 -0
- package/dist/metricsStore.js +84 -0
- package/dist/middleware/rateLimiting.js +193 -0
- package/dist/middleware/securityHeaders.js +239 -0
- package/dist/middleware/validation.js +181 -0
- package/dist/security/envValidation.js +266 -0
- package/dist/service.js +47 -0
- package/dist/validation/schemas.js +157 -0
- package/package.json +49 -0
- package/src/api.ts +58 -0
- package/src/bin/daemon.ts +18 -0
- package/src/metrics.ts +45 -0
- package/src/metricsStore.ts +114 -0
- package/src/middleware/rateLimiting.ts +244 -0
- package/src/middleware/securityHeaders.ts +305 -0
- package/src/middleware/validation.ts +222 -0
- package/src/security/envValidation.ts +321 -0
- package/src/service.ts +62 -0
- package/src/validation/schemas.ts +190 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Validation
|
|
3
|
+
* Validates daemon security configuration at startup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, accessSync, constants } from 'fs';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
// Mock detectSecrets function since core/security is not accessible from daemon
|
|
10
|
+
function detectSecrets(text: string): Array<{ severity: string }> {
|
|
11
|
+
// Placeholder for secrets detection
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ValidationResult {
|
|
16
|
+
passed: boolean;
|
|
17
|
+
checks: {
|
|
18
|
+
name: string;
|
|
19
|
+
status: 'PASS' | 'WARN' | 'FAIL';
|
|
20
|
+
message: string;
|
|
21
|
+
}[];
|
|
22
|
+
errors: string[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if process environment contains suspicious secrets
|
|
28
|
+
*/
|
|
29
|
+
export function checkEnvironmentSecrets(): { status: 'PASS' | 'WARN'; message: string } {
|
|
30
|
+
const envString = JSON.stringify(process.env);
|
|
31
|
+
const findings = detectSecrets(envString);
|
|
32
|
+
|
|
33
|
+
if (findings.length === 0) {
|
|
34
|
+
return { status: 'PASS', message: 'No hardcoded secrets in environment' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const critical = findings.filter((f) => f.severity === 'CRITICAL');
|
|
38
|
+
const high = findings.filter((f) => f.severity === 'HIGH');
|
|
39
|
+
|
|
40
|
+
if (critical.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
status: 'WARN',
|
|
43
|
+
message: `Found ${critical.length} CRITICAL and ${high.length} HIGH severity secrets in environment`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
status: 'PASS',
|
|
49
|
+
message: `Found ${high.length} potential high-entropy strings (may be expected secrets)`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate daemon port is not privileged
|
|
55
|
+
*/
|
|
56
|
+
export function checkDaemonPort(port: number): { status: 'PASS' | 'FAIL'; message: string } {
|
|
57
|
+
if (port < 1024) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'FAIL',
|
|
60
|
+
message: `Port ${port} is privileged (<1024). Use unprivileged port or run as root (not recommended).`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (port > 65535) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'FAIL',
|
|
67
|
+
message: `Port ${port} is invalid (>65535)`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
status: 'PASS',
|
|
73
|
+
message: `Daemon port ${port} is valid`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if log directory is writable
|
|
79
|
+
*/
|
|
80
|
+
export function checkLogDirectory(logDir: string): { status: 'PASS' | 'WARN'; message: string } {
|
|
81
|
+
// Create directory if it doesn't exist
|
|
82
|
+
if (!existsSync(logDir)) {
|
|
83
|
+
try {
|
|
84
|
+
const parentDir = dirname(logDir);
|
|
85
|
+
if (!existsSync(parentDir)) {
|
|
86
|
+
return {
|
|
87
|
+
status: 'WARN',
|
|
88
|
+
message: `Log directory parent doesn't exist: ${parentDir}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
accessSync(parentDir, constants.W_OK);
|
|
93
|
+
return {
|
|
94
|
+
status: 'PASS',
|
|
95
|
+
message: `Log directory can be created: ${logDir}`,
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return {
|
|
99
|
+
status: 'WARN',
|
|
100
|
+
message: `Cannot write to log directory parent: ${dirname(logDir)}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
accessSync(logDir, constants.W_OK);
|
|
107
|
+
return {
|
|
108
|
+
status: 'PASS',
|
|
109
|
+
message: `Log directory is writable: ${logDir}`,
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return {
|
|
113
|
+
status: 'WARN',
|
|
114
|
+
message: `Log directory is not writable: ${logDir}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if database path is writable
|
|
121
|
+
*/
|
|
122
|
+
export function checkDatabasePath(dbPath: string): { status: 'PASS' | 'WARN'; message: string } {
|
|
123
|
+
const dbDir = dirname(dbPath);
|
|
124
|
+
|
|
125
|
+
if (!existsSync(dbDir)) {
|
|
126
|
+
const parentDir = dirname(dbDir);
|
|
127
|
+
try {
|
|
128
|
+
accessSync(parentDir, constants.W_OK);
|
|
129
|
+
return {
|
|
130
|
+
status: 'PASS',
|
|
131
|
+
message: `Database directory can be created: ${dbDir}`,
|
|
132
|
+
};
|
|
133
|
+
} catch {
|
|
134
|
+
return {
|
|
135
|
+
status: 'WARN',
|
|
136
|
+
message: `Cannot write to database directory parent: ${parentDir}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
accessSync(dbDir, constants.W_OK);
|
|
143
|
+
return {
|
|
144
|
+
status: 'PASS',
|
|
145
|
+
message: `Database directory is writable: ${dbDir}`,
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return {
|
|
149
|
+
status: 'WARN',
|
|
150
|
+
message: `Database directory is not writable: ${dbDir}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check required environment variables
|
|
157
|
+
*/
|
|
158
|
+
export function checkRequiredEnvVars(required: string[]): { status: 'PASS' | 'FAIL'; message: string } {
|
|
159
|
+
const missing = required.filter((key) => !process.env[key]);
|
|
160
|
+
|
|
161
|
+
if (missing.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'PASS',
|
|
164
|
+
message: `All required environment variables are set`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
status: 'FAIL',
|
|
170
|
+
message: `Missing environment variables: ${missing.join(', ')}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check for deprecated or insecure environment variables
|
|
176
|
+
*/
|
|
177
|
+
export function checkDeprecatedEnvVars(): { status: 'PASS' | 'WARN'; message: string } {
|
|
178
|
+
const deprecated = [
|
|
179
|
+
'DEBUG',
|
|
180
|
+
'VERBOSE',
|
|
181
|
+
'LOG_LEVEL', // prefer structured logging
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const found = deprecated.filter((key) => process.env[key]);
|
|
185
|
+
|
|
186
|
+
if (found.length === 0) {
|
|
187
|
+
return {
|
|
188
|
+
status: 'PASS',
|
|
189
|
+
message: 'No deprecated environment variables detected',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
status: 'WARN',
|
|
195
|
+
message: `Found deprecated environment variables: ${found.join(', ')}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate NODE_ENV is set appropriately
|
|
201
|
+
*/
|
|
202
|
+
export function checkNodeEnv(): { status: 'PASS' | 'WARN'; message: string } {
|
|
203
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
204
|
+
|
|
205
|
+
if (nodeEnv === 'production' || nodeEnv === 'staging' || nodeEnv === 'development') {
|
|
206
|
+
return {
|
|
207
|
+
status: 'PASS',
|
|
208
|
+
message: `NODE_ENV is set to: ${nodeEnv}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
status: 'WARN',
|
|
214
|
+
message: `NODE_ENV has unexpected value: ${nodeEnv}. Should be production, staging, or development.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check memory and resource limits
|
|
220
|
+
*/
|
|
221
|
+
export function checkResourceLimits(): { status: 'PASS' | 'WARN'; message: string } {
|
|
222
|
+
const memUsage = process.memoryUsage();
|
|
223
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
224
|
+
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
|
|
225
|
+
|
|
226
|
+
if (heapTotalMB > 4096) {
|
|
227
|
+
return {
|
|
228
|
+
status: 'WARN',
|
|
229
|
+
message: `Daemon has high heap allocation: ${heapTotalMB}MB (${heapUsedMB}MB used)`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
status: 'PASS',
|
|
235
|
+
message: `Memory usage: ${heapUsedMB}MB / ${heapTotalMB}MB`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Full validation suite
|
|
241
|
+
*/
|
|
242
|
+
export function validateEnvironment(config: {
|
|
243
|
+
port: number;
|
|
244
|
+
logDir: string;
|
|
245
|
+
dbPath: string;
|
|
246
|
+
requiredEnvVars?: string[];
|
|
247
|
+
critical?: boolean;
|
|
248
|
+
}): ValidationResult {
|
|
249
|
+
const checks = [
|
|
250
|
+
{ name: 'Environment Secrets', ...checkEnvironmentSecrets() },
|
|
251
|
+
{ name: 'Daemon Port', ...checkDaemonPort(config.port) },
|
|
252
|
+
{ name: 'Log Directory', ...checkLogDirectory(config.logDir) },
|
|
253
|
+
{ name: 'Database Path', ...checkDatabasePath(config.dbPath) },
|
|
254
|
+
{ name: 'NODE_ENV', ...checkNodeEnv() },
|
|
255
|
+
{ name: 'Deprecated Variables', ...checkDeprecatedEnvVars() },
|
|
256
|
+
{ name: 'Resource Limits', ...checkResourceLimits() },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
if (config.requiredEnvVars && config.requiredEnvVars.length > 0) {
|
|
260
|
+
checks.push({
|
|
261
|
+
name: 'Required Variables',
|
|
262
|
+
...checkRequiredEnvVars(config.requiredEnvVars),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const failures = checks.filter((c) => c.status === 'FAIL');
|
|
267
|
+
const warnings = checks.filter((c) => c.status === 'WARN');
|
|
268
|
+
const errors = failures.map((c) => c.message);
|
|
269
|
+
|
|
270
|
+
// If critical mode and failures exist, fail entire validation
|
|
271
|
+
const passed = config.critical ? failures.length === 0 : true;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
passed,
|
|
275
|
+
checks,
|
|
276
|
+
errors,
|
|
277
|
+
warnings: warnings.map((c) => c.message),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Format validation result for console output
|
|
283
|
+
*/
|
|
284
|
+
export function formatValidationResult(result: ValidationResult): string {
|
|
285
|
+
const lines: string[] = [];
|
|
286
|
+
|
|
287
|
+
lines.push('\n=== Security Environment Validation ===\n');
|
|
288
|
+
|
|
289
|
+
for (const check of result.checks) {
|
|
290
|
+
const symbol = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '⚠' : '✗';
|
|
291
|
+
lines.push(`${symbol} ${check.name}: ${check.message}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (result.errors.length > 0) {
|
|
295
|
+
lines.push('\nErrors:');
|
|
296
|
+
for (const error of result.errors) {
|
|
297
|
+
lines.push(` ✗ ${error}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (result.warnings.length > 0) {
|
|
302
|
+
lines.push('\nWarnings:');
|
|
303
|
+
for (const warning of result.warnings) {
|
|
304
|
+
lines.push(` ⚠ ${warning}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push(`\nValidation Status: ${result.passed ? 'PASSED' : 'FAILED'}\n`);
|
|
309
|
+
|
|
310
|
+
return lines.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Exit with validation error
|
|
315
|
+
*/
|
|
316
|
+
export function exitOnValidationFailure(result: ValidationResult, exitCode = 1): void {
|
|
317
|
+
if (!result.passed) {
|
|
318
|
+
console.error(formatValidationResult(result));
|
|
319
|
+
process.exit(exitCode);
|
|
320
|
+
}
|
|
321
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { MetricsStore } from './metricsStore.js';
|
|
3
|
+
import { createApiRouter } from './api.js';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export class LeanClaudientDaemon {
|
|
8
|
+
private app = express();
|
|
9
|
+
private metricsStore: MetricsStore;
|
|
10
|
+
private port: number;
|
|
11
|
+
private logPath: string;
|
|
12
|
+
|
|
13
|
+
constructor(port: number = 9831, logPath?: string) {
|
|
14
|
+
this.port = port;
|
|
15
|
+
this.logPath = logPath || path.join(process.env.HOME!, '.lean-claudient', 'daemon.log');
|
|
16
|
+
this.metricsStore = new MetricsStore();
|
|
17
|
+
this.setupExpress();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private setupExpress() {
|
|
21
|
+
this.app.use(express.json());
|
|
22
|
+
this.app.use('/api', createApiRouter(this.metricsStore));
|
|
23
|
+
|
|
24
|
+
this.app.get('/health', (req, res) => {
|
|
25
|
+
res.json({ status: 'ok', uptime: process.uptime(), pid: process.pid });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async start() {
|
|
30
|
+
const logDir = path.dirname(this.logPath);
|
|
31
|
+
if (!fs.existsSync(logDir)) {
|
|
32
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.log('Lean Claudient Daemon starting...');
|
|
36
|
+
|
|
37
|
+
await this.metricsStore.initialize();
|
|
38
|
+
|
|
39
|
+
this.app.listen(this.port, () => {
|
|
40
|
+
this.log(`Daemon listening on port ${this.port}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
process.on('SIGTERM', () => this.shutdown());
|
|
44
|
+
process.on('SIGINT', () => this.shutdown());
|
|
45
|
+
|
|
46
|
+
await new Promise(() => {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async shutdown() {
|
|
50
|
+
this.log('Shutting down daemon...');
|
|
51
|
+
await this.metricsStore.close();
|
|
52
|
+
this.log('Daemon stopped cleanly');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private log(msg: string) {
|
|
57
|
+
const timestamp = new Date().toISOString();
|
|
58
|
+
const line = `[${timestamp}] ${msg}\n`;
|
|
59
|
+
console.log(line);
|
|
60
|
+
fs.appendFileSync(this.logPath, line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Schemas
|
|
3
|
+
* Zod schemas for all API inputs and daemon operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* UUID validation - 36 characters with hyphens
|
|
10
|
+
*/
|
|
11
|
+
export const uuidSchema = z.string().uuid().describe('Valid UUID format');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Session ID schema
|
|
15
|
+
*/
|
|
16
|
+
export const sessionIdSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.uuid()
|
|
19
|
+
.describe('Session ID must be a valid UUID');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Metrics record schema
|
|
23
|
+
*/
|
|
24
|
+
export const metricsRecordSchema = z.object({
|
|
25
|
+
sessionId: sessionIdSchema,
|
|
26
|
+
timestamp: z.number().int().positive().describe('Unix timestamp'),
|
|
27
|
+
inputTokens: z.number().int().nonnegative().describe('Input tokens used'),
|
|
28
|
+
outputTokens: z.number().int().nonnegative().describe('Output tokens generated'),
|
|
29
|
+
totalTokens: z.number().int().nonnegative().describe('Total tokens (input + output)'),
|
|
30
|
+
cost: z.number().nonnegative().describe('Cost in USD'),
|
|
31
|
+
savedTokens: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.nonnegative()
|
|
35
|
+
.describe('Tokens saved through compression'),
|
|
36
|
+
compressionRatio: z.number().nonnegative().describe('Compression ratio percentage'),
|
|
37
|
+
subagentCount: z.number().int().nonnegative().describe('Number of subagents used'),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type MetricsRecord = z.infer<typeof metricsRecordSchema>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Budget status schema
|
|
44
|
+
*/
|
|
45
|
+
export const budgetStatusSchema = z.object({
|
|
46
|
+
sessionId: sessionIdSchema,
|
|
47
|
+
remainingTokens: z.number().int().nonnegative(),
|
|
48
|
+
totalBudget: z.number().int().positive(),
|
|
49
|
+
usedTokens: z.number().int().nonnegative(),
|
|
50
|
+
percentUsed: z.number().min(0).max(100),
|
|
51
|
+
status: z.enum(['ACTIVE', 'WARNING', 'CRITICAL', 'EXCEEDED']),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type BudgetStatus = z.infer<typeof budgetStatusSchema>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Metrics payload schema for POST /api/metrics
|
|
58
|
+
*/
|
|
59
|
+
export const metricsPayloadSchema = z.object({
|
|
60
|
+
sessionId: sessionIdSchema,
|
|
61
|
+
metrics: metricsRecordSchema.omit({ sessionId: true }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type MetricsPayload = z.infer<typeof metricsPayloadSchema>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checkpoint schema - valid git commit hash or checkpoint identifier
|
|
68
|
+
*/
|
|
69
|
+
export const checkpointSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
checkpoint: z.string().min(1).max(1000).describe('Checkpoint identifier or hash'),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
|
|
75
|
+
export type CheckpointPayload = z.infer<typeof checkpointSchema>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Budget schema for budget updates
|
|
79
|
+
*/
|
|
80
|
+
export const budgetSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
sessionId: sessionIdSchema,
|
|
83
|
+
totalBudget: z.number().int().positive().describe('Total token budget'),
|
|
84
|
+
softLimit: z.number().int().positive().optional().describe('Warning threshold'),
|
|
85
|
+
hardLimit: z.number().int().positive().optional().describe('Absolute maximum'),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
|
|
89
|
+
export type BudgetPayload = z.infer<typeof budgetSchema>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Session creation schema
|
|
93
|
+
*/
|
|
94
|
+
export const sessionCreationSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
sessionId: sessionIdSchema,
|
|
97
|
+
budget: z.number().int().positive().describe('Initial token budget'),
|
|
98
|
+
metadata: z.record(z.any()).optional().describe('Custom metadata'),
|
|
99
|
+
})
|
|
100
|
+
.strict();
|
|
101
|
+
|
|
102
|
+
export type SessionCreation = z.infer<typeof sessionCreationSchema>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Query validation schema
|
|
106
|
+
*/
|
|
107
|
+
export const queryParamSchema = z.object({
|
|
108
|
+
sessionId: sessionIdSchema.optional(),
|
|
109
|
+
startTime: z.number().int().nonnegative().optional(),
|
|
110
|
+
endTime: z.number().int().nonnegative().optional(),
|
|
111
|
+
limit: z.number().int().min(1).max(1000).optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export type QueryParams = z.infer<typeof queryParamSchema>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Error response schema
|
|
118
|
+
*/
|
|
119
|
+
export const errorResponseSchema = z.object({
|
|
120
|
+
error: z.string(),
|
|
121
|
+
code: z.string().optional(),
|
|
122
|
+
details: z.any().optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Success response schema
|
|
127
|
+
*/
|
|
128
|
+
export const successResponseSchema = z.object({
|
|
129
|
+
success: z.boolean(),
|
|
130
|
+
data: z.any().optional(),
|
|
131
|
+
message: z.string().optional(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Health check response schema
|
|
136
|
+
*/
|
|
137
|
+
export const healthCheckSchema = z.object({
|
|
138
|
+
running: z.boolean(),
|
|
139
|
+
uptime: z.number().int().nonnegative(),
|
|
140
|
+
memory: z.number().nonnegative(),
|
|
141
|
+
pid: z.number().int().positive(),
|
|
142
|
+
timestamp: z.string().datetime().optional(),
|
|
143
|
+
version: z.string().optional(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Aggregate metrics schema
|
|
148
|
+
*/
|
|
149
|
+
export const aggregateMetricsSchema = z.object({
|
|
150
|
+
totalInputTokens: z.number().int().nonnegative(),
|
|
151
|
+
totalOutputTokens: z.number().int().nonnegative(),
|
|
152
|
+
totalTokens: z.number().int().nonnegative(),
|
|
153
|
+
totalCost: z.string(),
|
|
154
|
+
totalSavedTokens: z.number().int().nonnegative(),
|
|
155
|
+
averageCompressionRatio: z.number().nonnegative(),
|
|
156
|
+
sessionCount: z.number().int().nonnegative(),
|
|
157
|
+
averageSubagentCount: z.number().int().nonnegative(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export type AggregateMetrics = z.infer<typeof aggregateMetricsSchema>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate UUID in path parameter
|
|
164
|
+
*/
|
|
165
|
+
export function validateUUID(id: string): string | null {
|
|
166
|
+
try {
|
|
167
|
+
return uuidSchema.parse(id);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Safe parse helper - returns [data, error]
|
|
175
|
+
*/
|
|
176
|
+
export function safeParse<T>(
|
|
177
|
+
schema: z.ZodSchema<T>,
|
|
178
|
+
data: unknown
|
|
179
|
+
): [T | null, string | null] {
|
|
180
|
+
try {
|
|
181
|
+
const result = schema.parse(data);
|
|
182
|
+
return [result, null];
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof z.ZodError) {
|
|
185
|
+
const messages = error.errors.map((e) => `${e.path.join('.')}: ${e.message}`);
|
|
186
|
+
return [null, messages.join('; ')];
|
|
187
|
+
}
|
|
188
|
+
return [null, String(error)];
|
|
189
|
+
}
|
|
190
|
+
}
|