/** * @file safe-json.ts * @description Safe JSON parsing utilities with runtime type validation. * Prevents runtime crashes from corrupted or tampered localStorage/API data. */ /** * Safely parse a JSON string with runtime type validation. * Returns the fallback value if parsing fails or the parsed data * doesn't match the expected shape. * * @param json - The JSON string to parse * @param validator - A type guard function that validates the parsed data * @param fallback - The default value to return on failure * @returns The validated parsed data or the fallback value */ export function safeJsonParse( json: string, validator: (data: unknown) => data is T, fallback: T ): T { try { const parsed: unknown = JSON.parse(json); if (validator(parsed)) { return parsed; } console.warn("safeJsonParse: parsed data failed validation, returning fallback"); return fallback; } catch { console.warn("safeJsonParse: failed to parse JSON, returning fallback"); return fallback; } } /** * Type guard: validates that a value is a non-null object */ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * Type guard: validates a chat Message shape * Checks for required fields: id (string), role (valid enum), content (string), createdAt (string) */ export function isMessage(value: unknown): boolean { if (!isRecord(value)) return false; const validRoles = ["user", "assistant", "system"]; return ( typeof value.id === "string" && typeof value.role === "string" && validRoles.includes(value.role) && typeof value.content === "string" && typeof value.createdAt === "string" ); } /** * Type guard: validates an array of chat Messages */ export function isMessageArray(value: unknown): value is { id: string; role: "user" | "assistant" | "system"; content: string; createdAt: string; thinking?: string; model?: string; provider?: string; promptTokens?: number; completionTokens?: number; totalTokens?: number; }[] { return Array.isArray(value) && value.every(isMessage); } /** * Type guard: validates ChatOverlayState shape * Expects { isOpen: boolean, isMinimized: boolean } */ export function isChatOverlayState( value: unknown ): value is { isOpen: boolean; isMinimized: boolean } { if (!isRecord(value)) return false; return typeof value.isOpen === "boolean" && typeof value.isMinimized === "boolean"; } /** * Type guard: validates a WidgetPlacement shape * Checks for required fields: i (string), x/y/w/h (numbers) */ function isWidgetPlacement(value: unknown): boolean { if (!isRecord(value)) return false; return ( typeof value.i === "string" && typeof value.x === "number" && typeof value.y === "number" && typeof value.w === "number" && typeof value.h === "number" ); } /** * Type guard: validates a LayoutConfig shape * Expects { id: string, name: string, layout: WidgetPlacement[] } */ function isLayoutConfig(value: unknown): boolean { if (!isRecord(value)) return false; return ( typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.layout) && (value.layout as unknown[]).every(isWidgetPlacement) ); } /** * Type guard: validates a Record shape. * Uses a branded type approach to ensure compatibility with LayoutConfig consumers. */ export function isLayoutConfigRecord( value: unknown ): value is Record { if (!isRecord(value)) return false; return Object.values(value).every(isLayoutConfig); }