this repo has no description
at main 6.9 kB view raw
1import { err, ok, type Result } from "./types/result.ts"; 2import { 3 type AtUri, 4 type Cid, 5 type Did, 6 type DidPlc, 7 type DidWeb, 8 type EmailAddress, 9 type Handle, 10 isAtUri, 11 isCid, 12 isDid, 13 isDidPlc, 14 isDidWeb, 15 isEmail, 16 isHandle, 17 isISODate, 18 isNsid, 19 type ISODateString, 20 type Nsid, 21} from "./types/branded.ts"; 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( 93 s: string, 94): Result<ISODateString, ValidationError> { 95 if (isISODate(s)) { 96 return ok(s); 97 } 98 return err(new ValidationError(`Invalid ISO date: ${s}`, "date", s)); 99} 100 101export interface PasswordValidationResult { 102 valid: boolean; 103 errors: string[]; 104 strength: "weak" | "fair" | "good" | "strong"; 105} 106 107export function validatePassword(password: string): PasswordValidationResult { 108 const errors: string[] = []; 109 110 if (password.length < 8) { 111 errors.push("Password must be at least 8 characters"); 112 } 113 if (password.length > 256) { 114 errors.push("Password must be at most 256 characters"); 115 } 116 if (!/[a-z]/.test(password)) { 117 errors.push("Password must contain a lowercase letter"); 118 } 119 if (!/[A-Z]/.test(password)) { 120 errors.push("Password must contain an uppercase letter"); 121 } 122 if (!/\d/.test(password)) { 123 errors.push("Password must contain a number"); 124 } 125 126 let strength: PasswordValidationResult["strength"] = "weak"; 127 if (errors.length === 0) { 128 const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password); 129 const isLong = password.length >= 12; 130 const isVeryLong = password.length >= 16; 131 132 if (isVeryLong && hasSpecial) { 133 strength = "strong"; 134 } else if (isLong || hasSpecial) { 135 strength = "good"; 136 } else { 137 strength = "fair"; 138 } 139 } 140 141 return { 142 valid: errors.length === 0, 143 errors, 144 strength, 145 }; 146} 147 148export function validateHandle( 149 handle: string, 150): Result<Handle, ValidationError> { 151 const trimmed = handle.trim().toLowerCase(); 152 153 if (trimmed.length < 3) { 154 return err( 155 new ValidationError( 156 "Handle must be at least 3 characters", 157 "handle", 158 handle, 159 ), 160 ); 161 } 162 163 if (trimmed.length > 253) { 164 return err( 165 new ValidationError( 166 "Handle must be at most 253 characters", 167 "handle", 168 handle, 169 ), 170 ); 171 } 172 173 if (!isHandle(trimmed)) { 174 return err(new ValidationError("Invalid handle format", "handle", handle)); 175 } 176 177 return ok(trimmed); 178} 179 180export function validateInviteCode( 181 code: string, 182): Result<string, ValidationError> { 183 const trimmed = code.trim(); 184 185 if (trimmed.length === 0) { 186 return err( 187 new ValidationError("Invite code is required", "inviteCode", code), 188 ); 189 } 190 191 const pattern = /^[a-zA-Z0-9-]+$/; 192 if (!pattern.test(trimmed)) { 193 return err( 194 new ValidationError("Invalid invite code format", "inviteCode", code), 195 ); 196 } 197 198 return ok(trimmed); 199} 200 201export function validateTotpCode( 202 code: string, 203): Result<string, ValidationError> { 204 const trimmed = code.trim().replace(/\s/g, ""); 205 206 if (!/^\d{6}$/.test(trimmed)) { 207 return err(new ValidationError("TOTP code must be 6 digits", "code", code)); 208 } 209 210 return ok(trimmed); 211} 212 213export function validateBackupCode( 214 code: string, 215): Result<string, ValidationError> { 216 const trimmed = code.trim().replace(/\s/g, "").toLowerCase(); 217 218 if (!/^[a-z0-9]{8}$/.test(trimmed)) { 219 return err(new ValidationError("Invalid backup code format", "code", code)); 220 } 221 222 return ok(trimmed); 223} 224 225export interface FormValidation<T> { 226 validate: () => Result<T, ValidationError[]>; 227 field: <K extends keyof T>( 228 key: K, 229 validator: (value: unknown) => Result<T[K], ValidationError>, 230 ) => FormValidation<T>; 231 optional: <K extends keyof T>( 232 key: K, 233 validator: (value: unknown) => Result<T[K], ValidationError>, 234 ) => FormValidation<T>; 235} 236 237export function createFormValidation<T extends Record<string, unknown>>( 238 data: Record<string, unknown>, 239): FormValidation<T> { 240 const validators: Array<{ 241 key: string; 242 validator: (value: unknown) => Result<unknown, ValidationError>; 243 optional: boolean; 244 }> = []; 245 246 const builder: FormValidation<T> = { 247 field: (key, validator) => { 248 validators.push({ key: key as string, validator, optional: false }); 249 return builder; 250 }, 251 optional: (key, validator) => { 252 validators.push({ key: key as string, validator, optional: true }); 253 return builder; 254 }, 255 validate: () => { 256 const errors: ValidationError[] = []; 257 const result: Record<string, unknown> = {}; 258 259 for (const { key, validator, optional } of validators) { 260 const value = data[key]; 261 262 if (value == null || value === "") { 263 if (!optional) { 264 errors.push(new ValidationError(`${key} is required`, key)); 265 } 266 continue; 267 } 268 269 const validated = validator(value); 270 if (validated.ok) { 271 result[key] = validated.value; 272 } else { 273 errors.push(validated.error); 274 } 275 } 276 277 if (errors.length > 0) { 278 return err(errors); 279 } 280 281 return ok(result as T); 282 }, 283 }; 284 285 return builder; 286}