@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
295 lines • 11.6 kB
JavaScript
;
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