import { ok, err, type Result } from './types/result' import { type Did, type DidPlc, type DidWeb, type Handle, type EmailAddress, type AtUri, type Cid, type Nsid, type ISODateString, isDid, isDidPlc, isDidWeb, isHandle, isEmail, isAtUri, isCid, isNsid, isISODate, } from './types/branded' export class ValidationError extends Error { constructor( message: string, public readonly field?: string, public readonly value?: unknown ) { super(message) this.name = 'ValidationError' } } export function parseDid(s: string): Result { if (isDid(s)) { return ok(s) } return err(new ValidationError(`Invalid DID: ${s}`, 'did', s)) } export function parseDidPlc(s: string): Result { if (isDidPlc(s)) { return ok(s) } return err(new ValidationError(`Invalid DID:PLC: ${s}`, 'did', s)) } export function parseDidWeb(s: string): Result { if (isDidWeb(s)) { return ok(s) } return err(new ValidationError(`Invalid DID:WEB: ${s}`, 'did', s)) } export function parseHandle(s: string): Result { const trimmed = s.trim().toLowerCase() if (isHandle(trimmed)) { return ok(trimmed) } return err(new ValidationError(`Invalid handle: ${s}`, 'handle', s)) } export function parseEmail(s: string): Result { const trimmed = s.trim().toLowerCase() if (isEmail(trimmed)) { return ok(trimmed) } return err(new ValidationError(`Invalid email: ${s}`, 'email', s)) } export function parseAtUri(s: string): Result { if (isAtUri(s)) { return ok(s) } return err(new ValidationError(`Invalid AT-URI: ${s}`, 'uri', s)) } export function parseCid(s: string): Result { if (isCid(s)) { return ok(s) } return err(new ValidationError(`Invalid CID: ${s}`, 'cid', s)) } export function parseNsid(s: string): Result { if (isNsid(s)) { return ok(s) } return err(new ValidationError(`Invalid NSID: ${s}`, 'nsid', s)) } export function parseISODate(s: string): Result { if (isISODate(s)) { return ok(s) } return err(new ValidationError(`Invalid ISO date: ${s}`, 'date', s)) } export interface PasswordValidationResult { valid: boolean errors: string[] strength: 'weak' | 'fair' | 'good' | 'strong' } export function validatePassword(password: string): PasswordValidationResult { const errors: string[] = [] if (password.length < 8) { errors.push('Password must be at least 8 characters') } if (password.length > 256) { errors.push('Password must be at most 256 characters') } if (!/[a-z]/.test(password)) { errors.push('Password must contain a lowercase letter') } if (!/[A-Z]/.test(password)) { errors.push('Password must contain an uppercase letter') } if (!/\d/.test(password)) { errors.push('Password must contain a number') } let strength: PasswordValidationResult['strength'] = 'weak' if (errors.length === 0) { const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password) const isLong = password.length >= 12 const isVeryLong = password.length >= 16 if (isVeryLong && hasSpecial) { strength = 'strong' } else if (isLong || hasSpecial) { strength = 'good' } else { strength = 'fair' } } return { valid: errors.length === 0, errors, strength, } } export function validateHandle(handle: string): Result { const trimmed = handle.trim().toLowerCase() if (trimmed.length < 3) { return err(new ValidationError('Handle must be at least 3 characters', 'handle', handle)) } if (trimmed.length > 253) { return err(new ValidationError('Handle must be at most 253 characters', 'handle', handle)) } if (!isHandle(trimmed)) { return err(new ValidationError('Invalid handle format', 'handle', handle)) } return ok(trimmed) } export function validateInviteCode(code: string): Result { const trimmed = code.trim() if (trimmed.length === 0) { return err(new ValidationError('Invite code is required', 'inviteCode', code)) } const pattern = /^[a-zA-Z0-9-]+$/ if (!pattern.test(trimmed)) { return err(new ValidationError('Invalid invite code format', 'inviteCode', code)) } return ok(trimmed) } export function validateTotpCode(code: string): Result { const trimmed = code.trim().replace(/\s/g, '') if (!/^\d{6}$/.test(trimmed)) { return err(new ValidationError('TOTP code must be 6 digits', 'code', code)) } return ok(trimmed) } export function validateBackupCode(code: string): Result { const trimmed = code.trim().replace(/\s/g, '').toLowerCase() if (!/^[a-z0-9]{8}$/.test(trimmed)) { return err(new ValidationError('Invalid backup code format', 'code', code)) } return ok(trimmed) } export interface FormValidation { validate: () => Result field: ( key: K, validator: (value: unknown) => Result ) => FormValidation optional: ( key: K, validator: (value: unknown) => Result ) => FormValidation } export function createFormValidation>( data: Record ): FormValidation { const validators: Array<{ key: string validator: (value: unknown) => Result optional: boolean }> = [] const builder: FormValidation = { field: (key, validator) => { validators.push({ key: key as string, validator, optional: false }) return builder }, optional: (key, validator) => { validators.push({ key: key as string, validator, optional: true }) return builder }, validate: () => { const errors: ValidationError[] = [] const result: Record = {} for (const { key, validator, optional } of validators) { const value = data[key] if (value == null || value === '') { if (!optional) { errors.push(new ValidationError(`${key} is required`, key)) } continue } const validated = validator(value) if (validated.ok) { result[key] = validated.value } else { errors.push(validated.error) } } if (errors.length > 0) { return err(errors) } return ok(result as T) }, } return builder }