guardz-event
Version:
Type-safe event handling with runtime validation using guardz for unsafe data from 3rd parties
973 lines • 32.1 kB
JavaScript
"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