/** * Zod Validation Schemas * Constitutional Principle III: Local Execution Security * Constitutional Principle IV: Defensive Programming */ import { z } from 'zod'; /** * Common validation patterns */ // Vault name validation (alphanumeric, spaces, hyphens, underscores) export const vaultNameSchema = z .string() .min(1, 'Vault name cannot be empty') .max(255, 'Vault name too long') .regex(/^[a-zA-Z0-9\s_-]+$/, 'Vault name contains invalid characters'); // Note name validation (allow most characters, but not path separators) export const noteNameSchema = z .string() .min(1, 'Note name cannot be empty') .max(255, 'Note name too long') .regex(/^[^/\\]+$/, 'Note name cannot contain path separators'); // File path validation (allow subdirectories) export const filePathSchema = z .string() .min(1, 'File path cannot be empty') .max(1024, 'File path too long') .regex(/^[^<>:"|?*]+$/, 'File path contains invalid characters'); // Content validation (allow any string, but limit size) export const contentSchema = z .string() .max(1048576, 'Content too large (max 1MB)'); // 1MB limit // Tag validation (must start with #) export const tagSchema = z .string() .min(2, 'Tag too short') .regex(/^#[a-zA-Z0-9_/-]+$/, 'Invalid tag format (must start with # and contain only alphanumeric, _, /, -)'); // Property key validation export const propertyKeySchema = z .string() .min(1, 'Property key cannot be empty') .max(100, 'Property key too long') .regex(/^[a-zA-Z0-9_-]+$/, 'Property key contains invalid characters'); // Boolean flag validation export const booleanFlagSchema = z .union([z.boolean(), z.string()]) .transform((val) => { if (typeof val === 'boolean') return val; return val.toLowerCase() === 'true' || val === '1'; }); // Optional string that can be undefined or empty export const optionalStringSchema = z.string().optional(); // Date string validation (ISO 8601) export const dateSchema = z .string() .regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/, 'Invalid date format (use ISO 8601)'); /** * Common parameter schemas */ // File identifier (either file name or full path) export const fileIdentifierSchema = z.object({ file: z.string().optional(), path: z.string().optional(), }).refine( (data) => data.file || data.path, { message: 'Either file or path must be provided' } ); // Pagination parameters export const paginationSchema = z.object({ limit: z.number().int().positive().max(1000).optional(), offset: z.number().int().nonnegative().optional(), }); // Format options export const formatSchema = z.object({ format: z.enum(['json', 'tsv', 'csv', 'text']).optional().default('text'), }); /** * Tool-specific schemas */ // Create note parameters export const createNoteSchema = z.object({ name: noteNameSchema, path: filePathSchema.optional(), content: contentSchema.optional(), template: z.string().optional(), overwrite: booleanFlagSchema.optional(), open: booleanFlagSchema.optional(), }); // Read note parameters export const readNoteSchema = z.union([ z.object({ file: noteNameSchema }), z.object({ path: filePathSchema }), ]).refine( (data) => ('file' in data && data.file) || ('path' in data && data.path), { message: 'Either file or path must be provided' } ); // Append/Prepend parameters export const appendPrependSchema = z.intersection( fileIdentifierSchema, z.object({ content: contentSchema, inline: booleanFlagSchema.optional(), }) ); // Delete note parameters export const deleteNoteSchema = z.intersection( fileIdentifierSchema, z.object({ permanent: booleanFlagSchema.optional(), }) ); // Move/Rename parameters export const moveRenameSchema = z.intersection( fileIdentifierSchema, z.object({ newPath: filePathSchema.optional(), newName: noteNameSchema.optional(), }) ).refine( (data) => data.newPath || data.newName, { message: 'Either newPath or newName must be provided' } ); // Search parameters export const searchSchema = z.object({ query: z.string().min(1, 'Search query cannot be empty'), path: optionalStringSchema, // Folder path to limit search limit: z.number().int().positive().max(1000).optional(), total: booleanFlagSchema.optional(), // Return match count instead of files case: booleanFlagSchema.optional(), // Case sensitive search format: z.enum(['text', 'json']).optional().default('text'), }); // Tag search parameters export const tagSearchSchema = z.object({ tag: tagSchema, ...formatSchema.shape, }); // Property parameters export const propertySchema = z.intersection( fileIdentifierSchema, z.object({ key: propertyKeySchema, value: z.unknown().optional(), }) );