WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
at feature/issue-4-pr-create-list-view 188 lines 5.2 kB view raw
1import { z } from 'zod'; 2 3/** 4 * Validation schema for AT Protocol handle 5 * Supports standard Bluesky handles (user.bsky.social) and custom domains (example.com) 6 */ 7export const handleSchema: z.ZodString = z 8 .string() 9 .min(1, 'Handle cannot be empty') 10 .regex( 11 /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, 12 'Invalid handle format. Must be a valid domain (e.g., user.bsky.social or example.com)' 13 ); 14 15/** 16 * Validation schema for AT Protocol DID 17 */ 18export const didSchema: z.ZodString = z 19 .string() 20 .min(1, 'DID cannot be empty') 21 .regex( 22 /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/, 23 'Invalid DID format. Must start with "did:" followed by method and identifier' 24 ); 25 26/** 27 * Validation schema for app password 28 * AT Protocol app passwords are typically 19 characters with dashes 29 */ 30export const appPasswordSchema: z.ZodString = z 31 .string() 32 .min(1, 'Password cannot be empty') 33 .max(1000, 'Password is too long'); 34 35/** 36 * Validation schema for identifier (handle or DID) 37 */ 38export const identifierSchema: z.ZodUnion<[typeof handleSchema, typeof didSchema]> = z.union([ 39 handleSchema, 40 didSchema, 41]); 42 43/** 44 * Validate a handle 45 * @throws {z.ZodError} if validation fails 46 */ 47export function validateHandle(handle: string): string { 48 return handleSchema.parse(handle); 49} 50 51/** 52 * Validate a DID 53 * @throws {z.ZodError} if validation fails 54 */ 55export function validateDid(did: string): string { 56 return didSchema.parse(did); 57} 58 59/** 60 * Validate an identifier (handle or DID) 61 * @throws {z.ZodError} if validation fails 62 */ 63export function validateIdentifier(identifier: string): string { 64 return identifierSchema.parse(identifier); 65} 66 67/** 68 * Validate an app password 69 * @throws {z.ZodError} if validation fails 70 */ 71export function validateAppPassword(password: string): string { 72 return appPasswordSchema.parse(password); 73} 74 75/** 76 * Safe validation that returns success/error instead of throwing 77 */ 78export function safeValidateHandle( 79 handle: string 80): { success: true; data: string } | { success: false; error: string } { 81 const result = handleSchema.safeParse(handle); 82 if (result.success) { 83 return { success: true, data: result.data }; 84 } 85 return { success: false, error: result.error.issues[0]?.message ?? 'Validation failed' }; 86} 87 88/** 89 * Safe validation that returns success/error instead of throwing 90 */ 91export function safeValidateDid( 92 did: string 93): { success: true; data: string } | { success: false; error: string } { 94 const result = didSchema.safeParse(did); 95 if (result.success) { 96 return { success: true, data: result.data }; 97 } 98 return { success: false, error: result.error.issues[0]?.message ?? 'Validation failed' }; 99} 100 101/** 102 * Safe validation that returns success/error instead of throwing 103 */ 104export function safeValidateIdentifier( 105 identifier: string 106): { success: true; data: string } | { success: false; error: string } { 107 const result = identifierSchema.safeParse(identifier); 108 if (result.success) { 109 return { success: true, data: result.data }; 110 } 111 return { success: false, error: result.error.issues[0]?.message ?? 'Validation failed' }; 112} 113 114/** 115 * Validation schema for Tangled-specific DID (did:plc: format only) 116 */ 117export const tangledDidSchema: z.ZodString = z 118 .string() 119 .regex(/^did:plc:[a-z0-9]+$/, 'Invalid Tangled DID format. Expected: did:plc:...'); 120 121/** 122 * Check if a string is a valid AT Protocol handle 123 * Returns true/false without throwing 124 */ 125export function isValidHandle(handle: string): boolean { 126 return handleSchema.safeParse(handle).success; 127} 128 129/** 130 * Check if a string is a valid Tangled DID (did:plc: format) 131 * Returns true/false without throwing 132 */ 133export function isValidTangledDid(did: string): boolean { 134 return tangledDidSchema.safeParse(did).success; 135} 136 137/** 138 * Validation schema for issue title 139 * Titles must be 1-256 characters 140 */ 141export const issueTitleSchema: z.ZodString = z 142 .string() 143 .min(1, 'Issue title cannot be empty') 144 .max(256, 'Issue title must be 256 characters or less'); 145 146/** 147 * Validation schema for issue body 148 * Body is optional but limited to 50,000 characters 149 */ 150export const issueBodySchema: z.ZodOptional<z.ZodString> = z 151 .string() 152 .max(50000, 'Issue body must be 50,000 characters or less') 153 .optional(); 154 155/** 156 * Validation schema for AT-URI 157 * Format: at://did:method:identifier/collection[/rkey] 158 */ 159export const atUriSchema: z.ZodString = z 160 .string() 161 .regex( 162 /^at:\/\/did:[a-z]+:[a-zA-Z0-9._:%-]+\/[a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*(?:\/[a-zA-Z0-9._-]+)?$/, 163 'Invalid AT-URI format. Expected: at://did:method:id/collection[/rkey]' 164 ); 165 166/** 167 * Validate an issue title 168 * @throws {z.ZodError} if validation fails 169 */ 170export function validateIssueTitle(title: string): string { 171 return issueTitleSchema.parse(title); 172} 173 174/** 175 * Validate an issue body 176 * @throws {z.ZodError} if validation fails 177 */ 178export function validateIssueBody(body: string): string { 179 return issueBodySchema.parse(body) ?? ''; 180} 181 182/** 183 * Check if a string is a valid AT-URI 184 * Returns true/false without throwing 185 */ 186export function isValidAtUri(uri: string): boolean { 187 return atUriSchema.safeParse(uri).success; 188}