Files
codegraph/src/errors.ts
T
Colby McHenryandClaude Opus 5 0196c2e53a feat(ui): serve the viewer from codegraph ui, loopback-only and read-only (CG-41)
Adds the `codegraph ui [path]` command (alias `web`) and `src/ui-server/`, a
`node:http` server with no framework and no new dependency.

The command reads an index that already exists — it never creates one, so a
missing index prints the same friendly guidance the MCP tools give instead of
a stack trace, and a sensitive system directory is refused up front.

Security is the substance here, not the routing. The server binds 127.0.0.1
only, answers GET and HEAD only, and sends no CORS headers ever. The realistic
attack on a process that serves your source code from a local port is DNS
rebinding, so every request must carry a loopback `Host` (on our port) and, if
it carries an `Origin` at all, a loopback one — anything else is 403 before
the filesystem is touched. Every path resolves through the engine's existing
`validatePathWithinRoot` chokepoint, which already handles `../` traversal and
in-tree symlinks pointing out of the root (#527); `..` segments are refused
outright so a traversal attempt gets a 404 rather than the SPA shell.

`PathRefusalError` moves from `mcp/tools.ts` into the dependency-free
`errors.ts` (re-exported from its old home, so class identity and every
`instanceof` check are unchanged) — that is what lets a non-MCP read sink
enforce the same refusal without importing the MCP tool graph.

Assets come from `dist/viewer/` resolved relative to `__dirname`, the way
`db/index.ts` finds `schema.sql`. Hashed assets are cached immutably,
`index.html` never. Port 4747, or the next free one — an explicit `--port`
stays explicit rather than silently moving. `--no-open` skips the browser, and
`CODEGRAPH_BROWSER` picks one (or `none` to suppress it), which is also what
makes "did it open a browser" testable end to end.

`resolveProjectFile` and the `/api/` handler seam are the boundary CG-42's
JSON API plugs into; `/api/*` 404s as JSON so a typo'd endpoint never returns
the app shell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:17:09 -05:00

255 lines
6.4 KiB
TypeScript

/**
* CodeGraph Error Classes
*
* Custom error types for better error handling and debugging.
*
* @module errors
*
* @example
* ```typescript
* import { FileError, ParseError, setLogger, silentLogger } from 'codegraph';
*
* // Catch specific error types
* try {
* await cg.indexAll();
* } catch (error) {
* if (error instanceof FileError) {
* console.log(`File error at ${error.filePath}: ${error.message}`);
* } else if (error instanceof ParseError) {
* console.log(`Parse error at ${error.filePath}:${error.line}`);
* }
* }
*
* // Disable logging for tests
* setLogger(silentLogger);
* ```
*/
/**
* Base error class for all CodeGraph errors.
*
* All CodeGraph-specific errors extend this class, allowing you to catch
* all CodeGraph errors with a single catch block.
*
* @example
* ```typescript
* try {
* await cg.indexAll();
* } catch (error) {
* if (error instanceof CodeGraphError) {
* console.log(`CodeGraph error [${error.code}]: ${error.message}`);
* }
* }
* ```
*/
export class CodeGraphError extends Error {
/** Error code for categorization (e.g., 'FILE_ERROR', 'PARSE_ERROR') */
readonly code: string;
/** Additional context about the error */
readonly context?: Record<string, unknown>;
constructor(message: string, code: string, context?: Record<string, unknown>) {
super(message);
this.name = 'CodeGraphError';
this.code = code;
this.context = context;
// Maintain proper stack trace for V8
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
/**
* Error reading or accessing files
*/
export class FileError extends CodeGraphError {
readonly filePath: string;
constructor(message: string, filePath: string, cause?: Error) {
super(message, 'FILE_ERROR', { filePath, cause: cause?.message });
this.name = 'FileError';
this.filePath = filePath;
if (cause) {
this.cause = cause;
}
}
}
/**
* Error parsing source code
*/
export class ParseError extends CodeGraphError {
readonly filePath: string;
readonly line?: number;
readonly column?: number;
constructor(
message: string,
filePath: string,
options?: { line?: number; column?: number; cause?: Error }
) {
super(message, 'PARSE_ERROR', {
filePath,
line: options?.line,
column: options?.column,
cause: options?.cause?.message,
});
this.name = 'ParseError';
this.filePath = filePath;
this.line = options?.line;
this.column = options?.column;
if (options?.cause) {
this.cause = options.cause;
}
}
}
/**
* Error with database operations
*/
export class DatabaseError extends CodeGraphError {
readonly operation: string;
constructor(message: string, operation: string, cause?: Error) {
super(message, 'DATABASE_ERROR', { operation, cause: cause?.message });
this.name = 'DatabaseError';
this.operation = operation;
if (cause) {
this.cause = cause;
}
}
}
/**
* Error with search operations
*/
export class SearchError extends CodeGraphError {
readonly query: string;
constructor(message: string, query: string, cause?: Error) {
super(message, 'SEARCH_ERROR', { query, cause: cause?.message });
this.name = 'SearchError';
this.query = query;
if (cause) {
this.cause = cause;
}
}
}
/**
* Error with vector/embedding operations
*/
export class VectorError extends CodeGraphError {
constructor(message: string, operation: string, cause?: Error) {
super(message, 'VECTOR_ERROR', { operation, cause: cause?.message });
this.name = 'VectorError';
if (cause) {
this.cause = cause;
}
}
}
/**
* Error with configuration
*/
export class ConfigError extends CodeGraphError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'CONFIG_ERROR', details);
this.name = 'ConfigError';
}
}
/**
* A refused path — the caller asked for something outside the project root, or
* for a sensitive system directory. Deliberately a plain `Error` and NOT a
* {@link CodeGraphError}: it is a security marker every read sink tests with
* `instanceof`, not a categorized operational failure, and the MCP layer treats
* it as one of the only two "stop trying" conditions (see `mcp/tools.ts`).
*
* It lives here — in the dependency-free error module — rather than next to its
* first caller so that a consumer can enforce the refusal WITHOUT importing the
* MCP tool graph. `mcp/tools.ts` re-exports it, so the class identity stays
* single and every existing `instanceof` check keeps working.
*/
export class PathRefusalError extends Error {}
/**
* Simple logger for CodeGraph operations
*
* By default, logs to console.warn for warnings and console.error for errors.
* Can be configured to use custom logging.
*/
export interface Logger {
debug(message: string, context?: Record<string, unknown>): void;
warn(message: string, context?: Record<string, unknown>): void;
error(message: string, context?: Record<string, unknown>): void;
}
/**
* Default console-based logger
*/
export const defaultLogger: Logger = {
debug(message: string, context?: Record<string, unknown>): void {
if (process.env.CODEGRAPH_DEBUG) {
console.debug(`[CodeGraph] ${message}`, context ?? '');
}
},
warn(message: string, context?: Record<string, unknown>): void {
console.warn(`[CodeGraph] ${message}`, context ?? '');
},
error(message: string, context?: Record<string, unknown>): void {
console.error(`[CodeGraph] ${message}`, context ?? '');
},
};
/**
* Silent logger (no output) - useful for tests
*/
export const silentLogger: Logger = {
debug(): void {},
warn(): void {},
error(): void {},
};
/**
* Current logger instance (can be replaced)
*/
let currentLogger: Logger = defaultLogger;
/**
* Set the global logger
*/
export function setLogger(logger: Logger): void {
currentLogger = logger;
}
/**
* Get the current logger
*/
export function getLogger(): Logger {
return currentLogger;
}
/**
* Log a debug message
*/
export function logDebug(message: string, context?: Record<string, unknown>): void {
currentLogger.debug(message, context);
}
/**
* Log a warning message
*/
export function logWarn(message: string, context?: Record<string, unknown>): void {
currentLogger.warn(message, context);
}
/**
* Log an error message
*/
export function logError(message: string, context?: Record<string, unknown>): void {
currentLogger.error(message, context);
}