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}