@elliotding/ai-agent-mcp 0.1.25 → 0.1.27
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/package.json +4 -1
- package/.prompt-cache/cmd-cmd-client-sdk-ai-hub-generate-testcase.md +0 -101
- package/.prompt-cache/cmd-cmd-client-sdk-ai-hub-submit_zct_job.md +0 -158
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-conf-status.md +0 -311
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-sdk-log.md +0 -64
- package/.prompt-cache/skill-skill-client-sdk-ai-hub-analyze-zmb-log-errors.md +0 -84
- package/ai-resource-telemetry.json +0 -40
- package/src/api/cached-client.ts +0 -144
- package/src/api/client.ts +0 -697
- package/src/auth/index.ts +0 -11
- package/src/auth/middleware.ts +0 -244
- package/src/auth/permissions.ts +0 -323
- package/src/auth/token-validator.ts +0 -292
- package/src/cache/cache-manager.ts +0 -243
- package/src/cache/index.ts +0 -6
- package/src/cache/redis-client.ts +0 -249
- package/src/config/constants.ts +0 -33
- package/src/config/index.ts +0 -269
- package/src/filesystem/manager.ts +0 -235
- package/src/git/multi-source-manager.ts +0 -654
- package/src/git/operations.ts +0 -93
- package/src/index.ts +0 -157
- package/src/monitoring/health.ts +0 -132
- package/src/prompts/cache.ts +0 -140
- package/src/prompts/generator.ts +0 -143
- package/src/prompts/index.ts +0 -20
- package/src/prompts/manager.ts +0 -718
- package/src/resources/index.ts +0 -13
- package/src/resources/loader.ts +0 -563
- package/src/server/http.ts +0 -549
- package/src/server.ts +0 -206
- package/src/session/manager.ts +0 -296
- package/src/telemetry/index.ts +0 -10
- package/src/telemetry/manager.ts +0 -419
- package/src/tools/index.ts +0 -13
- package/src/tools/manage-subscription.ts +0 -388
- package/src/tools/registry.ts +0 -97
- package/src/tools/resolve-prompt-content.ts +0 -113
- package/src/tools/search-resources.ts +0 -185
- package/src/tools/sync-resources.ts +0 -829
- package/src/tools/track-usage.ts +0 -113
- package/src/tools/uninstall-resource.ts +0 -199
- package/src/tools/upload-resource.ts +0 -431
- package/src/transport/sse.ts +0 -308
- package/src/types/errors.ts +0 -146
- package/src/types/index.ts +0 -7
- package/src/types/mcp.ts +0 -61
- package/src/types/resources.ts +0 -141
- package/src/types/tools.ts +0 -305
- package/src/utils/cursor-paths.ts +0 -135
- package/src/utils/log-cleaner.ts +0 -92
- package/src/utils/logger.ts +0 -333
- package/src/utils/validation.ts +0 -262
package/src/utils/logger.ts
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logging Module
|
|
3
|
-
* Structured logging using pino with daily file rotation.
|
|
4
|
-
*
|
|
5
|
-
* Files are named app-YYYY-MM-DD.log. Rotation is implemented by:
|
|
6
|
-
* 1. Starting pino/file pointing at today's file (fixed fd, opened at startup).
|
|
7
|
-
* 2. A midnight timer in the main thread spawns a fresh child process for the
|
|
8
|
-
* next day's file via a second pino instance — but that would mean two loggers.
|
|
9
|
-
*
|
|
10
|
-
* Practical solution used here:
|
|
11
|
-
* - Use pino-roll (daily, dateFormat: 'yyyy-MM-dd').
|
|
12
|
-
* - pino-roll produces Logs/app.YYYY-MM-DD.1.log (date + sequential counter).
|
|
13
|
-
* - At midnight + 2 s we rename the *previous* day's app.YYYY-MM-DD.1.log
|
|
14
|
-
* → app-YYYY-MM-DD.log so the canonical name is clean.
|
|
15
|
-
* - The active (today's) file keeps the pino-roll name until it rotates.
|
|
16
|
-
* - log-cleaner scans by mtime so it handles both naming conventions.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import pino from 'pino';
|
|
20
|
-
import * as path from 'path';
|
|
21
|
-
import * as fs from 'fs';
|
|
22
|
-
import { config } from '../config';
|
|
23
|
-
|
|
24
|
-
// Ensure logs directory exists (relative to project root)
|
|
25
|
-
const logsDir = path.resolve(process.cwd(), config.logging.dir);
|
|
26
|
-
if (!fs.existsSync(logsDir)) {
|
|
27
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** ms until the next local midnight + 1 s buffer. */
|
|
31
|
-
function msUntilMidnight(): number {
|
|
32
|
-
const now = new Date();
|
|
33
|
-
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 1);
|
|
34
|
-
return next.getTime() - now.getTime();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Rename yesterday's pino-roll file (app.YYYY-MM-DD.1.log)
|
|
39
|
-
* to the canonical name (app-YYYY-MM-DD.log) once it has been rotated away.
|
|
40
|
-
*/
|
|
41
|
-
function renameYesterdayLog(): void {
|
|
42
|
-
const d = new Date();
|
|
43
|
-
d.setDate(d.getDate() - 1);
|
|
44
|
-
const dateStr = [
|
|
45
|
-
d.getFullYear(),
|
|
46
|
-
String(d.getMonth() + 1).padStart(2, '0'),
|
|
47
|
-
String(d.getDate()).padStart(2, '0'),
|
|
48
|
-
].join('-');
|
|
49
|
-
|
|
50
|
-
const src = path.join(logsDir, `app.${dateStr}.1.log`);
|
|
51
|
-
const dst = path.join(logsDir, `app-${dateStr}.log`);
|
|
52
|
-
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
53
|
-
try { fs.renameSync(src, dst); } catch { /* non-fatal */ }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Fire rename at midnight + 2 s, then every 24 h.
|
|
58
|
-
setTimeout(() => {
|
|
59
|
-
renameYesterdayLog();
|
|
60
|
-
setInterval(renameYesterdayLog, 24 * 60 * 60 * 1000).unref();
|
|
61
|
-
}, msUntilMidnight() + 2000).unref();
|
|
62
|
-
|
|
63
|
-
// Create pino logger with multi-target transport
|
|
64
|
-
export const logger = pino({
|
|
65
|
-
level: config.logLevel,
|
|
66
|
-
timestamp: pino.stdTimeFunctions.isoTime,
|
|
67
|
-
base: {
|
|
68
|
-
service: 'csp-ai-agent-mcp',
|
|
69
|
-
},
|
|
70
|
-
transport: {
|
|
71
|
-
targets: [
|
|
72
|
-
// Console output (pretty format in development)
|
|
73
|
-
{
|
|
74
|
-
target: 'pino-pretty',
|
|
75
|
-
level: config.logLevel,
|
|
76
|
-
options: {
|
|
77
|
-
colorize: true,
|
|
78
|
-
translateTime: 'SYS:standard',
|
|
79
|
-
ignore: 'pid,hostname',
|
|
80
|
-
singleLine: false,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
// Daily-rotating file output.
|
|
84
|
-
// Active file: Logs/app.YYYY-MM-DD.1.log
|
|
85
|
-
// After midnight rename: Logs/app-YYYY-MM-DD.log
|
|
86
|
-
{
|
|
87
|
-
target: 'pino-roll',
|
|
88
|
-
level: config.logLevel,
|
|
89
|
-
options: {
|
|
90
|
-
file: path.join(logsDir, 'app'),
|
|
91
|
-
frequency: 'daily',
|
|
92
|
-
dateFormat: 'yyyy-MM-dd',
|
|
93
|
-
mkdir: true,
|
|
94
|
-
sync: false,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Log MCP Tool call
|
|
103
|
-
*/
|
|
104
|
-
export function logToolCall(
|
|
105
|
-
toolName: string,
|
|
106
|
-
userId: string,
|
|
107
|
-
params: Record<string, unknown>,
|
|
108
|
-
durationMs: number
|
|
109
|
-
): void {
|
|
110
|
-
logger.info(
|
|
111
|
-
{
|
|
112
|
-
type: 'tool_call',
|
|
113
|
-
toolName,
|
|
114
|
-
userId,
|
|
115
|
-
params,
|
|
116
|
-
durationMs,
|
|
117
|
-
},
|
|
118
|
-
`Tool ${toolName} called by ${userId} (${durationMs}ms)`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Log error with context
|
|
124
|
-
*/
|
|
125
|
-
export function logError(error: Error, context?: Record<string, unknown>): void {
|
|
126
|
-
logger.error(
|
|
127
|
-
{
|
|
128
|
-
type: 'error',
|
|
129
|
-
error: {
|
|
130
|
-
message: error.message,
|
|
131
|
-
stack: error.stack,
|
|
132
|
-
name: error.name,
|
|
133
|
-
},
|
|
134
|
-
...context,
|
|
135
|
-
},
|
|
136
|
-
error.message
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Log performance metrics
|
|
142
|
-
*/
|
|
143
|
-
export function logPerformance(
|
|
144
|
-
operation: string,
|
|
145
|
-
durationMs: number,
|
|
146
|
-
metadata?: Record<string, unknown>
|
|
147
|
-
): void {
|
|
148
|
-
logger.info(
|
|
149
|
-
{
|
|
150
|
-
type: 'performance',
|
|
151
|
-
operation,
|
|
152
|
-
durationMs,
|
|
153
|
-
...metadata,
|
|
154
|
-
},
|
|
155
|
-
`${operation} completed in ${durationMs}ms`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Log API request with detailed information
|
|
161
|
-
*/
|
|
162
|
-
export function logApiRequest(
|
|
163
|
-
method: string,
|
|
164
|
-
url: string,
|
|
165
|
-
statusCode: number,
|
|
166
|
-
durationMs: number,
|
|
167
|
-
requestData?: unknown,
|
|
168
|
-
responseData?: unknown,
|
|
169
|
-
headers?: Record<string, string>
|
|
170
|
-
): void {
|
|
171
|
-
logger.info(
|
|
172
|
-
{
|
|
173
|
-
type: 'api_request',
|
|
174
|
-
method,
|
|
175
|
-
url,
|
|
176
|
-
statusCode,
|
|
177
|
-
durationMs,
|
|
178
|
-
requestData: requestData ? JSON.stringify(requestData).substring(0, 500) : undefined,
|
|
179
|
-
responseData: responseData ? JSON.stringify(responseData).substring(0, 1000) : undefined,
|
|
180
|
-
headers: headers ? sanitizeHeaders(headers) : undefined,
|
|
181
|
-
},
|
|
182
|
-
`${method} ${url} - ${statusCode} (${durationMs}ms)`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Log API error with full details
|
|
188
|
-
*/
|
|
189
|
-
export function logApiError(
|
|
190
|
-
method: string,
|
|
191
|
-
url: string,
|
|
192
|
-
error: Error,
|
|
193
|
-
requestData?: unknown,
|
|
194
|
-
statusCode?: number
|
|
195
|
-
): void {
|
|
196
|
-
logger.error(
|
|
197
|
-
{
|
|
198
|
-
type: 'api_error',
|
|
199
|
-
method,
|
|
200
|
-
url,
|
|
201
|
-
statusCode,
|
|
202
|
-
requestData: requestData ? JSON.stringify(requestData).substring(0, 500) : undefined,
|
|
203
|
-
error: {
|
|
204
|
-
message: error.message,
|
|
205
|
-
stack: error.stack,
|
|
206
|
-
name: error.name,
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
`API Error: ${method} ${url} - ${error.message}`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Log tool execution step
|
|
215
|
-
*/
|
|
216
|
-
export function logToolStep(
|
|
217
|
-
toolName: string,
|
|
218
|
-
step: string,
|
|
219
|
-
details?: Record<string, unknown>
|
|
220
|
-
): void {
|
|
221
|
-
logger.debug(
|
|
222
|
-
{
|
|
223
|
-
type: 'tool_step',
|
|
224
|
-
toolName,
|
|
225
|
-
step,
|
|
226
|
-
...details,
|
|
227
|
-
},
|
|
228
|
-
`[${toolName}] ${step}`
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Log tool execution result
|
|
234
|
-
*/
|
|
235
|
-
export function logToolResult(
|
|
236
|
-
toolName: string,
|
|
237
|
-
success: boolean,
|
|
238
|
-
result?: unknown,
|
|
239
|
-
error?: Error
|
|
240
|
-
): void {
|
|
241
|
-
const level = success ? 'info' : 'error';
|
|
242
|
-
logger[level](
|
|
243
|
-
{
|
|
244
|
-
type: 'tool_result',
|
|
245
|
-
toolName,
|
|
246
|
-
success,
|
|
247
|
-
result: result ? JSON.stringify(result).substring(0, 1000) : undefined,
|
|
248
|
-
error: error ? {
|
|
249
|
-
message: error.message,
|
|
250
|
-
stack: error.stack,
|
|
251
|
-
name: error.name,
|
|
252
|
-
} : undefined,
|
|
253
|
-
},
|
|
254
|
-
`[${toolName}] ${success ? 'Success' : 'Failed'}`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Log authentication attempt
|
|
260
|
-
*/
|
|
261
|
-
export function logAuthAttempt(
|
|
262
|
-
type: 'token_validation' | 'permission_check',
|
|
263
|
-
success: boolean,
|
|
264
|
-
details?: Record<string, unknown>
|
|
265
|
-
): void {
|
|
266
|
-
const level = success ? 'info' : 'warn';
|
|
267
|
-
logger[level](
|
|
268
|
-
{
|
|
269
|
-
type: 'auth',
|
|
270
|
-
operation: type,
|
|
271
|
-
success,
|
|
272
|
-
...details,
|
|
273
|
-
},
|
|
274
|
-
`Auth ${type}: ${success ? 'Success' : 'Failed'}`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Log cache operation
|
|
280
|
-
*/
|
|
281
|
-
export function logCacheOperation(
|
|
282
|
-
operation: 'get' | 'set' | 'delete' | 'hit' | 'miss',
|
|
283
|
-
key: string,
|
|
284
|
-
details?: Record<string, unknown>
|
|
285
|
-
): void {
|
|
286
|
-
logger.debug(
|
|
287
|
-
{
|
|
288
|
-
type: 'cache',
|
|
289
|
-
operation,
|
|
290
|
-
key,
|
|
291
|
-
...details,
|
|
292
|
-
},
|
|
293
|
-
`Cache ${operation}: ${key}`
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Sanitize headers to remove sensitive information
|
|
299
|
-
*/
|
|
300
|
-
function sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
|
|
301
|
-
const sanitized = { ...headers };
|
|
302
|
-
|
|
303
|
-
// Mask Authorization header
|
|
304
|
-
if (sanitized['Authorization'] || sanitized['authorization']) {
|
|
305
|
-
const authKey = sanitized['Authorization'] ? 'Authorization' : 'authorization';
|
|
306
|
-
const authValue = sanitized[authKey];
|
|
307
|
-
if (authValue && authValue.startsWith('Bearer ')) {
|
|
308
|
-
const token = authValue.substring(7);
|
|
309
|
-
sanitized[authKey] = `Bearer ${token.substring(0, 10)}...${token.substring(token.length - 10)}`;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return sanitized;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Log Git operation
|
|
318
|
-
*/
|
|
319
|
-
export function logGitOperation(
|
|
320
|
-
operation: string,
|
|
321
|
-
details: Record<string, unknown>,
|
|
322
|
-
durationMs: number
|
|
323
|
-
): void {
|
|
324
|
-
logger.info(
|
|
325
|
-
{
|
|
326
|
-
type: 'git_operation',
|
|
327
|
-
operation,
|
|
328
|
-
...details,
|
|
329
|
-
durationMs,
|
|
330
|
-
},
|
|
331
|
-
`Git ${operation} completed (${durationMs}ms)`
|
|
332
|
-
);
|
|
333
|
-
}
|
package/src/utils/validation.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request Validation Utilities
|
|
3
|
-
* Enhanced validation with clear error messages
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface ValidationError {
|
|
7
|
-
field: string;
|
|
8
|
-
message: string;
|
|
9
|
-
expected?: string;
|
|
10
|
-
received?: any;
|
|
11
|
-
suggestion?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class RequestValidationError extends Error {
|
|
15
|
-
public errors: ValidationError[];
|
|
16
|
-
public statusCode: number;
|
|
17
|
-
|
|
18
|
-
constructor(errors: ValidationError[], statusCode = 400) {
|
|
19
|
-
const message = errors.map(e => `${e.field}: ${e.message}`).join('; ');
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = 'RequestValidationError';
|
|
22
|
-
this.errors = errors;
|
|
23
|
-
this.statusCode = statusCode;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
toJSON() {
|
|
27
|
-
return {
|
|
28
|
-
error: 'Validation Error',
|
|
29
|
-
message: this.message,
|
|
30
|
-
details: this.errors,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate required field
|
|
37
|
-
*/
|
|
38
|
-
export function validateRequired(
|
|
39
|
-
value: any,
|
|
40
|
-
fieldName: string
|
|
41
|
-
): ValidationError | null {
|
|
42
|
-
if (value === undefined || value === null || value === '') {
|
|
43
|
-
return {
|
|
44
|
-
field: fieldName,
|
|
45
|
-
message: `Missing required field: '${fieldName}'`,
|
|
46
|
-
expected: 'non-empty value',
|
|
47
|
-
received: typeof value,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Validate string type
|
|
55
|
-
*/
|
|
56
|
-
export function validateString(
|
|
57
|
-
value: any,
|
|
58
|
-
fieldName: string
|
|
59
|
-
): ValidationError | null {
|
|
60
|
-
if (typeof value !== 'string') {
|
|
61
|
-
return {
|
|
62
|
-
field: fieldName,
|
|
63
|
-
message: `Field '${fieldName}' must be a string`,
|
|
64
|
-
expected: 'string',
|
|
65
|
-
received: typeof value,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Validate enum value
|
|
73
|
-
*/
|
|
74
|
-
export function validateEnum(
|
|
75
|
-
value: any,
|
|
76
|
-
fieldName: string,
|
|
77
|
-
allowedValues: readonly string[]
|
|
78
|
-
): ValidationError | null {
|
|
79
|
-
if (!allowedValues.includes(value)) {
|
|
80
|
-
// Find closest match for suggestion
|
|
81
|
-
const suggestion = findClosestMatch(value, allowedValues);
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
field: fieldName,
|
|
85
|
-
message: `Field '${fieldName}' has invalid value`,
|
|
86
|
-
expected: `one of: ${allowedValues.map(v => `'${v}'`).join(', ')}`,
|
|
87
|
-
received: value,
|
|
88
|
-
suggestion: suggestion ? `Did you mean '${suggestion}'?` : undefined,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Validate array type
|
|
96
|
-
*/
|
|
97
|
-
export function validateArray(
|
|
98
|
-
value: any,
|
|
99
|
-
fieldName: string
|
|
100
|
-
): ValidationError | null {
|
|
101
|
-
if (!Array.isArray(value)) {
|
|
102
|
-
return {
|
|
103
|
-
field: fieldName,
|
|
104
|
-
message: `Field '${fieldName}' must be an array`,
|
|
105
|
-
expected: 'array',
|
|
106
|
-
received: typeof value,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Validate object type
|
|
114
|
-
*/
|
|
115
|
-
export function validateObject(
|
|
116
|
-
value: any,
|
|
117
|
-
fieldName: string
|
|
118
|
-
): ValidationError | null {
|
|
119
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
120
|
-
return {
|
|
121
|
-
field: fieldName,
|
|
122
|
-
message: `Field '${fieldName}' must be an object`,
|
|
123
|
-
expected: 'object',
|
|
124
|
-
received: Array.isArray(value) ? 'array' : typeof value,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Validate boolean type
|
|
132
|
-
*/
|
|
133
|
-
export function validateBoolean(
|
|
134
|
-
value: any,
|
|
135
|
-
fieldName: string
|
|
136
|
-
): ValidationError | null {
|
|
137
|
-
if (typeof value !== 'boolean') {
|
|
138
|
-
return {
|
|
139
|
-
field: fieldName,
|
|
140
|
-
message: `Field '${fieldName}' must be a boolean`,
|
|
141
|
-
expected: 'boolean (true or false)',
|
|
142
|
-
received: typeof value,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Validate number type
|
|
150
|
-
*/
|
|
151
|
-
export function validateNumber(
|
|
152
|
-
value: any,
|
|
153
|
-
fieldName: string
|
|
154
|
-
): ValidationError | null {
|
|
155
|
-
if (typeof value !== 'number' || isNaN(value)) {
|
|
156
|
-
return {
|
|
157
|
-
field: fieldName,
|
|
158
|
-
message: `Field '${fieldName}' must be a number`,
|
|
159
|
-
expected: 'number',
|
|
160
|
-
received: typeof value,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Find closest string match (simple Levenshtein distance)
|
|
168
|
-
*/
|
|
169
|
-
function findClosestMatch(
|
|
170
|
-
value: string,
|
|
171
|
-
candidates: readonly string[]
|
|
172
|
-
): string | null {
|
|
173
|
-
if (!value || typeof value !== 'string') {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let minDistance = Infinity;
|
|
178
|
-
let closest: string | null = null;
|
|
179
|
-
|
|
180
|
-
for (const candidate of candidates) {
|
|
181
|
-
const distance = levenshteinDistance(
|
|
182
|
-
value.toLowerCase(),
|
|
183
|
-
candidate.toLowerCase()
|
|
184
|
-
);
|
|
185
|
-
if (distance < minDistance && distance <= 2) {
|
|
186
|
-
// Only suggest if distance is small
|
|
187
|
-
minDistance = distance;
|
|
188
|
-
closest = candidate;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return closest;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Calculate Levenshtein distance between two strings
|
|
197
|
-
*/
|
|
198
|
-
function levenshteinDistance(a: string, b: string): number {
|
|
199
|
-
const matrix: number[][] = [];
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i <= b.length; i++) {
|
|
202
|
-
matrix[i] = [i];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (let j = 0; j <= a.length; j++) {
|
|
206
|
-
matrix[0]![j] = j;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (let i = 1; i <= b.length; i++) {
|
|
210
|
-
for (let j = 1; j <= a.length; j++) {
|
|
211
|
-
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
212
|
-
matrix[i]![j] = matrix[i - 1]![j - 1]!;
|
|
213
|
-
} else {
|
|
214
|
-
matrix[i]![j] = Math.min(
|
|
215
|
-
matrix[i - 1]![j - 1]! + 1, // substitution
|
|
216
|
-
matrix[i]![j - 1]! + 1, // insertion
|
|
217
|
-
matrix[i - 1]![j]! + 1 // deletion
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return matrix[b.length]![a.length]!;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Validate SSE connection parameters
|
|
228
|
-
*/
|
|
229
|
-
export function validateSSEConnectionParams(_body: any): ValidationError[] {
|
|
230
|
-
const errors: ValidationError[] = [];
|
|
231
|
-
|
|
232
|
-
// Validate Authorization header (will be checked in middleware)
|
|
233
|
-
// No body parameters required for SSE connection
|
|
234
|
-
|
|
235
|
-
return errors;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Validate message parameters
|
|
240
|
-
*/
|
|
241
|
-
export function validateMessageParams(body: any): ValidationError[] {
|
|
242
|
-
const errors: ValidationError[] = [];
|
|
243
|
-
|
|
244
|
-
// sessionId is required
|
|
245
|
-
const sessionIdError = validateRequired(body.sessionId, 'sessionId');
|
|
246
|
-
if (sessionIdError) {
|
|
247
|
-
errors.push(sessionIdError);
|
|
248
|
-
} else {
|
|
249
|
-
const sessionIdTypeError = validateString(body.sessionId, 'sessionId');
|
|
250
|
-
if (sessionIdTypeError) {
|
|
251
|
-
errors.push(sessionIdTypeError);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// message is required
|
|
256
|
-
const messageError = validateRequired(body.message, 'message');
|
|
257
|
-
if (messageError) {
|
|
258
|
-
errors.push(messageError);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return errors;
|
|
262
|
-
}
|