Add EpicNext CMS foundation layer and fix critical security gaps

- Create src/lib/foundation/ (860 LOC, 9 files): typed action wrappers,
  DbService with health checks, CSRF validation, safe redirects,
  AsyncLocalStorage request tracing, branded types, reusable Zod schemas
- Migrate moderation.ts and user-settings.ts to foundation patterns
- Fix abuse-guard.ts: bound in-memory Maps with LRU eviction (was unbounded)
- Fix access-guard.ts: separate try/catch per check, log degradation
  instead of blanket fail-open
- Replace raw redirect() calls with safeRedirect() in guard.ts and
  permissions.ts to prevent open-redirect attacks
- Add CSRF validation to api-handler.ts for mutating methods
- Add canonicalizeFormData() utility for FormData input sanitization
This commit is contained in:
openhands committed 2026-07-13 12:03:49 +02:00
1 parent 8bf1aa2fa7
commit f6ad030c5b
16 files changed
+1012 -96

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
export class DomainError extends Error {
public readonly status: number;
constructor(message: string, status: number = 500) {
super(message);
this.name = "DomainError";
this.status = status;
}
}
export class NotFoundError extends DomainError {
constructor(entity: string, id?: number | string) {
super(id ? `${entity} #${id} not found` : `${entity} not found`, 404);
this.name = "NotFoundError";
}
}
export class UnauthorizedError extends DomainError {
constructor(message: string = "Unauthorized") {
super(message, 401);
this.name = "UnauthorizedError";
}
}
export class ForbiddenError extends DomainError {
constructor(message: string = "Forbidden") {
super(message, 403);
this.name = "ForbiddenError";
}
}
export class ValidationError extends DomainError {
public readonly fieldErrors: Record<string, string[]>;
constructor(fieldErrors: Record<string, string[]>) {
super("Validation failed", 422);
this.name = "ValidationError";
this.fieldErrors = fieldErrors;
}
}
export class RateLimitError extends DomainError {
public readonly retryAfter: number;
constructor(retryAfter: number) {
super(`Rate limited. Try again in ${retryAfter}s.`, 429);
this.name = "RateLimitError";
this.retryAfter = retryAfter;
}
}
export class ConflictError extends DomainError {
constructor(message: string) {
super(message, 409);
this.name = "ConflictError";
}
}
export class DatabaseError extends DomainError {
public readonly cause: unknown;
constructor(message: string, cause?: unknown) {
super(message, 500);
this.name = "DatabaseError";
this.cause = cause;
}
}