import type { CalendlyBookingOffer, CalendlyEmbeddedBooking, ChatHistoryResponse, ChatSseEvent, WidgetConfigResponse, WidgetTheme, } from "./public-contracts.js"; import { WidgetError } from "./errors.js"; import { parseStructuredUiRequest } from "./playbook-contract-parser.js"; // The widget validates network input by hand so every distribution can enforce // the public contract without pulling Zod into the browser bundle. const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const DEFAULT_PRIMARY_COLOR = "#111827"; /** Validates the complete fail-closed configuration boundary. */ export function parseWidgetConfig(value: unknown): WidgetConfigResponse { if ( !isRecord(value) || typeof value.embedEnabled !== "boolean" || typeof value.inlineEnabled !== "boolean" || (value.sessionVersion !== undefined && (!Number.isInteger(value.sessionVersion) || (value.sessionVersion as number) < 1)) || !isWidgetTheme(value.theme) || !isRecord(value.accessibility) || typeof value.accessibility.stt !== "boolean" || typeof value.accessibility.tts !== "boolean" ) { throw new WidgetError("invalid_response"); } return { sessionVersion: typeof value.sessionVersion === "number" ? value.sessionVersion : 2, embedEnabled: value.embedEnabled, inlineEnabled: value.inlineEnabled, theme: { ...value.theme, primaryColor: value.theme.primaryColor ?? DEFAULT_PRIMARY_COLOR, }, accessibility: { stt: value.accessibility.stt, tts: value.accessibility.tts, }, }; } /** * Validates persisted conversation data before it becomes renderable text or * internal source state. */ export function parseChatHistory(value: unknown): ChatHistoryResponse { if ( !isRecord(value) || typeof value.sessionId !== "string" || !Array.isArray(value.messages) ) { throw new WidgetError("invalid_response"); } const messages = value.messages.map((message) => { if ( !isRecord(message) || !isUuid(message.id) || typeof message.role !== "string" || typeof message.content !== "string" || !isNullableUuidArray(message.citedChunkIds) || !isNullableString(message.createdAt) ) { throw new WidgetError("invalid_response"); } return { id: message.id, role: message.role, content: message.content, citedChunkIds: message.citedChunkIds, createdAt: message.createdAt, }; }); const structuredUiRequest = value.structuredUiRequest === undefined || value.structuredUiRequest === null ? null : parseStructuredUiRequest(value.structuredUiRequest); let externalAction: ChatHistoryResponse["externalAction"] = null; if (value.externalAction !== undefined && value.externalAction !== null) { if ( !isRecord(value.externalAction) || !isUuid(value.externalAction.actionId) ) { throw new WidgetError("invalid_response"); } externalAction = { actionId: value.externalAction.actionId, ...parseCalendlyEmbeddedBooking(value.externalAction), }; } return { sessionId: value.sessionId, messages, externalAction, structuredUiRequest, }; } export function parseCalendlyEmbeddedBooking( value: unknown, ): CalendlyEmbeddedBooking { if ( !isRecord(value) || value.kind !== "calendly_inline_booking" || typeof value.schedulingUrl !== "string" || typeof value.eventTypeName !== "string" || typeof value.inviteeName !== "string" || typeof value.inviteeEmail !== "string" || typeof value.timezone !== "string" || typeof value.requestedStartTime !== "string" || !isTrustedCalendlyUrl(value.schedulingUrl) ) { throw new WidgetError("invalid_response"); } return { kind: value.kind, schedulingUrl: value.schedulingUrl, eventTypeName: value.eventTypeName, inviteeName: value.inviteeName, inviteeEmail: value.inviteeEmail, timezone: value.timezone, requestedStartTime: value.requestedStartTime, }; } /** Converts one untrusted JSON value into a shared SSE event variant. */ export function parseChatSseEvent(value: unknown): ChatSseEvent { if (!isRecord(value) || typeof value.type !== "string") { throw new WidgetError("stream_error"); } switch (value.type) { case "token": if (typeof value.content === "string") { return { type: "token", content: value.content }; } break; case "sources": if (isUuidArray(value.chunkIds)) { return { type: "sources", chunkIds: value.chunkIds }; } break; case "action_confirmation": if ( isUuid(value.actionId) && typeof value.toolName === "string" && typeof value.summary === "string" && typeof value.confirmationToken === "string" && typeof value.expiresAt === "string" ) { return { type: "action_confirmation", actionId: value.actionId, toolName: value.toolName, summary: value.summary, confirmationToken: value.confirmationToken, expiresAt: value.expiresAt, }; } break; case "booking_slots": { const offer = parseCalendlyBookingOffer(value.offer); return { type: "booking_slots", offer }; } case "structured_ui": return { type: "structured_ui", request: parseStructuredUiRequest(value.request), }; case "done": if (isUuid(value.messageId)) { return { type: "done", messageId: value.messageId }; } break; case "error": if (typeof value.error === "string") { return { type: "error", error: value.error }; } break; } throw new WidgetError("stream_error"); } function parseCalendlyBookingOffer(value: unknown): CalendlyBookingOffer { if ( !isRecord(value) || typeof value.eventTypeName !== "string" || !Array.isArray(value.availableTimes) || value.availableTimes.length === 0 || value.availableTimes.length > 30 ) { throw new WidgetError("stream_error"); } const availableTimes = value.availableTimes.map((slot) => { if ( !isRecord(slot) || typeof slot.startTime !== "string" || (slot.endTime !== undefined && typeof slot.endTime !== "string") || (slot.schedulingUrl !== undefined && (typeof slot.schedulingUrl !== "string" || !isTrustedCalendlyUrl(slot.schedulingUrl))) ) { throw new WidgetError("stream_error"); } return { startTime: slot.startTime, ...(typeof slot.endTime === "string" ? { endTime: slot.endTime } : {}), ...(typeof slot.schedulingUrl === "string" ? { schedulingUrl: slot.schedulingUrl } : {}), }; }); const provider = value.provider === "google_calendar" ? "google_calendar" : "calendly"; return { provider, eventTypeName: value.eventTypeName, availableTimes, }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isUuid(value: unknown): value is string { return typeof value === "string" && UUID_PATTERN.test(value); } function isUuidArray(value: unknown): value is string[] { return Array.isArray(value) && value.every(isUuid); } function isNullableUuidArray(value: unknown): value is string[] | null { return value === null || isUuidArray(value); } function isNullableString(value: unknown): value is string | null { return value === null || typeof value === "string"; } function isWidgetTheme(value: unknown): value is Omit< WidgetTheme, "primaryColor" > & { primaryColor?: string; } { return ( isRecord(value) && Array.isArray(value.borderColors) && value.borderColors.length >= 1 && value.borderColors.length <= 3 && value.borderColors.every(isHexColor) && isHexColor(value.buttonColor) && (value.primaryColor === undefined || isHexColor(value.primaryColor)) && typeof value.placeholder === "string" && value.placeholder.trim().length >= 1 && value.placeholder.length <= 120 ); } function isHexColor(value: unknown): value is string { return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value); } function isTrustedCalendlyUrl(value: string): boolean { try { const url = new URL(value); return ( url.protocol === "https:" && url.username.length === 0 && url.password.length === 0 && (url.hostname === "calendly.com" || url.hostname.endsWith(".calendly.com")) ); } catch { return false; } }