import { eq } from 'drizzle-orm'; import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'; import { webRoutes } from '../db/schema'; const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; export interface RegisterRouteRequest { slug: string; type: 'static' | 'reverse_proxy'; path: string; targetHost?: string; targetPort?: number; websocket?: boolean; } export interface UploadStaticAssetsRequest { slug: string; sourceDir: string; } export interface ValidationResult { valid: boolean; errors: string[]; } export function validateSlug(slug: string): ValidationResult { const errors: string[] = []; if (!slug) { errors.push('Slug is required'); } else if (!SLUG_PATTERN.test(slug)) { errors.push( `Slug "${slug}" must be kebab-case (lowercase letters, numbers, hyphens between segments)`, ); } return { valid: errors.length === 0, errors }; } export function validatePath(path: string): ValidationResult { const errors: string[] = []; if (!path) { errors.push('Path is required'); } else { if (!path.startsWith('/')) { errors.push('Path must start with /'); } if (path.length > 1 && path.endsWith('/')) { errors.push('Path must not have a trailing slash'); } if (path.includes('//')) { errors.push('Path must not contain double slashes'); } } return { valid: errors.length === 0, errors }; } export function validateRouteRequest(request: RegisterRouteRequest): ValidationResult { const errors: string[] = []; const slugResult = validateSlug(request.slug); errors.push(...slugResult.errors); const pathResult = validatePath(request.path); errors.push(...pathResult.errors); if (request.type === 'reverse_proxy') { if (!request.targetHost) { errors.push('reverse_proxy route requires targetHost'); } if (request.targetPort === undefined || request.targetPort === null) { errors.push('reverse_proxy route requires targetPort'); } else if (request.targetPort < 1 || request.targetPort > 65535) { errors.push('targetPort must be between 1 and 65535'); } } return { valid: errors.length === 0, errors }; } export async function checkPathUniqueness( path: string, db: BunSQLiteDatabase, excludeModuleId?: string, ): Promise<{ unique: boolean; conflictingModule?: string }> { const existing = await db.select().from(webRoutes).where(eq(webRoutes.path, path)); if (existing.length === 0) { return { unique: true }; } const conflict = existing[0]; if (excludeModuleId && conflict.moduleId === excludeModuleId) { return { unique: true }; } return { unique: false, conflictingModule: conflict.moduleId }; }