maestro/src/bridge/validation.ts
2026-06-03 05:08:00 +00:00

143 lines
5.2 KiB
TypeScript

const VALID_PROFILES = ['auto', 'fast', 'quality'] as const;
const VALID_OUTPUT_FORMATS = ['text', 'markdown', 'json'] as const;
const VALID_ASK_POLICIES = ['low', 'high'] as const;
const VALID_PRIORITIES = ['low', 'medium', 'high'] as const;
const MAX_BODY_LENGTH = 100_000;
const MAX_TITLE_LENGTH = 200;
const MAX_COMMENT_LENGTH = 100_000;
export function parseTaskId(raw: string): number | null {
const n = Number(raw);
if (!Number.isInteger(n) || n <= 0) return null;
return n;
}
export interface ValidatedCreateTask {
body: string;
title?: string;
piece?: string;
profile?: typeof VALID_PROFILES[number];
outputFormat?: typeof VALID_OUTPUT_FORMATS[number];
askPolicy?: typeof VALID_ASK_POLICIES[number];
priority?: typeof VALID_PRIORITIES[number];
attachments?: Array<{ name: string; contentBase64: string }>;
}
type ValidationResult =
| { valid: true; data: ValidatedCreateTask }
| { valid: false; error: string };
export function validateCreateTaskBody(raw: unknown): ValidationResult {
if (!raw || typeof raw !== 'object') {
return { valid: false, error: 'Request body must be an object' };
}
const obj = raw as Record<string, unknown>;
if (typeof obj.body !== 'string' || obj.body.trim().length === 0) {
return { valid: false, error: 'body is required' };
}
if (obj.body.length > MAX_BODY_LENGTH) {
return { valid: false, error: `body must be ${MAX_BODY_LENGTH} characters or less` };
}
if (obj.title !== undefined && obj.title !== null) {
if (typeof obj.title !== 'string') {
return { valid: false, error: 'title must be a string' };
}
if (obj.title.length > MAX_TITLE_LENGTH) {
return { valid: false, error: `title must be ${MAX_TITLE_LENGTH} characters or less` };
}
}
if (obj.profile !== undefined && obj.profile !== null) {
if (!(VALID_PROFILES as readonly string[]).includes(String(obj.profile))) {
return { valid: false, error: `profile must be one of: ${VALID_PROFILES.join(', ')}` };
}
}
if (obj.outputFormat !== undefined && obj.outputFormat !== null) {
if (!(VALID_OUTPUT_FORMATS as readonly string[]).includes(String(obj.outputFormat))) {
return { valid: false, error: `outputFormat must be one of: ${VALID_OUTPUT_FORMATS.join(', ')}` };
}
}
if (obj.askPolicy !== undefined && obj.askPolicy !== null) {
if (!(VALID_ASK_POLICIES as readonly string[]).includes(String(obj.askPolicy))) {
return { valid: false, error: `askPolicy must be one of: ${VALID_ASK_POLICIES.join(', ')}` };
}
}
if (obj.priority !== undefined && obj.priority !== null) {
if (!(VALID_PRIORITIES as readonly string[]).includes(String(obj.priority))) {
return { valid: false, error: `priority must be one of: ${VALID_PRIORITIES.join(', ')}` };
}
}
return {
valid: true,
data: {
body: obj.body as string,
title: obj.title as string | undefined,
piece: obj.piece as string | undefined,
profile: obj.profile as ValidatedCreateTask['profile'],
outputFormat: obj.outputFormat as ValidatedCreateTask['outputFormat'],
askPolicy: obj.askPolicy as ValidatedCreateTask['askPolicy'],
priority: obj.priority as ValidatedCreateTask['priority'],
attachments: obj.attachments as ValidatedCreateTask['attachments'],
},
};
}
export function validateCommentBody(raw: unknown): { valid: true; body: string; author: string; attachments?: Array<{ name: string; contentBase64: string }> } | { valid: false; error: string } {
if (!raw || typeof raw !== 'object') {
return { valid: false, error: 'Request body must be an object' };
}
const obj = raw as Record<string, unknown>;
const body = String(obj.body ?? '').trim();
if (!body) {
return { valid: false, error: 'body is required' };
}
if (body.length > MAX_COMMENT_LENGTH) {
return { valid: false, error: `body must be ${MAX_COMMENT_LENGTH} characters or less` };
}
const author = String(obj.author ?? 'user').trim() || 'user';
const attachments = Array.isArray(obj.attachments) ? obj.attachments as Array<{ name: string; contentBase64: string }> : undefined;
return { valid: true, body, author, attachments };
}
export type ValidatedFeedback = {
rating: 'good' | 'bad';
tags: string[];
comment: string | null;
};
type FeedbackValidationResult =
| { valid: true; data: ValidatedFeedback }
| { valid: false; error: string };
export function validateFeedbackBody(raw: unknown): FeedbackValidationResult {
if (!raw || typeof raw !== 'object') {
return { valid: false, error: 'Request body must be an object' };
}
const obj = raw as Record<string, unknown>;
if (obj.rating !== 'good' && obj.rating !== 'bad') {
return { valid: false, error: "rating must be 'good' or 'bad'" };
}
if (!Array.isArray(obj.tags) || obj.tags.some((t: unknown) => typeof t !== 'string')) {
return { valid: false, error: 'tags must be an array of strings' };
}
if (obj.tags.length > 10) {
return { valid: false, error: 'tags must have at most 10 items' };
}
const comment = obj.comment != null ? String(obj.comment) : null;
if (comment && comment.length > 1000) {
return { valid: false, error: 'comment must be at most 1000 characters' };
}
return {
valid: true,
data: { rating: obj.rating, tags: obj.tags as string[], comment },
};
}