kotadb 2.0.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 +79 -0
- package/package.json +75 -0
- package/src/api/auto-reindex.ts +55 -0
- package/src/api/openapi/builder.ts +209 -0
- package/src/api/openapi/paths.ts +354 -0
- package/src/api/openapi/schemas.ts +608 -0
- package/src/api/queries.ts +1168 -0
- package/src/api/routes.ts +339 -0
- package/src/auth/middleware.ts +83 -0
- package/src/cli.ts +221 -0
- package/src/config/constants.ts +96 -0
- package/src/config/environment.ts +96 -0
- package/src/config/gitignore.ts +68 -0
- package/src/config/index.ts +20 -0
- package/src/config/project-root.ts +52 -0
- package/src/db/client.ts +72 -0
- package/src/db/sqlite/index.ts +35 -0
- package/src/db/sqlite/jsonl-exporter.ts +416 -0
- package/src/db/sqlite/jsonl-importer.ts +361 -0
- package/src/db/sqlite/sqlite-client.ts +536 -0
- package/src/index.ts +66 -0
- package/src/indexer/ast-parser.ts +146 -0
- package/src/indexer/ast-types.ts +54 -0
- package/src/indexer/circular-detector.ts +262 -0
- package/src/indexer/dependency-extractor.ts +352 -0
- package/src/indexer/extractors.ts +54 -0
- package/src/indexer/import-resolver.ts +167 -0
- package/src/indexer/parsers.ts +177 -0
- package/src/indexer/reference-extractor.ts +488 -0
- package/src/indexer/repos.ts +245 -0
- package/src/indexer/storage.ts +277 -0
- package/src/indexer/symbol-extractor.ts +660 -0
- package/src/instrument.ts +88 -0
- package/src/logging/context.ts +46 -0
- package/src/logging/logger.ts +193 -0
- package/src/logging/middleware.ts +107 -0
- package/src/mcp/github-integration.ts +293 -0
- package/src/mcp/headers.ts +101 -0
- package/src/mcp/impact-analysis.ts +495 -0
- package/src/mcp/jsonrpc.ts +141 -0
- package/src/mcp/lifecycle.ts +73 -0
- package/src/mcp/server.ts +202 -0
- package/src/mcp/session.ts +44 -0
- package/src/mcp/spec-validation.ts +491 -0
- package/src/mcp/tools.ts +889 -0
- package/src/sync/deletion-manifest.ts +210 -0
- package/src/sync/index.ts +16 -0
- package/src/sync/merge-driver.ts +172 -0
- package/src/sync/watcher.ts +221 -0
- package/src/types/rate-limit.ts +88 -0
- package/src/validation/common-schemas.ts +96 -0
- package/src/validation/schemas.ts +187 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry SDK Instrumentation
|
|
3
|
+
* IMPORTANT: This file must be imported first in index.ts before all other imports
|
|
4
|
+
* to ensure proper tracing and error capture.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Sentry from "@sentry/node";
|
|
8
|
+
import { expressErrorHandler } from "@sentry/node";
|
|
9
|
+
|
|
10
|
+
// Test environment guard: disable Sentry in tests to prevent test errors from polluting dashboard
|
|
11
|
+
if (process.env.NODE_ENV !== "test") {
|
|
12
|
+
// Determine environment from Vercel or Node environment
|
|
13
|
+
const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || "development";
|
|
14
|
+
|
|
15
|
+
// Environment-specific sampling rates
|
|
16
|
+
const isDevelopment = environment === "development";
|
|
17
|
+
const tracesSampleRate = isDevelopment ? 1.0 : 0.1;
|
|
18
|
+
|
|
19
|
+
// Initialize Sentry SDK
|
|
20
|
+
try {
|
|
21
|
+
Sentry.init({
|
|
22
|
+
dsn: process.env.SENTRY_DSN,
|
|
23
|
+
environment,
|
|
24
|
+
tracesSampleRate,
|
|
25
|
+
|
|
26
|
+
// Privacy compliance: don't send IP addresses or user agents automatically
|
|
27
|
+
sendDefaultPii: false,
|
|
28
|
+
|
|
29
|
+
// Enable debug mode in development
|
|
30
|
+
debug: isDevelopment,
|
|
31
|
+
|
|
32
|
+
// Scrub sensitive headers before sending to Sentry
|
|
33
|
+
beforeSend(event, hint) {
|
|
34
|
+
// Remove sensitive headers
|
|
35
|
+
if (event.request?.headers) {
|
|
36
|
+
const headers = event.request.headers;
|
|
37
|
+
if (headers.authorization) headers.authorization = "[REDACTED]";
|
|
38
|
+
if (headers["x-api-key"]) headers["x-api-key"] = "[REDACTED]";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add request_id from Express request for correlation with structured logs
|
|
42
|
+
const originalException = hint.originalException as Error & { req?: { requestId?: string } };
|
|
43
|
+
if (originalException?.req?.requestId) {
|
|
44
|
+
event.tags = {
|
|
45
|
+
...event.tags,
|
|
46
|
+
request_id: originalException.req.requestId,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return event;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Filter out health check endpoints from transaction tracking
|
|
54
|
+
beforeSendTransaction(event) {
|
|
55
|
+
// Exclude /health endpoint from performance tracking
|
|
56
|
+
if (event.transaction === "GET /health") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return event;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (isDevelopment && process.env.SENTRY_DSN) {
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
level: "info",
|
|
68
|
+
message: "Sentry SDK initialized",
|
|
69
|
+
environment,
|
|
70
|
+
tracesSampleRate,
|
|
71
|
+
}) + "\n",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Log warning but don't crash - observability failures should not cause outages
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
level: "warn",
|
|
80
|
+
message: "Failed to initialize Sentry SDK",
|
|
81
|
+
error: error instanceof Error ? error.message : String(error),
|
|
82
|
+
}) + "\n",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Export Sentry instance and Express error handler for use in other modules
|
|
88
|
+
export { Sentry, expressErrorHandler };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation context utilities for tracking requests across async boundaries
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import type { LogContext } from "./logger";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a unique request ID for correlation
|
|
10
|
+
*/
|
|
11
|
+
export function generateRequestId(): string {
|
|
12
|
+
return randomUUID();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a unique job ID for queue job correlation
|
|
17
|
+
*/
|
|
18
|
+
export function generateJobId(): string {
|
|
19
|
+
return randomUUID();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a correlation context from request metadata
|
|
24
|
+
*/
|
|
25
|
+
export function createRequestContext(requestId: string, userId?: string, keyId?: string): LogContext {
|
|
26
|
+
const context: LogContext = { request_id: requestId };
|
|
27
|
+
if (userId) context.user_id = userId;
|
|
28
|
+
if (keyId) context.key_id = keyId;
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a correlation context for queue jobs
|
|
34
|
+
*/
|
|
35
|
+
export function createJobContext(jobId: string, userId?: string): LogContext {
|
|
36
|
+
const context: LogContext = { job_id: jobId };
|
|
37
|
+
if (userId) context.user_id = userId;
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extend existing context with additional fields
|
|
43
|
+
*/
|
|
44
|
+
export function extendContext(base: LogContext, additional: LogContext): LogContext {
|
|
45
|
+
return { ...base, ...additional };
|
|
46
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging with JSON format, correlation IDs, and sensitive data masking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
6
|
+
|
|
7
|
+
export interface LogContext {
|
|
8
|
+
request_id?: string;
|
|
9
|
+
user_id?: string;
|
|
10
|
+
key_id?: string;
|
|
11
|
+
job_id?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LogEntry {
|
|
16
|
+
timestamp: string;
|
|
17
|
+
level: LogLevel;
|
|
18
|
+
message: string;
|
|
19
|
+
context?: LogContext;
|
|
20
|
+
error?: {
|
|
21
|
+
message: string;
|
|
22
|
+
stack?: string;
|
|
23
|
+
code?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Logger {
|
|
28
|
+
debug(message: string, context?: LogContext): void;
|
|
29
|
+
info(message: string, context?: LogContext): void;
|
|
30
|
+
warn(message: string, context?: LogContext): void;
|
|
31
|
+
error(message: string, errorOrContext?: Error | LogContext, context?: LogContext): void;
|
|
32
|
+
child(childContext: LogContext): Logger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sensitive keys that should be masked in logs
|
|
36
|
+
const SENSITIVE_KEYS = [
|
|
37
|
+
"apiKey",
|
|
38
|
+
"api_key",
|
|
39
|
+
"apikey",
|
|
40
|
+
"token",
|
|
41
|
+
"password",
|
|
42
|
+
"secret",
|
|
43
|
+
"authorization",
|
|
44
|
+
"bearer",
|
|
45
|
+
"key",
|
|
46
|
+
"private_key",
|
|
47
|
+
"privateKey",
|
|
48
|
+
"client_secret",
|
|
49
|
+
"clientSecret",
|
|
50
|
+
"access_token",
|
|
51
|
+
"accessToken",
|
|
52
|
+
"refresh_token",
|
|
53
|
+
"refreshToken",
|
|
54
|
+
"session",
|
|
55
|
+
"cookie",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
59
|
+
debug: 0,
|
|
60
|
+
info: 1,
|
|
61
|
+
warn: 2,
|
|
62
|
+
error: 3,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get configured log level from environment (default: info)
|
|
67
|
+
*/
|
|
68
|
+
function getLogLevel(): LogLevel {
|
|
69
|
+
const level = process.env.LOG_LEVEL?.toLowerCase();
|
|
70
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
71
|
+
return level;
|
|
72
|
+
}
|
|
73
|
+
return "info";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a log level should be output based on configured level
|
|
78
|
+
*/
|
|
79
|
+
function shouldLog(level: LogLevel): boolean {
|
|
80
|
+
const configuredLevel = getLogLevel();
|
|
81
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[configuredLevel];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mask sensitive data in context objects
|
|
86
|
+
*/
|
|
87
|
+
function maskSensitiveData(context: LogContext): LogContext {
|
|
88
|
+
const masked: LogContext = {};
|
|
89
|
+
for (const [key, value] of Object.entries(context)) {
|
|
90
|
+
if (SENSITIVE_KEYS.some((sensitiveKey) => key.toLowerCase().includes(sensitiveKey))) {
|
|
91
|
+
masked[key] = "[REDACTED]";
|
|
92
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
93
|
+
masked[key] = maskSensitiveData(value as LogContext);
|
|
94
|
+
} else {
|
|
95
|
+
masked[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return masked;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format and write log entry to stdout (info/debug/warn) or stderr (error)
|
|
103
|
+
*/
|
|
104
|
+
function writeLog(entry: LogEntry): void {
|
|
105
|
+
const json = JSON.stringify(entry);
|
|
106
|
+
const output = `${json}\n`;
|
|
107
|
+
|
|
108
|
+
if (entry.level === "error") {
|
|
109
|
+
process.stderr.write(output);
|
|
110
|
+
} else {
|
|
111
|
+
process.stdout.write(output);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a logger instance with optional correlation context
|
|
117
|
+
*/
|
|
118
|
+
export function createLogger(baseContext?: LogContext): Logger {
|
|
119
|
+
const context = baseContext ? maskSensitiveData(baseContext) : {};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
debug(message: string, additionalContext?: LogContext): void {
|
|
123
|
+
if (!shouldLog("debug")) return;
|
|
124
|
+
|
|
125
|
+
const entry: LogEntry = {
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
level: "debug",
|
|
128
|
+
message,
|
|
129
|
+
context: additionalContext ? { ...context, ...maskSensitiveData(additionalContext) } : Object.keys(context).length > 0 ? context : undefined,
|
|
130
|
+
};
|
|
131
|
+
writeLog(entry);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
info(message: string, additionalContext?: LogContext): void {
|
|
135
|
+
if (!shouldLog("info")) return;
|
|
136
|
+
|
|
137
|
+
const entry: LogEntry = {
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
level: "info",
|
|
140
|
+
message,
|
|
141
|
+
context: additionalContext ? { ...context, ...maskSensitiveData(additionalContext) } : Object.keys(context).length > 0 ? context : undefined,
|
|
142
|
+
};
|
|
143
|
+
writeLog(entry);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
warn(message: string, additionalContext?: LogContext): void {
|
|
147
|
+
if (!shouldLog("warn")) return;
|
|
148
|
+
|
|
149
|
+
const entry: LogEntry = {
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
level: "warn",
|
|
152
|
+
message,
|
|
153
|
+
context: additionalContext ? { ...context, ...maskSensitiveData(additionalContext) } : Object.keys(context).length > 0 ? context : undefined,
|
|
154
|
+
};
|
|
155
|
+
writeLog(entry);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
error(message: string, errorOrContext?: Error | LogContext, additionalContext?: LogContext): void {
|
|
159
|
+
if (!shouldLog("error")) return;
|
|
160
|
+
|
|
161
|
+
const entry: LogEntry = {
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
level: "error",
|
|
164
|
+
message,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Handle error as second parameter
|
|
168
|
+
if (errorOrContext instanceof Error) {
|
|
169
|
+
entry.error = {
|
|
170
|
+
message: errorOrContext.message,
|
|
171
|
+
stack: errorOrContext.stack,
|
|
172
|
+
code: (errorOrContext as Error & { code?: string }).code,
|
|
173
|
+
};
|
|
174
|
+
if (additionalContext) {
|
|
175
|
+
entry.context = { ...context, ...maskSensitiveData(additionalContext) };
|
|
176
|
+
} else if (Object.keys(context).length > 0) {
|
|
177
|
+
entry.context = context;
|
|
178
|
+
}
|
|
179
|
+
} else if (errorOrContext) {
|
|
180
|
+
// Handle context as second parameter
|
|
181
|
+
entry.context = { ...context, ...maskSensitiveData(errorOrContext) };
|
|
182
|
+
} else if (Object.keys(context).length > 0) {
|
|
183
|
+
entry.context = context;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
writeLog(entry);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
child(childContext: LogContext): Logger {
|
|
190
|
+
return createLogger({ ...context, ...childContext });
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware for request/response logging with correlation IDs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Request, Response, NextFunction } from "express";
|
|
6
|
+
import { createLogger } from "./logger";
|
|
7
|
+
import { generateRequestId, createRequestContext } from "./context";
|
|
8
|
+
import type { Logger } from "./logger";
|
|
9
|
+
|
|
10
|
+
// Extend Express Request to include logger
|
|
11
|
+
declare global {
|
|
12
|
+
namespace Express {
|
|
13
|
+
interface Request {
|
|
14
|
+
logger: Logger;
|
|
15
|
+
requestId: string;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Express middleware that:
|
|
22
|
+
* - Generates unique request_id for each request
|
|
23
|
+
* - Attaches logger instance to req.logger with correlation context
|
|
24
|
+
* - Logs incoming requests and outgoing responses
|
|
25
|
+
* - Captures errors with stack traces
|
|
26
|
+
*/
|
|
27
|
+
export function requestLoggingMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
const requestId = generateRequestId();
|
|
30
|
+
|
|
31
|
+
// Attach request ID to request object
|
|
32
|
+
req.requestId = requestId;
|
|
33
|
+
|
|
34
|
+
// Create base logger with request_id
|
|
35
|
+
const baseContext = createRequestContext(requestId);
|
|
36
|
+
req.logger = createLogger(baseContext);
|
|
37
|
+
|
|
38
|
+
// Log incoming request
|
|
39
|
+
req.logger.info("Incoming request", {
|
|
40
|
+
method: req.method,
|
|
41
|
+
url: req.url,
|
|
42
|
+
path: req.path,
|
|
43
|
+
ip: req.ip,
|
|
44
|
+
userAgent: req.get("user-agent"),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Capture response finish event for logging
|
|
48
|
+
const originalSend = res.send;
|
|
49
|
+
res.send = function (body) {
|
|
50
|
+
// Log response
|
|
51
|
+
const duration = Date.now() - startTime;
|
|
52
|
+
const level = res.statusCode >= 400 ? "warn" : "info";
|
|
53
|
+
|
|
54
|
+
if (level === "warn") {
|
|
55
|
+
req.logger.warn("Request completed with error", {
|
|
56
|
+
method: req.method,
|
|
57
|
+
url: req.url,
|
|
58
|
+
status: res.statusCode,
|
|
59
|
+
duration_ms: duration,
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
req.logger.info("Request completed", {
|
|
63
|
+
method: req.method,
|
|
64
|
+
url: req.url,
|
|
65
|
+
status: res.statusCode,
|
|
66
|
+
duration_ms: duration,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return originalSend.call(this, body);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Capture errors
|
|
74
|
+
const originalNext = next;
|
|
75
|
+
next = function (error?: unknown) {
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
req.logger.error("Request error", error);
|
|
78
|
+
}
|
|
79
|
+
return originalNext(error);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
next();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Error handling middleware to log unhandled errors
|
|
87
|
+
*/
|
|
88
|
+
export function errorLoggingMiddleware(error: Error, req: Request, res: Response, next: NextFunction): void {
|
|
89
|
+
// Log error with full stack trace
|
|
90
|
+
if (req.logger) {
|
|
91
|
+
req.logger.error("Unhandled error", error, {
|
|
92
|
+
method: req.method,
|
|
93
|
+
url: req.url,
|
|
94
|
+
path: req.path,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// Fallback if logger not attached
|
|
98
|
+
const logger = createLogger({ request_id: req.requestId || "unknown" });
|
|
99
|
+
logger.error("Unhandled error (no logger attached)", error, {
|
|
100
|
+
method: req.method,
|
|
101
|
+
url: req.url,
|
|
102
|
+
path: req.path,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
next(error);
|
|
107
|
+
}
|