ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

replace custom validation with Zod

byarielm.fyi 88177fe1 c71e2f56

verified
Changed files
+180 -96
src
+113 -4
package-lock.json
··· 25 "jszip": "^3.10.1", 26 "lucide-react": "^0.544.0", 27 "react": "^18.3.1", 28 - "react-dom": "^18.3.1" 29 }, 30 "devDependencies": { 31 "@types/jszip": "^3.4.0", ··· 112 "zod": "^3.23.8" 113 } 114 }, 115 "node_modules/@atproto-labs/fetch": { 116 "version": "0.2.3", 117 "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", ··· 162 "node": ">=18.7.0" 163 } 164 }, 165 "node_modules/@atproto-labs/identity-resolver": { 166 "version": "0.3.1", 167 "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.1.tgz", ··· 216 "zod": "^3.23.8" 217 } 218 }, 219 "node_modules/@atproto/common-web": { 220 "version": "0.4.3", 221 "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", ··· 226 "multiformats": "^9.9.0", 227 "uint8arrays": "3.0.0", 228 "zod": "^3.23.8" 229 } 230 }, 231 "node_modules/@atproto/crypto": { ··· 251 "zod": "^3.23.8" 252 } 253 }, 254 "node_modules/@atproto/identity": { 255 "version": "0.4.9", 256 "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.9.tgz", ··· 304 "zod": "^3.23.8" 305 } 306 }, 307 "node_modules/@atproto/lexicon": { 308 "version": "0.5.1", 309 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", ··· 315 "iso-datestring-validator": "^2.2.2", 316 "multiformats": "^9.9.0", 317 "zod": "^3.23.8" 318 } 319 }, 320 "node_modules/@atproto/oauth-client": { ··· 357 "node": ">=18.7.0" 358 } 359 }, 360 "node_modules/@atproto/oauth-types": { 361 "version": "0.4.1", 362 "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.1.tgz", ··· 367 "zod": "^3.23.8" 368 } 369 }, 370 "node_modules/@atproto/syntax": { 371 "version": "0.4.1", 372 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", ··· 381 "dependencies": { 382 "@atproto/lexicon": "^0.5.1", 383 "zod": "^3.23.8" 384 } 385 }, 386 "node_modules/@babel/code-frame": { ··· 1996 }, 1997 "engines": { 1998 "node": ">=10" 1999 } 2000 }, 2001 "node_modules/@noble/curves": { ··· 8164 } 8165 }, 8166 "node_modules/zod": { 8167 - "version": "3.25.76", 8168 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 8169 - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 8170 "license": "MIT", 8171 "funding": { 8172 "url": "https://github.com/sponsors/colinhacks"
··· 25 "jszip": "^3.10.1", 26 "lucide-react": "^0.544.0", 27 "react": "^18.3.1", 28 + "react-dom": "^18.3.1", 29 + "zod": "^4.2.1" 30 }, 31 "devDependencies": { 32 "@types/jszip": "^3.4.0", ··· 113 "zod": "^3.23.8" 114 } 115 }, 116 + "node_modules/@atproto-labs/did-resolver/node_modules/zod": { 117 + "version": "3.25.76", 118 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 119 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 120 + "license": "MIT", 121 + "funding": { 122 + "url": "https://github.com/sponsors/colinhacks" 123 + } 124 + }, 125 "node_modules/@atproto-labs/fetch": { 126 "version": "0.2.3", 127 "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", ··· 172 "node": ">=18.7.0" 173 } 174 }, 175 + "node_modules/@atproto-labs/handle-resolver/node_modules/zod": { 176 + "version": "3.25.76", 177 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 178 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 179 + "license": "MIT", 180 + "funding": { 181 + "url": "https://github.com/sponsors/colinhacks" 182 + } 183 + }, 184 "node_modules/@atproto-labs/identity-resolver": { 185 "version": "0.3.1", 186 "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.1.tgz", ··· 235 "zod": "^3.23.8" 236 } 237 }, 238 + "node_modules/@atproto/api/node_modules/zod": { 239 + "version": "3.25.76", 240 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 241 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 242 + "license": "MIT", 243 + "funding": { 244 + "url": "https://github.com/sponsors/colinhacks" 245 + } 246 + }, 247 "node_modules/@atproto/common-web": { 248 "version": "0.4.3", 249 "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", ··· 254 "multiformats": "^9.9.0", 255 "uint8arrays": "3.0.0", 256 "zod": "^3.23.8" 257 + } 258 + }, 259 + "node_modules/@atproto/common-web/node_modules/zod": { 260 + "version": "3.25.76", 261 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 262 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 263 + "license": "MIT", 264 + "funding": { 265 + "url": "https://github.com/sponsors/colinhacks" 266 } 267 }, 268 "node_modules/@atproto/crypto": { ··· 288 "zod": "^3.23.8" 289 } 290 }, 291 + "node_modules/@atproto/did/node_modules/zod": { 292 + "version": "3.25.76", 293 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 294 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 295 + "license": "MIT", 296 + "funding": { 297 + "url": "https://github.com/sponsors/colinhacks" 298 + } 299 + }, 300 "node_modules/@atproto/identity": { 301 "version": "0.4.9", 302 "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.9.tgz", ··· 350 "zod": "^3.23.8" 351 } 352 }, 353 + "node_modules/@atproto/jwk-webcrypto/node_modules/zod": { 354 + "version": "3.25.76", 355 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 356 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 357 + "license": "MIT", 358 + "funding": { 359 + "url": "https://github.com/sponsors/colinhacks" 360 + } 361 + }, 362 + "node_modules/@atproto/jwk/node_modules/zod": { 363 + "version": "3.25.76", 364 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 365 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 366 + "license": "MIT", 367 + "funding": { 368 + "url": "https://github.com/sponsors/colinhacks" 369 + } 370 + }, 371 "node_modules/@atproto/lexicon": { 372 "version": "0.5.1", 373 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", ··· 379 "iso-datestring-validator": "^2.2.2", 380 "multiformats": "^9.9.0", 381 "zod": "^3.23.8" 382 + } 383 + }, 384 + "node_modules/@atproto/lexicon/node_modules/zod": { 385 + "version": "3.25.76", 386 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 387 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 388 + "license": "MIT", 389 + "funding": { 390 + "url": "https://github.com/sponsors/colinhacks" 391 } 392 }, 393 "node_modules/@atproto/oauth-client": { ··· 430 "node": ">=18.7.0" 431 } 432 }, 433 + "node_modules/@atproto/oauth-client/node_modules/zod": { 434 + "version": "3.25.76", 435 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 436 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 437 + "license": "MIT", 438 + "funding": { 439 + "url": "https://github.com/sponsors/colinhacks" 440 + } 441 + }, 442 "node_modules/@atproto/oauth-types": { 443 "version": "0.4.1", 444 "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.1.tgz", ··· 449 "zod": "^3.23.8" 450 } 451 }, 452 + "node_modules/@atproto/oauth-types/node_modules/zod": { 453 + "version": "3.25.76", 454 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 455 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 456 + "license": "MIT", 457 + "funding": { 458 + "url": "https://github.com/sponsors/colinhacks" 459 + } 460 + }, 461 "node_modules/@atproto/syntax": { 462 "version": "0.4.1", 463 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", ··· 472 "dependencies": { 473 "@atproto/lexicon": "^0.5.1", 474 "zod": "^3.23.8" 475 + } 476 + }, 477 + "node_modules/@atproto/xrpc/node_modules/zod": { 478 + "version": "3.25.76", 479 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 480 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 481 + "license": "MIT", 482 + "funding": { 483 + "url": "https://github.com/sponsors/colinhacks" 484 } 485 }, 486 "node_modules/@babel/code-frame": { ··· 2096 }, 2097 "engines": { 2098 "node": ">=10" 2099 + } 2100 + }, 2101 + "node_modules/@netlify/zip-it-and-ship-it/node_modules/zod": { 2102 + "version": "3.25.76", 2103 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 2104 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 2105 + "license": "MIT", 2106 + "funding": { 2107 + "url": "https://github.com/sponsors/colinhacks" 2108 } 2109 }, 2110 "node_modules/@noble/curves": { ··· 8273 } 8274 }, 8275 "node_modules/zod": { 8276 + "version": "4.2.1", 8277 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", 8278 + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", 8279 "license": "MIT", 8280 "funding": { 8281 "url": "https://github.com/sponsors/colinhacks"
+2 -1
package.json
··· 30 "jszip": "^3.10.1", 31 "lucide-react": "^0.544.0", 32 "react": "^18.3.1", 33 - "react-dom": "^18.3.1" 34 }, 35 "devDependencies": { 36 "@types/jszip": "^3.4.0",
··· 30 "jszip": "^3.10.1", 31 "lucide-react": "^0.544.0", 32 "react": "^18.3.1", 33 + "react-dom": "^18.3.1", 34 + "zod": "^4.2.1" 35 }, 36 "devDependencies": { 37 "@types/jszip": "^3.4.0",
+65 -91
src/lib/validation.ts
··· 1 /** 2 - * Validation utilities for forms 3 */ 4 5 export interface ValidationResult { 6 isValid: boolean; ··· 8 } 9 10 /** 11 - * Validate AT Protocol handle 12 */ 13 - export function validateHandle(handle: string): ValidationResult { 14 - const trimmed = handle.trim(); 15 - 16 - if (!trimmed) { 17 - return { 18 - isValid: false, 19 - error: "Please enter your handle", 20 - }; 21 - } 22 - 23 - // Remove @ if user included it 24 - const cleanHandle = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed; 25 - 26 - // Basic format validation 27 - if (cleanHandle.length < 3) { 28 - return { 29 - isValid: false, 30 - error: "Handle is too short", 31 - }; 32 - } 33 - 34 - // Check for valid characters (alphanumeric, dots, hyphens) 35 - const validFormat = /^[a-zA-Z0-9.-]+$/; 36 - if (!validFormat.test(cleanHandle)) { 37 - return { 38 - isValid: false, 39 - error: "Handle contains invalid characters", 40 - }; 41 } 42 43 - // Must contain at least one dot (domain required) 44 - if (!cleanHandle.includes(".")) { 45 - return { 46 - isValid: false, 47 - error: "Handle must include a domain (e.g., username.bsky.social)", 48 - }; 49 - } 50 51 - // Can't start or end with dot or hyphen 52 - if (/^[.-]|[.-]$/.test(cleanHandle)) { 53 - return { 54 - isValid: false, 55 - error: "Handle cannot start or end with . or -", 56 - }; 57 - } 58 59 - return { isValid: true }; 60 } 61 62 /** 63 * Validate email format 64 */ 65 export function validateEmail(email: string): ValidationResult { 66 - const trimmed = email.trim(); 67 - 68 - if (!trimmed) { 69 - return { 70 - isValid: false, 71 - error: "Please enter your email", 72 - }; 73 - } 74 - 75 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 76 - if (!emailRegex.test(trimmed)) { 77 - return { 78 - isValid: false, 79 - error: "Please enter a valid email address", 80 - }; 81 - } 82 - 83 - return { isValid: true }; 84 } 85 86 /** ··· 90 value: string, 91 fieldName: string = "This field", 92 ): ValidationResult { 93 - const trimmed = value.trim(); 94 - 95 - if (!trimmed) { 96 - return { 97 - isValid: false, 98 - error: `${fieldName} is required`, 99 - }; 100 - } 101 - 102 - return { isValid: true }; 103 } 104 105 /** ··· 110 minLength: number, 111 fieldName: string = "This field", 112 ): ValidationResult { 113 - const trimmed = value.trim(); 114 - 115 - if (trimmed.length < minLength) { 116 - return { 117 - isValid: false, 118 - error: `${fieldName} must be at least ${minLength} characters`, 119 - }; 120 - } 121 - 122 - return { isValid: true }; 123 } 124 125 /** ··· 130 maxLength: number, 131 fieldName: string = "This field", 132 ): ValidationResult { 133 - if (value.length > maxLength) { 134 - return { 135 - isValid: false, 136 - error: `${fieldName} must be ${maxLength} characters or less`, 137 - }; 138 - } 139 140 - return { isValid: true }; 141 - }
··· 1 /** 2 + * Validation utilities using Zod schemas 3 */ 4 + import { z } from "zod"; 5 6 export interface ValidationResult { 7 isValid: boolean; ··· 9 } 10 11 /** 12 + * Helper to convert Zod validation to ValidationResult 13 */ 14 + function validateWithZod<T>( 15 + schema: z.ZodSchema<T>, 16 + value: unknown, 17 + ): ValidationResult { 18 + const result = schema.safeParse(value); 19 + if (result.success) { 20 + return { isValid: true }; 21 } 22 + return { 23 + isValid: false, 24 + error: result.error.errors[0]?.message || "Validation failed", 25 + }; 26 + } 27 28 + /** 29 + * Zod Schemas 30 + */ 31 + const handleSchema = z 32 + .string() 33 + .trim() 34 + .min(1, "Please enter your handle") 35 + .transform((val) => (val.startsWith("@") ? val.slice(1) : val)) 36 + .pipe( 37 + z 38 + .string() 39 + .min(3, "Handle is too short") 40 + .regex(/^[a-zA-Z0-9.-]+$/, "Handle contains invalid characters") 41 + .refine((val) => val.includes("."), { 42 + message: "Handle must include a domain (e.g., username.bsky.social)", 43 + }) 44 + .refine((val) => !/^[.-]|[.-]$/.test(val), { 45 + message: "Handle cannot start or end with . or -", 46 + }), 47 + ); 48 49 + const emailSchema = z 50 + .string() 51 + .trim() 52 + .min(1, "Please enter your email") 53 + .email("Please enter a valid email address"); 54 55 + /** 56 + * Validate AT Protocol handle 57 + */ 58 + export function validateHandle(handle: string): ValidationResult { 59 + return validateWithZod(handleSchema, handle); 60 } 61 62 /** 63 * Validate email format 64 */ 65 export function validateEmail(email: string): ValidationResult { 66 + return validateWithZod(emailSchema, email); 67 } 68 69 /** ··· 73 value: string, 74 fieldName: string = "This field", 75 ): ValidationResult { 76 + const schema = z.string().trim().min(1, `${fieldName} is required`); 77 + return validateWithZod(schema, value); 78 } 79 80 /** ··· 85 minLength: number, 86 fieldName: string = "This field", 87 ): ValidationResult { 88 + const schema = z 89 + .string() 90 + .trim() 91 + .min(minLength, `${fieldName} must be at least ${minLength} characters`); 92 + return validateWithZod(schema, value); 93 } 94 95 /** ··· 100 maxLength: number, 101 fieldName: string = "This field", 102 ): ValidationResult { 103 + const schema = z 104 + .string() 105 + .max(maxLength, `${fieldName} must be ${maxLength} characters or less`); 106 + return validateWithZod(schema, value); 107 + } 108 109 + /** 110 + * Export schemas for advanced usage 111 + */ 112 + export const schemas = { 113 + handle: handleSchema, 114 + email: emailSchema, 115 + };