this repo has no description
1import { ok, err, type Result } from './types/result' 2import { 3 type Did, 4 type DidPlc, 5 type DidWeb, 6 type Handle, 7 type EmailAddress, 8 type AtUri, 9 type Cid, 10 type Nsid, 11 type ISODateString, 12 isDid, 13 isDidPlc, 14 isDidWeb, 15 isHandle, 16 isEmail, 17 isAtUri, 18 isCid, 19 isNsid, 20 isISODate, 21} from './types/branded' 22 23export class ValidationError extends Error { 24 constructor( 25 message: string, 26 public readonly field?: string, 27 public readonly value?: unknown 28 ) { 29 super(message) 30 this.name = 'ValidationError' 31 } 32} 33 34export function parseDid(s: string): Result<Did, ValidationError> { 35 if (isDid(s)) { 36 return ok(s) 37 } 38 return err(new ValidationError(`Invalid DID: ${s}`, 'did', s)) 39} 40 41export function parseDidPlc(s: string): Result<DidPlc, ValidationError> { 42 if (isDidPlc(s)) { 43 return ok(s) 44 } 45 return err(new ValidationError(`Invalid DID:PLC: ${s}`, 'did', s)) 46} 47 48export function parseDidWeb(s: string): Result<DidWeb, ValidationError> { 49 if (isDidWeb(s)) { 50 return ok(s) 51 } 52 return err(new ValidationError(`Invalid DID:WEB: ${s}`, 'did', s)) 53} 54 55export function parseHandle(s: string): Result<Handle, ValidationError> { 56 const trimmed = s.trim().toLowerCase() 57 if (isHandle(trimmed)) { 58 return ok(trimmed) 59 } 60 return err(new ValidationError(`Invalid handle: ${s}`, 'handle', s)) 61} 62 63export function parseEmail(s: string): Result<EmailAddress, ValidationError> { 64 const trimmed = s.trim().toLowerCase() 65 if (isEmail(trimmed)) { 66 return ok(trimmed) 67 } 68 return err(new ValidationError(`Invalid email: ${s}`, 'email', s)) 69} 70 71export function parseAtUri(s: string): Result<AtUri, ValidationError> { 72 if (isAtUri(s)) { 73 return ok(s) 74 } 75 return err(new ValidationError(`Invalid AT-URI: ${s}`, 'uri', s)) 76} 77 78export function parseCid(s: string): Result<Cid, ValidationError> { 79 if (isCid(s)) { 80 return ok(s) 81 } 82 return err(new ValidationError(`Invalid CID: ${s}`, 'cid', s)) 83} 84 85export function parseNsid(s: string): Result<Nsid, ValidationError> { 86 if (isNsid(s)) { 87 return ok(s) 88 } 89 return err(new ValidationError(`Invalid NSID: ${s}`, 'nsid', s)) 90} 91 92export function parseISODate(s: string): Result<ISODateString, ValidationError> { 93 if (isISODate(s)) { 94 return ok(s) 95 } 96 return err(new ValidationError(`Invalid ISO date: ${s}`, 'date', s)) 97} 98 99export interface PasswordValidationResult { 100 valid: boolean 101 errors: string[] 102 strength: 'weak' | 'fair' | 'good' | 'strong' 103} 104 105export function validatePassword(password: string): PasswordValidationResult { 106 const errors: string[] = [] 107 108 if (password.length < 8) { 109 errors.push('Password must be at least 8 characters') 110 } 111 if (password.length > 256) { 112 errors.push('Password must be at most 256 characters') 113 } 114 if (!/[a-z]/.test(password)) { 115 errors.push('Password must contain a lowercase letter') 116 } 117 if (!/[A-Z]/.test(password)) { 118 errors.push('Password must contain an uppercase letter') 119 } 120 if (!/\d/.test(password)) { 121 errors.push('Password must contain a number') 122 } 123 124 let strength: PasswordValidationResult['strength'] = 'weak' 125 if (errors.length === 0) { 126 const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password) 127 const isLong = password.length >= 12 128 const isVeryLong = password.length >= 16 129 130 if (isVeryLong && hasSpecial) { 131 strength = 'strong' 132 } else if (isLong || hasSpecial) { 133 strength = 'good' 134 } else { 135 strength = 'fair' 136 } 137 } 138 139 return { 140 valid: errors.length === 0, 141 errors, 142 strength, 143 } 144} 145 146export function validateHandle(handle: string): Result<Handle, ValidationError> { 147 const trimmed = handle.trim().toLowerCase() 148 149 if (trimmed.length < 3) { 150 return err(new ValidationError('Handle must be at least 3 characters', 'handle', handle)) 151 } 152 153 if (trimmed.length > 253) { 154 return err(new ValidationError('Handle must be at most 253 characters', 'handle', handle)) 155 } 156 157 if (!isHandle(trimmed)) { 158 return err(new ValidationError('Invalid handle format', 'handle', handle)) 159 } 160 161 return ok(trimmed) 162} 163 164export function validateInviteCode(code: string): Result<string, ValidationError> { 165 const trimmed = code.trim() 166 167 if (trimmed.length === 0) { 168 return err(new ValidationError('Invite code is required', 'inviteCode', code)) 169 } 170 171 const pattern = /^[a-zA-Z0-9-]+$/ 172 if (!pattern.test(trimmed)) { 173 return err(new ValidationError('Invalid invite code format', 'inviteCode', code)) 174 } 175 176 return ok(trimmed) 177} 178 179export function validateTotpCode(code: string): Result<string, ValidationError> { 180 const trimmed = code.trim().replace(/\s/g, '') 181 182 if (!/^\d{6}$/.test(trimmed)) { 183 return err(new ValidationError('TOTP code must be 6 digits', 'code', code)) 184 } 185 186 return ok(trimmed) 187} 188 189export function validateBackupCode(code: string): Result<string, ValidationError> { 190 const trimmed = code.trim().replace(/\s/g, '').toLowerCase() 191 192 if (!/^[a-z0-9]{8}$/.test(trimmed)) { 193 return err(new ValidationError('Invalid backup code format', 'code', code)) 194 } 195 196 return ok(trimmed) 197} 198 199export interface FormValidation<T> { 200 validate: () => Result<T, ValidationError[]> 201 field: <K extends keyof T>( 202 key: K, 203 validator: (value: unknown) => Result<T[K], ValidationError> 204 ) => FormValidation<T> 205 optional: <K extends keyof T>( 206 key: K, 207 validator: (value: unknown) => Result<T[K], ValidationError> 208 ) => FormValidation<T> 209} 210 211export function createFormValidation<T extends Record<string, unknown>>( 212 data: Record<string, unknown> 213): FormValidation<T> { 214 const validators: Array<{ 215 key: string 216 validator: (value: unknown) => Result<unknown, ValidationError> 217 optional: boolean 218 }> = [] 219 220 const builder: FormValidation<T> = { 221 field: (key, validator) => { 222 validators.push({ key: key as string, validator, optional: false }) 223 return builder 224 }, 225 optional: (key, validator) => { 226 validators.push({ key: key as string, validator, optional: true }) 227 return builder 228 }, 229 validate: () => { 230 const errors: ValidationError[] = [] 231 const result: Record<string, unknown> = {} 232 233 for (const { key, validator, optional } of validators) { 234 const value = data[key] 235 236 if (value == null || value === '') { 237 if (!optional) { 238 errors.push(new ValidationError(`${key} is required`, key)) 239 } 240 continue 241 } 242 243 const validated = validator(value) 244 if (validated.ok) { 245 result[key] = validated.value 246 } else { 247 errors.push(validated.error) 248 } 249 } 250 251 if (errors.length > 0) { 252 return err(errors) 253 } 254 255 return ok(result as T) 256 }, 257 } 258 259 return builder 260}