UNPKG

guardz-event

Version:

Type-safe event handling with runtime validation using guardz for unsafe data from 3rd parties

973 lines 32.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SafeEventBuilder = exports.Status = void 0; exports.safePostMessageListener = safePostMessageListener; exports.safeWebSocketListener = safeWebSocketListener; exports.safeDOMEventListener = safeDOMEventListener; exports.safeEventSourceListener = safeEventSourceListener; exports.safeCustomEventListener = safeCustomEventListener; exports.safeStorageEventListener = safeStorageEventListener; exports.safeDOMEvent = safeDOMEvent; exports.safePostMessage = safePostMessage; exports.safeWebSocket = safeWebSocket; exports.safeEventSource = safeEventSource; exports.safeCustomEvent = safeCustomEvent; exports.safeStorageEvent = safeStorageEvent; exports.safeEvent = safeEvent; exports.safe = safe; exports.createSafeEventContext = createSafeEventContext; exports.createTypedSafeDOMEvent = createTypedSafeDOMEvent; exports.createTypedSafePostMessage = createTypedSafePostMessage; exports.createTypedSafeWebSocket = createTypedSafeWebSocket; exports.createTypedSafeEventSource = createTypedSafeEventSource; exports.createTypedSafeCustomEvent = createTypedSafeCustomEvent; exports.createTypedSafeStorageEvent = createTypedSafeStorageEvent; const Status_1 = require("../domain/event/Status"); Object.defineProperty(exports, "Status", { enumerable: true, get: function () { return Status_1.Status; } }); // =============================== // Ergonomic Event Listeners (New API) // =============================== /** * Safe PostMessage event listener with callback-based API * Usage: window.addEventListener('message', safePostMessageListener(isUserMessage, { tolerance: true, onTypeMismatch: console.warn })); */ function safePostMessageListener(guard, config) { return (event) => { const identifier = 'postMessage'; // Handle null/undefined events if (!event) { if (config.onError) { try { config.onError('Invalid event: Event is null or undefined', { type: 'unknown', eventType: 'message', identifier: identifier, }); } catch { // Ignore callback errors } } return; } // Check for security violations first if (config.allowedOrigins && !config.allowedOrigins.includes(event.origin)) { const errorMessage = `PostMessage origin not allowed: ${event.origin || 'null'}`; if (config.onSecurityViolation) { try { config.onSecurityViolation(event.origin || 'null', errorMessage); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(errorMessage, { type: 'security', eventType: 'message', origin: event.origin || 'null', identifier: identifier, }); } catch { // Ignore callback errors } } return; } const result = executePostMessageValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError && context.type !== 'security' && context.type !== 'validation') { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } /** * Safe WebSocket event listener with callback-based API * Usage: ws.addEventListener('message', safeWebSocketListener(isWSData, { onSuccess: handleWSData })); */ function safeWebSocketListener(guard, config) { return (event) => { const identifier = 'websocket'; const result = executeWebSocketValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } /** * Safe DOM event listener with callback-based API * Usage: element.addEventListener('click', safeDOMEventListener(isClickData, { onSuccess: handleClick })); */ function safeDOMEventListener(guard, config) { return (event) => { const identifier = 'dom'; const result = executeEventValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } /** * Safe EventSource event listener with callback-based API * Usage: eventSource.addEventListener('message', safeEventSourceListener(isSSEData, { onSuccess: handleSSEData })); */ function safeEventSourceListener(guard, config) { return (event) => { const identifier = 'eventsource'; const result = executeEventSourceValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } /** * Safe CustomEvent listener with callback-based API * Usage: element.addEventListener('customEvent', safeCustomEventListener(isCustomData, { onSuccess: handleCustomData })); */ function safeCustomEventListener(guard, config) { return (event) => { const identifier = 'customEvent'; const result = executeCustomEventValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } /** * Safe StorageEvent listener with callback-based API * Usage: window.addEventListener('storage', safeStorageEventListener(isStorageData, { onSuccess: handleStorageData })); */ function safeStorageEventListener(guard, config) { return (event) => { const identifier = 'storage'; const result = executeStorageEventValidation(event, { guard, tolerance: config.tolerance, allowedOrigins: config.allowedOrigins, allowedSources: config.allowedSources, identifier, onError: (error, context) => { // Handle security violations if (context.type === 'security' && config.onSecurityViolation) { try { config.onSecurityViolation(context.origin || 'null', error); } catch { // Ignore callback errors } } else if (context.type === 'validation' && context.type === 'validation' && config.onTypeMismatch) { try { config.onTypeMismatch(error); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(error, context); } catch { // Ignore callback errors } } }, }); if (result.status === Status_1.Status.SUCCESS) { try { config.onSuccess(result.data); } catch { // Ignore callback errors } } else { // In tolerance mode, validation errors should call onTypeMismatch if (result.code === 500 && config.onTypeMismatch) { try { config.onTypeMismatch(result.message); } catch { // Ignore callback errors } } else if (config.onError) { try { config.onError(result); } catch { // Ignore callback errors } } } }; } // =============================== // Legacy API (Result-based) // =============================== /** * Safe DOM event handler (legacy API) */ function safeDOMEvent(config) { return (event) => { return executeEventValidation(event, config); }; } /** * Safe PostMessage handler (legacy API) */ function safePostMessage(config) { return (event) => { return executePostMessageValidation(event, config); }; } /** * Safe WebSocket handler (legacy API) */ function safeWebSocket(config) { return (event) => { return executeWebSocketValidation(event, config); }; } /** * Safe EventSource handler (legacy API) */ function safeEventSource(config) { return (event) => { return executeEventSourceValidation(event, config); }; } /** * Safe CustomEvent handler (legacy API) */ function safeCustomEvent(config) { return (event) => { return executeCustomEventValidation(event, config); }; } /** * Safe StorageEvent handler (legacy API) */ function safeStorageEvent(config) { return (event) => { return executeStorageEventValidation(event, config); }; } /** * Generic safe event handler (legacy API) */ async function safeEvent(eventConfig) { const { event, ...config } = eventConfig; if (event instanceof MessageEvent) { return executePostMessageValidation(event, config); } else if (event instanceof CustomEvent) { return executeCustomEventValidation(event, config); } else if (event instanceof StorageEvent) { return executeStorageEventValidation(event, config); } else { return executeEventValidation(event, config); } } // =============================== // Builder API // =============================== class SafeEventBuilder { constructor() { this.config = {}; this.eventType = ''; } domEvent(eventType) { this.eventType = eventType; return this; } postMessage() { this.eventType = 'postmessage'; return this; } webSocket() { this.eventType = 'websocket'; return this; } eventSource() { this.eventType = 'eventsource'; return this; } customEvent(eventType) { this.eventType = eventType; return this; } storageEvent() { this.eventType = 'storage'; return this; } guard(guardFn) { const newBuilder = new SafeEventBuilder(); newBuilder.config = { ...this.config, guard: guardFn }; newBuilder.eventType = this.eventType; return newBuilder; } tolerance(enabled = true) { this.config.tolerance = enabled; return this; } identifier(id) { this.config.identifier = id; return this; } timeout(ms) { this.config.timeout = ms; return this; } allowedOrigins(origins) { this.config.allowedOrigins = origins; return this; } allowedSources(sources) { this.config.allowedSources = sources; return this; } onError(callback) { this.config.onError = callback; return this; } createHandler() { if (!this.config.guard) { throw new Error('Guard function is required'); } // Set identifier if not already set if (!this.config.identifier) { if (this.eventType === 'postmessage') this.config.identifier = 'postMessage'; else if (this.eventType) this.config.identifier = this.eventType; } const config = this.config; switch (this.eventType) { case 'postmessage': return safePostMessage(config); case 'websocket': return safeWebSocket(config); case 'eventsource': return safeEventSource(config); case 'storage': return safeStorageEvent(config); default: return safeDOMEvent(config); } } } exports.SafeEventBuilder = SafeEventBuilder; function safe() { return new SafeEventBuilder(); } function createSafeEventContext(contextConfig = {}) { const { defaultTolerance = false, allowedOrigins, allowedSources, defaultTimeout, onError, } = contextConfig; const createConfig = (config) => ({ tolerance: defaultTolerance, allowedOrigins, allowedSources, timeout: defaultTimeout, onError, ...config, }); return { domEvent: (eventType, config) => { return safeDOMEvent(createConfig(config)); }, postMessage: (config) => { return safePostMessage(createConfig(config)); }, webSocket: (config) => { return safeWebSocket(createConfig(config)); }, eventSource: (config) => { return safeEventSource(createConfig(config)); }, customEvent: (eventType, config) => { return safeCustomEvent(createConfig(config)); }, storageEvent: (config) => { return safeStorageEvent(createConfig(config)); }, }; } function buildErrorContext({ type, eventType, origin, source, originalError, identifier, }) { return { type: type || 'unknown', eventType: eventType || 'unknown', origin, source, originalError, identifier, }; } // =============================== // Core Validation Functions // =============================== /** * Extract data from event objects with optimized performance */ function extractEventData(event, tolerance = false) { // Fast path for common event types if (event instanceof MessageEvent) { return event.data; } if (event instanceof CustomEvent) { return event.detail; } if (event instanceof StorageEvent) { const data = { key: event.key, newValue: event.newValue, oldValue: event.oldValue, url: event.url, }; // Only include storageArea in tolerance mode if (tolerance) { data.storageArea = event.storageArea; } return data; } if (event instanceof MouseEvent) { return { clientX: event.clientX, clientY: event.clientY, button: event.button, }; } // Generic event data extraction return { type: event.type, target: event.target, currentTarget: event.currentTarget, eventPhase: event.eventPhase, bubbles: event.bubbles, cancelable: event.cancelable, defaultPrevented: event.defaultPrevented, composed: event.composed, timeStamp: event.timeStamp, isTrusted: event.isTrusted, }; } /** * Validate message security with optimized performance */ function validateMessageSecurity(event, config) { // Origin validation if (config.allowedOrigins && !config.allowedOrigins.includes(event.origin)) { const errorMessage = `Origin not allowed: ${event.origin || 'null'}`; if (config.onError) { try { config.onError(errorMessage, buildErrorContext({ type: 'security', eventType: 'message', origin: event.origin || 'null', identifier: config.identifier || 'postMessage', })); } catch { // Ignore callback errors } } return { status: Status_1.Status.ERROR, code: 403, message: errorMessage, }; } // Source validation if (config.allowedSources) { const source = event.source; const isAllowedSource = config.allowedSources.some(allowedSource => { if (typeof allowedSource === 'string') { if (typeof source === 'string') { return source === allowedSource; } if (source && typeof source === 'object') { return (source[allowedSource] === true || source.name === allowedSource || source.id === allowedSource); } } return false; }); if (!isAllowedSource) { const errorMessage = `Source not allowed: ${source || 'null'}`; if (config.onError) { try { config.onError(errorMessage, buildErrorContext({ type: 'security', eventType: 'message', source: String(source), origin: event.origin || undefined, identifier: config.identifier || 'postMessage', })); } catch { // Ignore callback errors } } return { status: Status_1.Status.ERROR, code: 403, message: errorMessage, }; } } return null; // No security violation } /** * Validate event data with optimized performance */ function validateEventData(data, event, config) { if (config.guard(data)) { return null; // Validation passed } const errorMessage = `Validation failed: ${config.identifier || (event instanceof MessageEvent ? 'postMessage' : 'event data')}`; const errorContext = buildErrorContext({ type: 'validation', eventType: event.type, originalError: new Error(errorMessage), identifier: config.identifier || (event instanceof MessageEvent ? 'postMessage' : 'event data'), }); if (config.tolerance) { // In tolerance mode, check if this is completely invalid data or partial data if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || data == null) { if (config.onError) { try { config.onError(errorMessage, errorContext); } catch { // Ignore callback errors } } return { status: Status_1.Status.ERROR, code: 500, message: errorMessage, }; } else { if (config.onError) { try { config.onError(errorMessage, errorContext); } catch { // Ignore callback errors } } return { status: Status_1.Status.SUCCESS, data: data, }; } } else { if (config.onError) { try { config.onError(errorMessage, errorContext); } catch { // Ignore callback errors } } return { status: Status_1.Status.ERROR, code: 500, message: errorMessage, }; } } function executeEventValidation(event, config) { try { // Basic event validation if (!event || typeof event !== 'object') { return { status: Status_1.Status.ERROR, code: 500, message: 'Invalid event: Event is null, undefined, or not an object', }; } // Extract data from event using optimized data extraction const data = extractEventData(event, config.tolerance); // Security validation with optimized performance if (event instanceof MessageEvent) { const securityResult = validateMessageSecurity(event, config); if (securityResult) { return securityResult; } } // Type validation with optimized performance const validationResult = validateEventData(data, event, config); if (validationResult) { return validationResult; } return { status: Status_1.Status.SUCCESS, data: data, }; } catch (error) { const errorMessage = `Unexpected error during event validation: ${error instanceof Error ? error.message : String(error)}`; if (config.onError) { try { config.onError(errorMessage, buildErrorContext({ type: 'unknown', eventType: event?.type || 'unknown', originalError: error, identifier: config.identifier || (event instanceof MessageEvent ? 'postMessage' : 'event data'), })); } catch { // Ignore callback errors } } return { status: Status_1.Status.ERROR, code: 500, message: errorMessage, }; } } function executePostMessageValidation(event, config) { return executeEventValidation(event, config); } function executeWebSocketValidation(event, config) { return executeEventValidation(event, config); } function executeEventSourceValidation(event, config) { return executeEventValidation(event, config); } function executeCustomEventValidation(event, config) { return executeEventValidation(event, config); } function executeStorageEventValidation(event, config) { return executeEventValidation(event, config); } function createTypedSafeDOMEvent(guard) { return (config) => safeDOMEvent({ ...config, guard }); } function createTypedSafePostMessage(guard) { return (config) => safePostMessage({ ...config, guard }); } function createTypedSafeWebSocket(guard) { return (config) => safeWebSocket({ ...config, guard }); } function createTypedSafeEventSource(guard) { return (config) => safeEventSource({ ...config, guard }); } function createTypedSafeCustomEvent(guard) { return (config) => safeCustomEvent({ ...config, guard }); } function createTypedSafeStorageEvent(guard) { return (config) => safeStorageEvent({ ...config, guard }); } //# sourceMappingURL=safe-event-never-throws.js.map