export interface GenerateMeetingDocumentRequest { meetingId: string templateId: string idempotencyKey: string title: string model: 'claude-haiku-4-5-20251001' | 'claude-sonnet-4-6' | 'claude-opus-4-6' } export interface ProviderDocumentResult { content: string inputTokens: number | null outputTokens: number | null } const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const MODELS = new Set([ 'claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6', ]) const REQUEST_KEYS = new Set([ 'meetingId', 'templateId', 'idempotencyKey', 'title', 'model', ]) export class MeetingDocumentRequestError extends Error { readonly status = 400 constructor(message: string) { super(message) this.name = 'MeetingDocumentRequestError' } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function requireUuid(value: unknown, field: string): string { if (typeof value !== 'string' || !UUID_PATTERN.test(value)) { throw new MeetingDocumentRequestError(`${field} must be a UUID`) } return value.toLowerCase() } export function parseGenerateMeetingDocumentRequest( value: unknown, ): GenerateMeetingDocumentRequest { if (!isRecord(value)) throw new MeetingDocumentRequestError('Invalid JSON body') const unsupported = Object.keys(value).filter((key) => !REQUEST_KEYS.has(key)) if (unsupported.length > 0) { throw new MeetingDocumentRequestError('Unsupported request fields') } const title = typeof value.title === 'string' ? value.title.trim().replace(/\s+/g, ' ') : '' if (title.length < 1 || title.length > 160) { throw new MeetingDocumentRequestError('title must contain 1-160 characters') } const model = value.model === undefined ? 'claude-haiku-4-5-20251001' : value.model if (typeof model !== 'string' || !MODELS.has(model as GenerateMeetingDocumentRequest['model'])) { throw new MeetingDocumentRequestError('Unsupported model') } return { meetingId: requireUuid(value.meetingId, 'meetingId'), templateId: requireUuid(value.templateId, 'templateId'), idempotencyKey: requireUuid(value.idempotencyKey, 'idempotencyKey'), title, model: model as GenerateMeetingDocumentRequest['model'], } } function safeTokenCount(value: unknown): number | null { return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null } export function parseProviderDocumentResult(value: unknown): ProviderDocumentResult | null { if (!isRecord(value) || !Array.isArray(value.content)) return null const text = value.content .filter((block): block is Record => isRecord(block)) .filter((block) => block.type === 'text' && typeof block.text === 'string') .map((block) => (block.text as string).trim()) .filter((part) => part.length > 0) .join('\n\n') .trim() if (text.length < 1 || text.length > 100000) return null const usage = isRecord(value.usage) ? value.usage : {} return { content: text, inputTokens: safeTokenCount(usage.input_tokens), outputTokens: safeTokenCount(usage.output_tokens), } }