mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
227 lines
8.6 KiB
JavaScript
227 lines
8.6 KiB
JavaScript
"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
|