/** * Name validation for projects and repositories. * Rules: * 1. All English letters (stored as lowercase) * 2. Cannot start with a number * 3. Only letters, numbers, hyphens, and underscores * 4. Cannot conflict with reserved route names */ export const RESERVED_ROUTE_NAMES = new Set([ 'auth', 'init', 'user', 'settings', 'project', 'explore', 'market', 'docs', 'notify', 'api', 'admin', 'dashboard', 'login', 'register', 'logout', 'oauth', 'webhook', 'static', ]); /** Regex: starts with letter, then letters/numbers/hyphens/underscores */ export const NAME_PATTERN = /^[a-z][a-z0-9_-]*$/; /** Minimum length */ export const MIN_NAME_LENGTH = 2; /** Maximum length */ export const MAX_NAME_LENGTH = 100; export type NameValidationResult = | { valid: true; message: '' } | { valid: false; message: string }; /** * Validates a project or repository name. * Pass the raw input (before any transformation). */ export function validateName(raw: string): NameValidationResult { const trimmed = raw.trim(); if (trimmed.length < MIN_NAME_LENGTH) { return { valid: false, message: `Must be at least ${MIN_NAME_LENGTH} characters` }; } if (trimmed.length > MAX_NAME_LENGTH) { return { valid: false, message: `Must not exceed ${MAX_NAME_LENGTH} characters` }; } const lower = trimmed.toLowerCase(); if (!NAME_PATTERN.test(lower)) { return { valid: false, message: 'Must start with a letter and contain only letters, numbers, hyphens, and underscores', }; } if (RESERVED_ROUTE_NAMES.has(lower)) { return { valid: false, message: `"${lower}" is a reserved name and cannot be used` }; } return { valid: true, message: '' }; }