Skip to main content
Glama
workflow-sanitizer.js9.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowSanitizer = void 0; const crypto_1 = require("crypto"); class WorkflowSanitizer { static sanitizeWorkflow(workflow) { const sanitized = JSON.parse(JSON.stringify(workflow)); if (sanitized.nodes && Array.isArray(sanitized.nodes)) { sanitized.nodes = sanitized.nodes.map((node) => this.sanitizeNode(node)); } if (sanitized.connections) { sanitized.connections = this.sanitizeConnections(sanitized.connections); } delete sanitized.settings?.errorWorkflow; delete sanitized.staticData; delete sanitized.pinData; delete sanitized.credentials; delete sanitized.sharedWorkflows; delete sanitized.ownedBy; delete sanitized.createdBy; delete sanitized.updatedBy; const nodeTypes = sanitized.nodes?.map((n) => n.type) || []; const uniqueNodeTypes = [...new Set(nodeTypes)]; const hasTrigger = nodeTypes.some((type) => type.includes('trigger') || type.includes('webhook')); const hasWebhook = nodeTypes.some((type) => type.includes('webhook')); const nodeCount = sanitized.nodes?.length || 0; let complexity = 'simple'; if (nodeCount > 20) { complexity = 'complex'; } else if (nodeCount > 10) { complexity = 'medium'; } const workflowStructure = JSON.stringify({ nodeTypes: uniqueNodeTypes.sort(), connections: sanitized.connections }); const workflowHash = (0, crypto_1.createHash)('sha256') .update(workflowStructure) .digest('hex') .substring(0, 16); return { nodes: sanitized.nodes || [], connections: sanitized.connections || {}, nodeCount, nodeTypes: uniqueNodeTypes, hasTrigger, hasWebhook, complexity, workflowHash }; } static sanitizeNode(node) { const sanitized = { ...node }; delete sanitized.credentials; if (sanitized.parameters) { sanitized.parameters = this.sanitizeObject(sanitized.parameters); } return sanitized; } static sanitizeObject(obj) { if (!obj || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.sanitizeObject(item)); } const sanitized = {}; for (const [key, value] of Object.entries(obj)) { const isSensitive = this.isSensitiveField(key); const isUrlField = key.toLowerCase().includes('url') || key.toLowerCase().includes('endpoint') || key.toLowerCase().includes('webhook'); if (typeof value === 'object' && value !== null) { if (isSensitive && !isUrlField) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = this.sanitizeObject(value); } } else if (typeof value === 'string') { if (isSensitive && !isUrlField) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = this.sanitizeString(value, key); } } else if (isSensitive) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized; } static sanitizeString(value, fieldName) { if (value.includes('/webhook/') || value.includes('/hook/')) { return 'https://[webhook-url]'; } let sanitized = value; for (const patternDef of this.SENSITIVE_PATTERNS) { if (patternDef.placeholder.includes('WEBHOOK')) { continue; } if (sanitized.includes('[REDACTED')) { break; } if (patternDef.placeholder === '[REDACTED_URL_WITH_AUTH]') { const matches = value.match(patternDef.pattern); if (matches) { for (const match of matches) { const fullUrlMatch = value.indexOf(match); if (fullUrlMatch !== -1) { const afterUrl = value.substring(fullUrlMatch + match.length); if (afterUrl && afterUrl.startsWith('/')) { const pathPart = afterUrl.split(/[\s?&#]/)[0]; sanitized = sanitized.replace(match + pathPart, patternDef.placeholder + pathPart); } else { sanitized = sanitized.replace(match, patternDef.placeholder); } } } } continue; } sanitized = sanitized.replace(patternDef.pattern, patternDef.placeholder); } if (fieldName.toLowerCase().includes('url') || fieldName.toLowerCase().includes('endpoint')) { if (sanitized.startsWith('http://') || sanitized.startsWith('https://')) { if (sanitized.includes('[REDACTED_URL_WITH_AUTH]')) { return sanitized; } if (sanitized.includes('[REDACTED]')) { return sanitized; } const urlParts = sanitized.split('/'); if (urlParts.length > 2) { urlParts[2] = '[domain]'; sanitized = urlParts.join('/'); } } } return sanitized; } static isSensitiveField(fieldName) { const lowerFieldName = fieldName.toLowerCase(); return this.SENSITIVE_FIELDS.some(sensitive => lowerFieldName.includes(sensitive.toLowerCase())); } static sanitizeConnections(connections) { if (!connections || typeof connections !== 'object') { return connections; } const sanitized = {}; for (const [nodeId, nodeConnections] of Object.entries(connections)) { if (typeof nodeConnections === 'object' && nodeConnections !== null) { sanitized[nodeId] = {}; for (const [connType, connArray] of Object.entries(nodeConnections)) { if (Array.isArray(connArray)) { sanitized[nodeId][connType] = connArray.map((conns) => { if (Array.isArray(conns)) { return conns.map((conn) => ({ node: conn.node, type: conn.type, index: conn.index })); } return conns; }); } else { sanitized[nodeId][connType] = connArray; } } } else { sanitized[nodeId] = nodeConnections; } } return sanitized; } static generateWorkflowHash(workflow) { const sanitized = this.sanitizeWorkflow(workflow); return sanitized.workflowHash; } static sanitizeWorkflowRaw(workflow) { const sanitized = JSON.parse(JSON.stringify(workflow)); if (sanitized.nodes && Array.isArray(sanitized.nodes)) { sanitized.nodes = sanitized.nodes.map((node) => this.sanitizeNode(node)); } if (sanitized.connections) { sanitized.connections = this.sanitizeConnections(sanitized.connections); } delete sanitized.settings?.errorWorkflow; delete sanitized.staticData; delete sanitized.pinData; delete sanitized.credentials; delete sanitized.sharedWorkflows; delete sanitized.ownedBy; delete sanitized.createdBy; delete sanitized.updatedBy; return sanitized; } } exports.WorkflowSanitizer = WorkflowSanitizer; WorkflowSanitizer.SENSITIVE_PATTERNS = [ { pattern: /https?:\/\/[^\s/]+\/webhook\/[^\s]+/g, placeholder: '[REDACTED_WEBHOOK]' }, { pattern: /https?:\/\/[^\s/]+\/hook\/[^\s]+/g, placeholder: '[REDACTED_WEBHOOK]' }, { pattern: /https?:\/\/[^:]+:[^@]+@[^\s/]+/g, placeholder: '[REDACTED_URL_WITH_AUTH]' }, { pattern: /wss?:\/\/[^:]+:[^@]+@[^\s/]+/g, placeholder: '[REDACTED_URL_WITH_AUTH]' }, { pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@[^\s]+/g, placeholder: '[REDACTED_URL_WITH_AUTH]' }, { pattern: /sk-[a-zA-Z0-9]{16,}/g, placeholder: '[REDACTED_APIKEY]' }, { pattern: /Bearer\s+[^\s]+/gi, placeholder: 'Bearer [REDACTED]', preservePrefix: true }, { pattern: /\b[a-zA-Z0-9_-]{32,}\b/g, placeholder: '[REDACTED_TOKEN]' }, { pattern: /\b[a-zA-Z0-9_-]{20,31}\b/g, placeholder: '[REDACTED]' }, ]; WorkflowSanitizer.SENSITIVE_FIELDS = [ 'apiKey', 'api_key', 'token', 'secret', 'password', 'credential', 'auth', 'authorization', 'webhook', 'webhookUrl', 'url', 'endpoint', 'host', 'server', 'database', 'connectionString', 'privateKey', 'publicKey', 'certificate', ]; //# sourceMappingURL=workflow-sanitizer.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