/** * Mandu Kitchen DevTools - Event Protocol * @version 1.0.3 */ import type { KitchenEvent, NormalizedError, IslandSnapshot, NetworkRequest, DevToolsGuardViolation, } from './types'; // ============================================================================ // Event Type Definitions // ============================================================================ export type KitchenEvents = // Error events | KitchenEvent<'error', NormalizedError> | KitchenEvent<'error:clear', { id?: string }> // Island events | KitchenEvent<'island:register', IslandSnapshot> | KitchenEvent<'island:hydrate:start', { id: string }> | KitchenEvent<'island:hydrate:end', { id: string; time: number }> | KitchenEvent<'island:error', { id: string; error: NormalizedError }> // Network events | KitchenEvent<'network:request', NetworkRequest> | KitchenEvent<'network:response', { id: string; status: number; endTime: number }> | KitchenEvent<'network:chunk', { id: string; chunkIndex: number; size: number }> | KitchenEvent<'network:error', { id: string; error: string }> // Guard events | KitchenEvent<'guard:violation', DevToolsGuardViolation> | KitchenEvent<'guard:clear', { ruleId?: string }> // HMR events | KitchenEvent<'hmr:update', { routeId: string; timestamp: number }> | KitchenEvent<'hmr:error', { message: string; stack?: string }> | KitchenEvent<'hmr:connected', { url: string }> | KitchenEvent<'hmr:disconnected', { reason?: string }>; // ============================================================================ // Event Type Guards // ============================================================================ export function isErrorEvent( event: KitchenEvent ): event is KitchenEvent<'error', NormalizedError> { return event.type === 'error'; } export function isIslandEvent( event: KitchenEvent ): event is KitchenEvent<'island:register', IslandSnapshot> { return event.type.startsWith('island:'); } export function isNetworkEvent(event: KitchenEvent): boolean { return event.type.startsWith('network:'); } export function isGuardEvent(event: KitchenEvent): boolean { return event.type.startsWith('guard:'); } export function isHmrEvent(event: KitchenEvent): boolean { return event.type.startsWith('hmr:'); } // ============================================================================ // Event Factory Functions // ============================================================================ let eventIdCounter = 0; function generateEventId(): string { return `mk-${Date.now()}-${++eventIdCounter}`; } export function createErrorEvent(error: Omit): KitchenEvents { return { type: 'error', timestamp: Date.now(), data: { ...error, id: generateEventId(), timestamp: Date.now(), }, }; } export function createIslandRegisterEvent(island: Omit): KitchenEvents { return { type: 'island:register', timestamp: Date.now(), data: { ...island, id: generateEventId(), }, }; } export function createIslandHydrateStartEvent(id: string): KitchenEvents { return { type: 'island:hydrate:start', timestamp: Date.now(), data: { id }, }; } export function createIslandHydrateEndEvent(id: string, time: number): KitchenEvents { return { type: 'island:hydrate:end', timestamp: Date.now(), data: { id, time }, }; } export function createNetworkRequestEvent( request: Omit ): KitchenEvents { return { type: 'network:request', timestamp: Date.now(), data: { ...request, id: generateEventId(), startTime: Date.now(), }, }; } export function createNetworkResponseEvent( id: string, status: number ): KitchenEvents { return { type: 'network:response', timestamp: Date.now(), data: { id, status, endTime: Date.now() }, }; } export function createDevToolsGuardViolationEvent( violation: Omit ): KitchenEvents { return { type: 'guard:violation', timestamp: Date.now(), data: { ...violation, id: generateEventId(), timestamp: Date.now(), }, }; } export function createHmrUpdateEvent(routeId: string): KitchenEvents { return { type: 'hmr:update', timestamp: Date.now(), data: { routeId, timestamp: Date.now() }, }; } export function createHmrErrorEvent(message: string, stack?: string): KitchenEvents { return { type: 'hmr:error', timestamp: Date.now(), data: { message, stack }, }; } // ============================================================================ // Constants // ============================================================================ export const DEVTOOLS_VERSION = '1.0.3'; export const DEFAULT_CONFIG = { enabled: true, position: 'bottom-right' as const, defaultOpen: false, theme: 'auto' as const, features: { errorOverlay: true, islandsInspector: true, networkMonitor: true, guardViewer: true, }, dataSafety: { stringMode: 'smart' as const, collectUserActions: false, collectCodeContext: false, customRedactPatterns: [], }, network: { collectBody: false, bodyMaxBytes: 10_000, }, persistence: { enabled: true, maxPersistEvents: 50, maxPersistBytes: 2_000_000, priority: 'errors-first' as const, incremental: { enabled: true, idleSyncMs: 300, }, }, }; // ============================================================================ // Header Allowlist/Blocklist for Network Masking // ============================================================================ export const ALLOWED_HEADERS = new Set([ 'content-type', 'content-length', 'accept', 'cache-control', 'accept-language', 'accept-encoding', 'user-agent', ]); export const BLOCKED_HEADERS = new Set([ 'authorization', 'cookie', 'set-cookie', 'x-csrf-token', 'x-api-key', 'x-auth-token', ]);