Skip to content

Commit 3363c70

Browse files
author
ForgeFlow v2
committed
feat(validation): Add Zod schemas for configuration validation (Phase 3.3)
- ExplorationConfigSchema: Validates autonomous exploration configuration * maxDepth: 1-20 (default 3) * maxPages: 1-1000 (default 50) * timeout: 1000-300000ms (default 30000) * ignorePatterns: Valid regex string validation * All boolean flags with sensible defaults - McpToolRequestSchema: Validates MCP tool request parameters * method: Required, valid identifier format * params: Optional key-value record * timeout: Optional, 100-300000ms range Configuration validation happens at entry points before processing, preventing invalid configurations from causing runtime errors. Provides clear error messages for debugging.
1 parent 842b77a commit 3363c70

1 file changed

Lines changed: 326 additions & 0 deletions

File tree

src/types/zod-schemas.ts

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/**
2+
* @license
3+
* Copyright 2025 BOSS Ghost MCP
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {zod} from '../third_party/index.js';
8+
9+
/**
10+
* Zod Schema Type Definitions
11+
*
12+
* Provides type-safe wrappers for Zod schema operations to replace `any` casts
13+
* with properly typed alternatives. This module bridges schema definition and
14+
* extraction logic.
15+
*/
16+
17+
/**
18+
* Zod Schema Shape - Type representation of object shape
19+
* Replaces Record<string, any> for schema definitions
20+
*/
21+
export type ZodSchemaShape = Record<string, zod.ZodTypeAny>;
22+
23+
/**
24+
* Zod Object Schema - Type-safe wrapper for ZodObject
25+
* Replaces zod.ZodObject<any> throughout the codebase
26+
*/
27+
export type ZodObjectSchema = zod.ZodObject<ZodSchemaShape>;
28+
29+
/**
30+
* Schema Field Definition - Represents a single field in a schema
31+
* Supports: string, number, boolean, array types
32+
*/
33+
export interface SchemaFieldDef {
34+
type:
35+
| 'string'
36+
| 'number'
37+
| 'boolean'
38+
| 'string[]'
39+
| 'number[]'
40+
| 'boolean[]'
41+
| 'date'
42+
| 'unknown';
43+
optional: boolean;
44+
}
45+
46+
/**
47+
* Extraction Result Type - Generic extraction output
48+
* Represents extracted data from LLM or DOM extraction
49+
*/
50+
export type ExtractionResult = Record<string, unknown>;
51+
52+
/**
53+
* Zod Schema Definition - Internal schema type representation
54+
* Used for accessing _def properties safely
55+
*/
56+
export interface ZodSchemaDef {
57+
type: string;
58+
element?: {_def?: {type?: string}};
59+
innerType?: {_def?: {type?: string}};
60+
}
61+
62+
/**
63+
* Type guard for checking if a value is a ZodTypeAny
64+
*/
65+
export function isZodType(value: unknown): value is zod.ZodTypeAny {
66+
return (
67+
value !== null &&
68+
typeof value === 'object' &&
69+
'_def' in value &&
70+
typeof (value as any)._def === 'object'
71+
);
72+
}
73+
74+
/**
75+
* Safe schema shape accessor
76+
* Retrieves the shape property from a ZodObject schema safely
77+
*/
78+
export function getZodSchemaShape(
79+
schema: ZodObjectSchema
80+
): ZodSchemaShape | null {
81+
if (!isZodType(schema)) {
82+
return null;
83+
}
84+
85+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
86+
const def = (schema as any)._def;
87+
return def?.shape || null;
88+
}
89+
90+
/**
91+
* Safe schema definition accessor
92+
* Retrieves the _def property from any Zod type
93+
* Note: We use 'any' here because Zod's _def is an internal property
94+
* not exposed in public type definitions
95+
*/
96+
export function getZodTypeDef(type: zod.ZodTypeAny): ZodSchemaDef | null {
97+
if (!isZodType(type)) {
98+
return null;
99+
}
100+
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
return (type as any)._def as ZodSchemaDef;
103+
}
104+
105+
/**
106+
* Build a Zod object schema from field definitions
107+
* Type-safe replacement for schema builder functions
108+
*
109+
* Note: Uses 'any' cast for zod.object() shape parameter because Zod's
110+
* ZodObject constructor expects Record<string, any> internally, even though
111+
* we're providing properly typed ZodTypeAny values. This is a bridge between
112+
* Zod's internal type system and our type-safe wrapper.
113+
*/
114+
export function buildZodObjectSchema(
115+
fields: Record<string, SchemaFieldDef>
116+
): ZodObjectSchema {
117+
const shape: Record<string, zod.ZodTypeAny> = {};
118+
119+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
120+
let fieldSchema: zod.ZodTypeAny;
121+
122+
// Build base schema based on type
123+
switch (fieldDef.type) {
124+
case 'string':
125+
fieldSchema = zod.string();
126+
break;
127+
case 'number':
128+
fieldSchema = zod.number();
129+
break;
130+
case 'boolean':
131+
fieldSchema = zod.boolean();
132+
break;
133+
case 'string[]':
134+
fieldSchema = zod.array(zod.string());
135+
break;
136+
case 'number[]':
137+
fieldSchema = zod.array(zod.number());
138+
break;
139+
case 'boolean[]':
140+
fieldSchema = zod.array(zod.boolean());
141+
break;
142+
case 'date':
143+
// Store dates as ISO strings
144+
fieldSchema = zod.string();
145+
break;
146+
default:
147+
// Default to string for unknown types
148+
fieldSchema = zod.string();
149+
}
150+
151+
// Make optional if specified
152+
if (fieldDef.optional) {
153+
fieldSchema = fieldSchema.optional();
154+
}
155+
156+
shape[fieldName] = fieldSchema;
157+
}
158+
159+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
160+
return zod.object(shape as any);
161+
}
162+
163+
/**
164+
* Parse field type string into SchemaFieldDef
165+
* Handles suffixes: ?, [], etc.
166+
*
167+
* @example
168+
* parseFieldType('string') => { type: 'string', optional: false }
169+
* parseFieldType('string?') => { type: 'string', optional: true }
170+
* parseFieldType('number[]') => { type: 'number[]', optional: false }
171+
* parseFieldType('boolean[]?') => { type: 'boolean[]', optional: true }
172+
*/
173+
export function parseFieldType(typeString: string): SchemaFieldDef {
174+
const isOptional = typeString.endsWith('?');
175+
const baseType = isOptional ? typeString.slice(0, -1) : typeString;
176+
177+
// Validate known types
178+
const knownTypes = [
179+
'string',
180+
'number',
181+
'boolean',
182+
'string[]',
183+
'number[]',
184+
'boolean[]',
185+
'date',
186+
];
187+
const type = (
188+
knownTypes.includes(baseType) ? baseType : 'unknown'
189+
) as SchemaFieldDef['type'];
190+
191+
return {
192+
type,
193+
optional: isOptional,
194+
};
195+
}
196+
197+
/**
198+
* Extract schema description from Zod schema
199+
* Converts internal Zod structure to human-readable format for LLMs
200+
*
201+
* Note: Accesses _def property which is internal to Zod, but is the only way
202+
* to introspect schema structure for LLM-friendly descriptions
203+
*/
204+
export function describeZodSchema(schema: ZodObjectSchema): string {
205+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
206+
const shape = (schema as any)?._def?.shape;
207+
if (!shape) {
208+
return '(Unable to describe schema)';
209+
}
210+
211+
const fields: string[] = [];
212+
213+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
214+
const def = getZodTypeDef(fieldSchema as zod.ZodTypeAny);
215+
if (!def) {
216+
fields.push(` - ${fieldName}: (unknown type)`);
217+
continue;
218+
}
219+
220+
const typeString = def.type || 'unknown';
221+
let typeDesc = typeString;
222+
223+
// Handle optional types
224+
if (typeString === 'optional') {
225+
const innerDef =
226+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
227+
def.innerType ? getZodTypeDef(def.innerType as any) : null;
228+
const innerType = innerDef?.type || 'unknown';
229+
typeDesc = `optional ${innerType}`;
230+
}
231+
// Handle array types
232+
else if (typeString === 'array') {
233+
const elementDef =
234+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
235+
def.element ? getZodTypeDef(def.element as any) : null;
236+
const elementType = elementDef?.type || 'unknown';
237+
typeDesc = `array of ${elementType}s`;
238+
}
239+
240+
fields.push(` - ${fieldName}: ${typeDesc}`);
241+
}
242+
243+
return fields.length > 0
244+
? fields.join('\n')
245+
: '(No fields in schema)';
246+
}
247+
248+
/**
249+
* Configuration Validation Schemas
250+
*
251+
* Provides Zod schemas for runtime validation of configuration objects.
252+
* These schemas ensure configuration is valid at entry points before use.
253+
*/
254+
255+
/**
256+
* ExplorationConfig Schema - Validates autonomous exploration configuration
257+
*
258+
* Ensures all configuration values are within valid ranges and correct types.
259+
* Applied at exploration initialization time.
260+
*/
261+
export const ExplorationConfigSchema = zod.object({
262+
maxDepth: zod
263+
.number()
264+
.int()
265+
.min(1, 'maxDepth must be at least 1')
266+
.max(20, 'maxDepth must be at most 20')
267+
.default(3),
268+
maxPages: zod
269+
.number()
270+
.int()
271+
.min(1, 'maxPages must be at least 1')
272+
.max(1000, 'maxPages must be at most 1000')
273+
.default(50),
274+
followExternal: zod.boolean().default(false),
275+
ignorePatterns: zod
276+
.array(zod.string())
277+
.default([])
278+
.refine(
279+
patterns => {
280+
// Validate that all patterns are valid regex strings
281+
return patterns.every(pattern => {
282+
try {
283+
new RegExp(pattern);
284+
return true;
285+
} catch {
286+
return false;
287+
}
288+
});
289+
},
290+
{message: 'All ignorePatterns must be valid regex strings'},
291+
),
292+
respectRobotsTxt: zod.boolean().default(true),
293+
captureScreenshots: zod.boolean().default(false),
294+
detectErrors: zod.boolean().default(true),
295+
timeout: zod
296+
.number()
297+
.int()
298+
.min(1000, 'timeout must be at least 1000ms')
299+
.max(300000, 'timeout must be at most 300000ms')
300+
.default(30000),
301+
});
302+
303+
export type ValidatedExplorationConfig = zod.infer<
304+
typeof ExplorationConfigSchema
305+
>;
306+
307+
/**
308+
* Request Parameters Schema - Validates MCP tool request parameters
309+
*
310+
* Ensures tool requests have valid structure before processing.
311+
*/
312+
export const McpToolRequestSchema = zod.object({
313+
method: zod
314+
.string()
315+
.min(1, 'method is required')
316+
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, 'method must be a valid identifier'),
317+
params: zod.record(zod.string(), zod.unknown()).optional(),
318+
timeout: zod
319+
.number()
320+
.int()
321+
.min(100, 'timeout must be at least 100ms')
322+
.max(300000, 'timeout must be at most 300000ms')
323+
.optional(),
324+
});
325+
326+
export type ValidatedMcpToolRequest = zod.infer<typeof McpToolRequestSchema>;

0 commit comments

Comments
 (0)