validators.ts•7.51 kB
/**
* Input Validation Utilities
*
* Provides validation functions for ByteBot API inputs using Zod schemas
*/
import { z } from 'zod';
import {
TaskPriority,
TaskStatus,
MouseButton,
ScrollDirection,
} from '../types/bytebot.js';
/**
* Validation schemas
*/
// Task-related schemas
export const TaskPrioritySchema = z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']);
export const TaskStatusSchema = z.enum([
'PENDING',
'IN_PROGRESS',
'NEEDS_HELP',
'NEEDS_REVIEW',
'COMPLETED',
'CANCELLED',
'FAILED',
]);
export const CreateTaskSchema = z.object({
description: z.string().min(1, 'Description cannot be empty'),
priority: TaskPrioritySchema.optional(),
files: z
.array(
z.object({
name: z.string(),
content: z.string(), // Base64
mimeType: z.string().optional(),
})
)
.optional(),
});
export const UpdateTaskSchema = z.object({
status: TaskStatusSchema.optional(),
priority: TaskPrioritySchema.optional(),
message: z.string().optional(),
});
export const ListTasksSchema = z.object({
status: TaskStatusSchema.optional(),
priority: TaskPrioritySchema.optional(),
limit: z.number().int().positive().optional(),
offset: z.number().int().nonnegative().optional(),
});
// Desktop action schemas
export const MouseButtonSchema = z.enum(['left', 'right', 'middle']);
export const ScrollDirectionSchema = z.enum(['up', 'down', 'left', 'right']);
export const CoordinatesSchema = z.object({
x: z.number().int().nonnegative(),
y: z.number().int().nonnegative(),
});
export const MoveMouseSchema = z.object({
action: z.literal('move_mouse'),
x: z.number().int().nonnegative(),
y: z.number().int().nonnegative(),
});
export const ClickMouseSchema = z.object({
action: z.literal('click_mouse'),
x: z.number().int().nonnegative(),
y: z.number().int().nonnegative(),
button: MouseButtonSchema.optional(),
count: z.number().int().positive().optional(),
});
export const DragMouseSchema = z.object({
action: z.literal('drag_mouse'),
from_x: z.number().int().nonnegative(),
from_y: z.number().int().nonnegative(),
to_x: z.number().int().nonnegative(),
to_y: z.number().int().nonnegative(),
});
export const ScrollSchema = z.object({
action: z.literal('scroll'),
direction: ScrollDirectionSchema,
count: z.number().int().positive().optional(),
});
export const TypeTextSchema = z.object({
action: z.literal('type_text'),
text: z.string().min(1),
delay: z.number().int().nonnegative().optional(),
});
export const PasteTextSchema = z.object({
action: z.literal('paste_text'),
text: z.string().min(1),
});
export const PressKeysSchema = z.object({
action: z.literal('press_keys'),
keys: z.array(z.string()).min(1),
});
export const ReadFileSchema = z.object({
action: z.literal('read_file'),
path: z.string().min(1),
});
export const WriteFileSchema = z.object({
action: z.literal('write_file'),
path: z.string().min(1),
content: z.string(), // Base64
});
export const ApplicationSchema = z.object({
action: z.literal('application'),
name: z.string().min(1),
});
export const WaitSchema = z.object({
action: z.literal('wait'),
duration: z.number().int().positive(),
});
export const ScreenshotSchema = z.object({
action: z.literal('screenshot'),
});
export const CursorPositionSchema = z.object({
action: z.literal('cursor_position'),
});
/**
* Validation helper functions
*/
export function validateTaskId(taskId: string): void {
if (!taskId || taskId.trim().length === 0) {
throw new Error('Task ID cannot be empty');
}
}
export function validateTaskDescription(description: string): void {
if (!description || description.trim().length === 0) {
throw new Error('Task description cannot be empty');
}
if (description.length > 10000) {
throw new Error('Task description is too long (max 10000 characters)');
}
}
export function validatePriority(priority: string): TaskPriority {
const result = TaskPrioritySchema.safeParse(priority);
if (!result.success) {
throw new Error(
`Invalid priority: ${priority}. Must be one of: LOW, MEDIUM, HIGH, URGENT`
);
}
return result.data;
}
export function validateStatus(status: string): TaskStatus {
const result = TaskStatusSchema.safeParse(status);
if (!result.success) {
throw new Error(
`Invalid status: ${status}. Must be one of: PENDING, IN_PROGRESS, NEEDS_HELP, NEEDS_REVIEW, COMPLETED, CANCELLED, FAILED`
);
}
return result.data;
}
export function validateCoordinates(x: number, y: number): void {
if (!Number.isInteger(x) || x < 0) {
throw new Error(`Invalid x coordinate: ${x}. Must be a non-negative integer`);
}
if (!Number.isInteger(y) || y < 0) {
throw new Error(`Invalid y coordinate: ${y}. Must be a non-negative integer`);
}
}
export function validateMouseButton(button: string): MouseButton {
const result = MouseButtonSchema.safeParse(button);
if (!result.success) {
throw new Error(
`Invalid mouse button: ${button}. Must be one of: left, right, middle`
);
}
return result.data;
}
export function validateScrollDirection(direction: string): ScrollDirection {
const result = ScrollDirectionSchema.safeParse(direction);
if (!result.success) {
throw new Error(
`Invalid scroll direction: ${direction}. Must be one of: up, down, left, right`
);
}
return result.data;
}
export function validateFilePath(path: string): void {
if (!path || path.trim().length === 0) {
throw new Error('File path cannot be empty');
}
}
export function validateFileSize(base64Content: string, maxSize: number): void {
// Calculate approximate size from base64
// Base64 is ~1.37x larger than binary, so divide by 1.37 to get approximate binary size
const approximateSize = (base64Content.length * 3) / 4;
if (approximateSize > maxSize) {
const sizeMB = (approximateSize / (1024 * 1024)).toFixed(2);
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
throw new Error(
`File size (${sizeMB}MB) exceeds maximum allowed size (${maxSizeMB}MB)`
);
}
}
export function validateBase64(content: string): void {
// Basic base64 validation
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (!base64Regex.test(content)) {
throw new Error('Invalid base64 content');
}
}
export function validateTimeout(timeout: number): void {
if (!Number.isInteger(timeout) || timeout <= 0) {
throw new Error(`Invalid timeout: ${timeout}. Must be a positive integer`);
}
if (timeout > 600000) {
// 10 minutes
throw new Error('Timeout cannot exceed 10 minutes (600000ms)');
}
}
export function validatePollInterval(interval: number): void {
if (!Number.isInteger(interval) || interval < 100) {
throw new Error(
`Invalid poll interval: ${interval}. Must be at least 100ms`
);
}
if (interval > 60000) {
// 1 minute
throw new Error('Poll interval cannot exceed 1 minute (60000ms)');
}
}
/**
* Sanitize user input to prevent injection attacks
*/
export function sanitizeInput(input: string): string {
// Remove null bytes and control characters except newlines and tabs
return input.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '');
}
/**
* Validate and parse JSON safely
*/
export function safeJsonParse<T>(json: string): T {
try {
return JSON.parse(json) as T;
} catch (error) {
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}