|
|
@@ -0,0 +1,122 @@
|
|
|
+/**
|
|
|
+ * Application error types and utilities.
|
|
|
+ *
|
|
|
+ * All domain errors extend `AppError`, which carries a machine-readable `code`,
|
|
|
+ * an HTTP `statusCode`, and an optional `details` payload for structured context.
|
|
|
+ *
|
|
|
+ * Infrastructure errors (K8s, Gogs) should always be wrapped via `wrapInfraError`
|
|
|
+ * so that internal details are logged but never leaked to clients.
|
|
|
+ */
|
|
|
+
|
|
|
+// --- Base ---
|
|
|
+
|
|
|
+class AppError extends Error {
|
|
|
+ readonly code: string;
|
|
|
+ readonly statusCode: number;
|
|
|
+ readonly details?: Record<string, unknown>;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ code: string,
|
|
|
+ message: string,
|
|
|
+ statusCode: number,
|
|
|
+ details?: Record<string, unknown>,
|
|
|
+ ) {
|
|
|
+ super(message);
|
|
|
+ this.name = 'AppError';
|
|
|
+ this.code = code;
|
|
|
+ this.statusCode = statusCode;
|
|
|
+ this.details = details;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class NotFoundError extends AppError {
|
|
|
+ constructor(resource: string, id?: string) {
|
|
|
+ super('NOT_FOUND', id ? `${resource} '${id}' not found` : `${resource} not found`, 404);
|
|
|
+ this.name = 'NotFoundError';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class UnauthorizedError extends AppError {
|
|
|
+ constructor(message = 'Authentication required') {
|
|
|
+ super('UNAUTHORIZED', message, 401);
|
|
|
+ this.name = 'UnauthorizedError';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ForbiddenError extends AppError {
|
|
|
+ constructor(message = 'Insufficient permissions') {
|
|
|
+ super('FORBIDDEN', message, 403);
|
|
|
+ this.name = 'ForbiddenError';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ValidationError extends AppError {
|
|
|
+ constructor(message: string, fields?: Record<string, string>) {
|
|
|
+ super('VALIDATION_ERROR', message, 400, fields ? { fields } : undefined);
|
|
|
+ this.name = 'ValidationError';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ConflictError extends AppError {
|
|
|
+ constructor(resource: string, id: string) {
|
|
|
+ super('CONFLICT', `${resource} '${id}' already exists`, 409);
|
|
|
+ this.name = 'ConflictError';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Wraps a raw error from an external system (K8s, Gogs) into an AppError.
|
|
|
+ * The original error is preserved as `cause` for logging but the exposed
|
|
|
+ * message is sanitized to avoid leaking internal details to clients.
|
|
|
+ */
|
|
|
+class InfrastructureError extends AppError {
|
|
|
+ constructor(system: string, operation: string, cause: unknown) {
|
|
|
+ super('INFRASTRUCTURE_ERROR', `${system} error during ${operation}`, 502, {
|
|
|
+ system,
|
|
|
+ operation,
|
|
|
+ });
|
|
|
+ this.name = 'InfrastructureError';
|
|
|
+ this.cause = cause;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Thrown during server init when a required config key is absent.
|
|
|
+ * Not an HTTP error — causes the process to exit (OrDie behavior).
|
|
|
+ */
|
|
|
+class ConfigError extends Error {
|
|
|
+ readonly key: string;
|
|
|
+
|
|
|
+ constructor(key: string) {
|
|
|
+ super(`Required config key '${key}' is not set`);
|
|
|
+ this.name = 'ConfigError';
|
|
|
+ this.key = key;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Throw a ConfigError if the value is absent. Use in Server.init() for required config. */
|
|
|
+const requireConfig = (key: string, value: string | undefined): string => {
|
|
|
+ if (!value) throw new ConfigError(key);
|
|
|
+ return value;
|
|
|
+};
|
|
|
+
|
|
|
+/** Wrap an unknown infrastructure error, preserving it as `cause`. */
|
|
|
+const wrapInfraError = (system: string, operation: string, cause: unknown): InfrastructureError =>
|
|
|
+ new InfrastructureError(system, operation, cause);
|
|
|
+
|
|
|
+/** Type guard — true if `err` is an AppError (and subclasses). */
|
|
|
+const isAppError = (err: unknown): err is AppError => err instanceof AppError;
|
|
|
+
|
|
|
+export type { AppError };
|
|
|
+export {
|
|
|
+ ConfigError,
|
|
|
+ ConflictError,
|
|
|
+ ForbiddenError,
|
|
|
+ InfrastructureError,
|
|
|
+ isAppError,
|
|
|
+ NotFoundError,
|
|
|
+ requireConfig,
|
|
|
+ UnauthorizedError,
|
|
|
+ ValidationError,
|
|
|
+ wrapInfraError,
|
|
|
+};
|