Skip to main content
Glama
event-validator.js8.79 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TelemetryEventValidator = exports.workflowTelemetrySchema = exports.telemetryEventSchema = void 0; const zod_1 = require("zod"); const logger_1 = require("../utils/logger"); const sanitizedString = zod_1.z.string().transform(val => { let sanitized = val.replace(/https?:\/\/[^\s]+/gi, '[URL]'); sanitized = sanitized.replace(/[a-zA-Z0-9_-]{32,}/g, '[KEY]'); sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]'); return sanitized; }); const eventPropertiesSchema = zod_1.z.record(zod_1.z.unknown()).transform(obj => { const sanitized = {}; for (const [key, value] of Object.entries(obj)) { if (isSensitiveKey(key)) { continue; } if (typeof value === 'string') { sanitized[key] = sanitizedString.parse(value); } else if (typeof value === 'number' || typeof value === 'boolean') { sanitized[key] = value; } else if (value === null || value === undefined) { sanitized[key] = null; } else if (typeof value === 'object') { sanitized[key] = sanitizeNestedObject(value, 3); } } return sanitized; }); exports.telemetryEventSchema = zod_1.z.object({ user_id: zod_1.z.string().min(1).max(64), event: zod_1.z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/), properties: eventPropertiesSchema, created_at: zod_1.z.string().datetime().optional() }); exports.workflowTelemetrySchema = zod_1.z.object({ user_id: zod_1.z.string().min(1).max(64), workflow_hash: zod_1.z.string().min(1).max(64), node_count: zod_1.z.number().int().min(0).max(1000), node_types: zod_1.z.array(zod_1.z.string()).max(100), has_trigger: zod_1.z.boolean(), has_webhook: zod_1.z.boolean(), complexity: zod_1.z.enum(['simple', 'medium', 'complex']), sanitized_workflow: zod_1.z.object({ nodes: zod_1.z.array(zod_1.z.any()).max(1000), connections: zod_1.z.record(zod_1.z.any()) }), created_at: zod_1.z.string().datetime().optional() }); const toolUsagePropertiesSchema = zod_1.z.object({ tool: zod_1.z.string().max(100), success: zod_1.z.boolean(), duration: zod_1.z.number().min(0).max(3600000), }); const searchQueryPropertiesSchema = zod_1.z.object({ query: zod_1.z.string().max(100).transform(val => { let sanitized = val.replace(/https?:\/\/[^\s]+/gi, '[URL]'); sanitized = sanitized.replace(/[a-zA-Z0-9_-]{32,}/g, '[KEY]'); sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]'); return sanitized; }), resultsFound: zod_1.z.number().int().min(0), searchType: zod_1.z.string().max(50), hasResults: zod_1.z.boolean(), isZeroResults: zod_1.z.boolean() }); const validationDetailsPropertiesSchema = zod_1.z.object({ nodeType: zod_1.z.string().max(100), errorType: zod_1.z.string().max(100), errorCategory: zod_1.z.string().max(50), details: zod_1.z.record(zod_1.z.any()).optional() }); const performanceMetricPropertiesSchema = zod_1.z.object({ operation: zod_1.z.string().max(100), duration: zod_1.z.number().min(0).max(3600000), isSlow: zod_1.z.boolean(), isVerySlow: zod_1.z.boolean(), metadata: zod_1.z.record(zod_1.z.any()).optional() }); const startupErrorPropertiesSchema = zod_1.z.object({ checkpoint: zod_1.z.string().max(100), errorMessage: zod_1.z.string().max(500), errorType: zod_1.z.string().max(100), checkpointsPassed: zod_1.z.array(zod_1.z.string()).max(20), checkpointsPassedCount: zod_1.z.number().int().min(0).max(20), startupDuration: zod_1.z.number().min(0).max(300000), platform: zod_1.z.string().max(50), arch: zod_1.z.string().max(50), nodeVersion: zod_1.z.string().max(50), isDocker: zod_1.z.boolean() }); const startupCompletedPropertiesSchema = zod_1.z.object({ version: zod_1.z.string().max(50) }); const EVENT_SCHEMAS = { 'tool_used': toolUsagePropertiesSchema, 'search_query': searchQueryPropertiesSchema, 'validation_details': validationDetailsPropertiesSchema, 'performance_metric': performanceMetricPropertiesSchema, 'startup_error': startupErrorPropertiesSchema, 'startup_completed': startupCompletedPropertiesSchema, }; function isSensitiveKey(key) { const sensitivePatterns = [ 'password', 'passwd', 'pwd', 'token', 'jwt', 'bearer', 'apikey', 'api_key', 'api-key', 'secret', 'private', 'credential', 'cred', 'auth', 'url', 'uri', 'endpoint', 'host', 'hostname', 'database', 'db', 'connection', 'conn', 'slack', 'discord', 'telegram', 'oauth', 'client_secret', 'client-secret', 'clientsecret', 'access_token', 'access-token', 'accesstoken', 'refresh_token', 'refresh-token', 'refreshtoken' ]; const lowerKey = key.toLowerCase(); if (sensitivePatterns.includes(lowerKey)) { return true; } if (lowerKey.includes('key') && lowerKey !== 'key') { const keyPatterns = ['apikey', 'api_key', 'api-key', 'secretkey', 'secret_key', 'privatekey', 'private_key']; if (keyPatterns.some(pattern => lowerKey.includes(pattern))) { return true; } } return sensitivePatterns.some(pattern => { const regex = new RegExp(`(?:^|[_-])${pattern}(?:[_-]|$)`, 'i'); return regex.test(key) || lowerKey.includes(pattern); }); } function sanitizeNestedObject(obj, maxDepth) { if (maxDepth <= 0 || !obj || typeof obj !== 'object') { return '[NESTED]'; } if (Array.isArray(obj)) { return obj.slice(0, 10).map(item => typeof item === 'object' ? sanitizeNestedObject(item, maxDepth - 1) : item); } const sanitized = {}; let keyCount = 0; for (const [key, value] of Object.entries(obj)) { if (keyCount++ >= 20) { sanitized['...'] = 'truncated'; break; } if (isSensitiveKey(key)) { continue; } if (typeof value === 'string') { sanitized[key] = sanitizedString.parse(value); } else if (typeof value === 'object' && value !== null) { sanitized[key] = sanitizeNestedObject(value, maxDepth - 1); } else { sanitized[key] = value; } } return sanitized; } class TelemetryEventValidator { constructor() { this.validationErrors = 0; this.validationSuccesses = 0; } validateEvent(event) { try { const specificSchema = EVENT_SCHEMAS[event.event]; if (specificSchema) { const validatedProperties = specificSchema.safeParse(event.properties); if (!validatedProperties.success) { logger_1.logger.debug(`Event validation failed for ${event.event}:`, validatedProperties.error.errors); this.validationErrors++; return null; } event.properties = validatedProperties.data; } const validated = exports.telemetryEventSchema.parse(event); this.validationSuccesses++; return validated; } catch (error) { if (error instanceof zod_1.z.ZodError) { logger_1.logger.debug('Event validation error:', error.errors); } else { logger_1.logger.debug('Unexpected validation error:', error); } this.validationErrors++; return null; } } validateWorkflow(workflow) { try { const validated = exports.workflowTelemetrySchema.parse(workflow); this.validationSuccesses++; return validated; } catch (error) { if (error instanceof zod_1.z.ZodError) { logger_1.logger.debug('Workflow validation error:', error.errors); } else { logger_1.logger.debug('Unexpected workflow validation error:', error); } this.validationErrors++; return null; } } getStats() { return { errors: this.validationErrors, successes: this.validationSuccesses, total: this.validationErrors + this.validationSuccesses, errorRate: this.validationErrors / (this.validationErrors + this.validationSuccesses) || 0 }; } resetStats() { this.validationErrors = 0; this.validationSuccesses = 0; } } exports.TelemetryEventValidator = TelemetryEventValidator; //# sourceMappingURL=event-validator.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server