← vibesdk
/**
* Centralized Validation Utilities
*/
import type { PasswordValidationResult } from '../types/auth-types';
import { z } from 'zod';
/**
* Email validation configuration
*/
export interface EmailValidationConfig {
allowPlusAddressing?: boolean; // Allow email+tag@domain.com
allowInternational?: boolean; // Allow international domains
maxLength?: number; // Maximum email length
blockedDomains?: string[]; // Blocked email domains
}
/**
* Default email validation configuration
*/
const DEFAULT_EMAIL_CONFIG: EmailValidationConfig = {
allowPlusAddressing: true,
allowInternational: true,
maxLength: 254, // RFC 5321 limit
blockedDomains: ['10minutemail.com', 'tempmail.org'], // Add known temp email domains
};
/**
* Comprehensive email validation
*/
export function validateEmail(
email: string,
config: EmailValidationConfig = DEFAULT_EMAIL_CONFIG,
): { valid: boolean; error?: string } {
if (!email || typeof email !== 'string') {
return { valid: false, error: 'Email is required' };
}
// Length check
const maxLength = config.maxLength || DEFAULT_EMAIL_CONFIG.maxLength!;
if (email.length > maxLength) {
return {
valid: false,
error: `Email must be less than ${maxLength} characters`,
};
}
// Basic format validation
const emailRegex = config.allowInternational
? /^[^\s@]+@[^\s@]+\.[^\s@]+$/ // Basic international-friendly regex
: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; // ASCII only
if (!emailRegex.test(email)) {
return { valid: false, error: 'Invalid email format' };
}
// Domain validation
const domain = email.split('@')[1]?.toLowerCase();
if (config.blockedDomains?.includes(domain)) {
return { valid: false, error: 'Email domain is not allowed' };
}
// Plus addressing check (if disabled)
if (!config.allowPlusAddressing && email.includes('+')) {
return { valid: false, error: 'Plus addressing is not allowed' };
}
return { valid: true };
}
/**
* Zod schema for password validation
*/
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(128, 'Password must be less than 128 characters')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[0-9]/, 'Password must contain at least one number');
/**
* Password validation using Zod
*/
export function validatePassword(
password: string,
_config?: unknown,
_userInfo?: { email?: string; username?: string; name?: string },
): PasswordValidationResult {
if (!password || typeof password !== 'string') {
return {
valid: false,
errors: ['Password is required'],
score: 0,
requirements: {
minLength: false,
hasLowercase: false,
hasUppercase: false,
hasNumbers: false,
hasSpecialChars: false,
notCommon: false,
noSequential: false,
},
};
}
const result = passwordSchema.safeParse(password);
const requirements = {
minLength: password.length >= 8,
hasLowercase: /[a-z]/.test(password),
hasUppercase: /[A-Z]/.test(password),
hasNumbers: /[0-9]/.test(password),
hasSpecialChars: /[^a-zA-Z0-9]/.test(password),
notCommon: true,
noSequential: true,
};
// Calculate score based on strength
let score = 0;
if (requirements.minLength) score++;
if (requirements.hasLowercase && requirements.hasUppercase) score++;
if (requirements.hasNumbers) score++;
if (requirements.hasSpecialChars) score++;
if (password.length >= 12) score = Math.min(4, score + 1);
// Generate suggestions
const suggestions: string[] = [];
if (password.length < 12) {
suggestions.push('Use at least 12 characters for better security');
}
if (!requirements.hasSpecialChars) {
suggestions.push('Add special characters for enhanced security');
}
if (!result.success) {
return {
valid: false,
errors: result.error.issues.map(e => e.message),
score,
requirements,
suggestions: suggestions.length > 0 ? suggestions : undefined,
};
}
return {
valid: true,
score,
requirements,
suggestions: suggestions.length > 0 ? suggestions : undefined,
};
}
/**
* Validate username format
*/
export function validateUsername(
username: string,
config?: {
minLength?: number;
maxLength?: number;
allowSpecialChars?: boolean;
reservedNames?: string[];
},
): { valid: boolean; error?: string } {
const {
minLength = 3,
maxLength = 30,
allowSpecialChars = false,
reservedNames = ['admin', 'root', 'api', 'www', 'mail', 'support'],
} = config || {};
if (!username || typeof username !== 'string') {
return { valid: false, error: 'Username is required' };
}
if (username.length < minLength) {
return {
valid: false,
error: `Username must be at least ${minLength} characters`,
};
}
if (username.length > maxLength) {
return {
valid: false,
error: `Username must be less than ${maxLength} characters`,
};
}
// Format validation
const validPattern = allowSpecialChars
? /^[a-zA-Z0-9_.-]+$/
: /^[a-zA-Z0-9_]+$/;
if (!validPattern.test(username)) {
return {
valid: false,
error: allowSpecialChars
? 'Username can only contain letters, numbers, underscores, dots, and hyphens'
: 'Username can only contain letters, numbers, and underscores',
};
}
// Reserved names check
if (reservedNames.includes(username.toLowerCase())) {
return { valid: false, error: 'Username is reserved' };
}
// Must start with letter or number
if (!/^[a-zA-Z0-9]/.test(username)) {
return {
valid: false,
error: 'Username must start with a letter or number',
};
}
return { valid: true };
}
/**
* Batch validation utility
*/
export interface ValidationField<
T extends readonly unknown[] = readonly unknown[],
> {
value: string;
validator: (
value: string,
...args: T
) => { valid: boolean; error?: string };
validatorArgs?: T;
fieldName: string;
}