UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

295 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandSanitizer = void 0; class CommandSanitizer { static sanitizeTmuxCommand(command, args) { const result = { command, args: [], isValid: true, errors: [] }; if (!this.ALLOWED_TMUX_COMMANDS.has(command)) { result.isValid = false; result.errors.push(`Command '${command}' is not in the allowed list`); return result; } for (const arg of args) { const sanitizedArg = this.sanitizeArgument(arg); if (sanitizedArg.isValid) { result.args.push(sanitizedArg.value); } else { result.isValid = false; result.errors.push(...sanitizedArg.errors); } } return result; } static sanitizeArgument(arg) { const result = { value: arg, isValid: true, errors: [] }; if (arg == null) { result.isValid = false; result.errors.push('Argument cannot be null or undefined'); return result; } const stringArg = String(arg).trim(); if (stringArg.length === 0) { result.isValid = false; result.errors.push('Argument cannot be empty'); return result; } if (stringArg.length > 1000) { result.isValid = false; result.errors.push('Argument exceeds maximum length of 1000 characters'); return result; } for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(stringArg)) { result.isValid = false; result.errors.push(`Argument contains dangerous pattern: ${pattern.source}`); } } result.value = stringArg; return result; } static validateProjectName(projectName) { const result = { isValid: true, errors: [] }; if (!projectName || typeof projectName !== 'string') { result.isValid = false; result.errors.push('Project name must be a non-empty string'); return result; } const trimmed = projectName.trim(); if (trimmed.length === 0) { result.isValid = false; result.errors.push('Project name cannot be empty'); } if (trimmed.length > 50) { result.isValid = false; result.errors.push('Project name cannot exceed 50 characters'); } if (!this.SAFE_IDENTIFIER_PATTERN.test(trimmed)) { result.isValid = false; result.errors.push('Project name can only contain letters, numbers, underscores, and hyphens'); } const reservedNames = ['system', 'admin', 'root', 'tmp', 'var', 'etc', 'bin']; if (reservedNames.includes(trimmed.toLowerCase())) { result.isValid = false; result.errors.push('Project name cannot be a reserved system name'); } return result; } static validateSessionName(sessionName) { return this.validateProjectName(sessionName); } static sanitizeFilePath(filePath, allowedBasePath) { const result = { path: filePath, isValid: true, errors: [] }; if (!filePath || typeof filePath !== 'string') { result.isValid = false; result.errors.push('File path must be a non-empty string'); return result; } const path = require('path'); let normalizedPath; try { normalizedPath = path.normalize(filePath); } catch (error) { result.isValid = false; result.errors.push('Invalid file path format'); return result; } if (normalizedPath.includes('..') || normalizedPath.includes('/.')) { result.isValid = false; result.errors.push('File path contains directory traversal attempts'); } if (allowedBasePath) { const resolvedPath = path.resolve(normalizedPath); const resolvedBase = path.resolve(allowedBasePath); if (!resolvedPath.startsWith(resolvedBase)) { result.isValid = false; result.errors.push('File path is outside of allowed directory'); } } const dangerousPaths = ['/etc', '/var', '/bin', '/sbin', '/usr/bin', '/usr/sbin', '/root']; for (const dangerousPath of dangerousPaths) { if (normalizedPath.startsWith(dangerousPath)) { result.isValid = false; result.errors.push('File path accesses system directory'); } } result.path = normalizedPath; return result; } static createSafeTmuxTarget(sessionName, windowIndex) { const sessionValidation = this.validateSessionName(sessionName); if (!sessionValidation.isValid) { throw new Error(`Invalid session name: ${sessionValidation.errors.join(', ')}`); } const windowIndexNum = typeof windowIndex === 'string' ? parseInt(windowIndex, 10) : windowIndex; if (isNaN(windowIndexNum) || windowIndexNum < 0 || windowIndexNum > 999) { throw new Error('Window index must be a number between 0 and 999'); } return `${sessionName}:${windowIndexNum}`; } static prepareTmuxExecution(command, args) { const sanitized = this.sanitizeTmuxCommand(command, args); if (!sanitized.isValid) { throw new Error(`Command sanitization failed: ${sanitized.errors.join(', ')}`); } return sanitized; } static validateProjectIdea(projectIdea) { const result = { isValid: true, errors: [] }; if (!projectIdea || typeof projectIdea !== 'string') { result.isValid = false; result.errors.push('Project idea must be a non-empty string'); return result; } const trimmed = projectIdea.trim(); if (trimmed.length === 0) { result.isValid = false; result.errors.push('Project idea cannot be empty'); } if (trimmed.length < 10) { result.isValid = false; result.errors.push('Project idea must be at least 10 characters long'); } if (trimmed.length > 5000) { result.isValid = false; result.errors.push('Project idea cannot exceed 5000 characters'); } for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(trimmed)) { result.isValid = false; result.errors.push('Project idea contains potentially dangerous content'); } } return result; } static validateQualityRequirements(qualityRequirements) { const result = { isValid: true, errors: [] }; if (!qualityRequirements || typeof qualityRequirements !== 'string') { result.isValid = false; result.errors.push('Quality requirements must be a non-empty string'); return result; } const validQualityLevels = ['lite', 'standard', 'high', 'critical']; if (!validQualityLevels.includes(qualityRequirements.toLowerCase())) { result.isValid = false; result.errors.push(`Quality requirements must be one of: ${validQualityLevels.join(', ')}`); } return result; } static validateTimeString(timeString) { const result = { isValid: true, errors: [] }; if (!timeString || typeof timeString !== 'string') { return result; } const trimmed = timeString.trim(); const timeFormats = [ /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, /^\d{4}-\d{2}-\d{2} ([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, /^\d{4}-\d{2}-\d{2}T([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.\d{3})?Z?$/ ]; const isValidFormat = timeFormats.some(format => format.test(trimmed)); if (!isValidFormat) { result.isValid = false; result.errors.push('Invalid time format. Expected HH:MM, YYYY-MM-DD HH:MM, or ISO 8601 format'); } return result; } static validateReference(reference) { const result = { isValid: true, errors: [] }; if (!reference || typeof reference !== 'string') { return result; } const trimmed = reference.trim(); if (trimmed.length > 200) { result.isValid = false; result.errors.push('Reference cannot exceed 200 characters'); } for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(trimmed)) { result.isValid = false; result.errors.push('Reference contains potentially dangerous content'); } } return result; } static validateProjectInputs(inputs) { const result = { isValid: true, errors: [] }; const projectIdeaValidation = this.validateProjectIdea(inputs.projectIdea); if (!projectIdeaValidation.isValid) { result.isValid = false; result.errors.push(...projectIdeaValidation.errors.map(err => `Project Idea: ${err}`)); } const projectNameValidation = this.validateProjectName(inputs.projectName); if (!projectNameValidation.isValid) { result.isValid = false; result.errors.push(...projectNameValidation.errors.map(err => `Project Name: ${err}`)); } const projectPathValidation = this.sanitizeFilePath(inputs.projectPath); if (!projectPathValidation.isValid) { result.isValid = false; result.errors.push(...projectPathValidation.errors.map(err => `Project Path: ${err}`)); } const qualityValidation = this.validateQualityRequirements(inputs.qualityRequirements); if (!qualityValidation.isValid) { result.isValid = false; result.errors.push(...qualityValidation.errors.map(err => `Quality Requirements: ${err}`)); } if (inputs.preferredStartTime) { const timeValidation = this.validateTimeString(inputs.preferredStartTime); if (!timeValidation.isValid) { result.isValid = false; result.errors.push(...timeValidation.errors.map(err => `Start Time: ${err}`)); } } if (inputs.reference) { const referenceValidation = this.validateReference(inputs.reference); if (!referenceValidation.isValid) { result.isValid = false; result.errors.push(...referenceValidation.errors.map(err => `Reference: ${err}`)); } } return result; } } exports.CommandSanitizer = CommandSanitizer; CommandSanitizer.ALLOWED_TMUX_COMMANDS = new Set([ 'new-session', 'new-window', 'send-keys', 'capture-pane', 'list-sessions', 'list-windows', 'kill-session', 'kill-window', 'has-session', 'display-message' ]); CommandSanitizer.DANGEROUS_PATTERNS = [ /[;&|`$(){}]/, /\.\./, /[<>]/, /\s*rm\s/i, /\s*sudo\s/i, /\s*chmod\s/i, /\s*wget\s/i, /\s*curl\s/i, /\s*ssh\s/i, /\s*nc\s/i, /\\\\/, ]; CommandSanitizer.SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z0-9_-]+$/; //# sourceMappingURL=commandSanitizer.js.map