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! :)
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}