UNPKG

@necto-ai/pgit

Version:

Private file tracking with dual git repositories

351 lines 13.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidationSchemas = exports.InputValidator = void 0; const path = __importStar(require("path")); const zod_1 = require("zod"); const specific_errors_1 = require("../errors/specific.errors"); /** * Input validation service for security and data integrity */ class InputValidator { /** * Validate file path for security and correctness */ static validatePath(inputPath, options = {}) { const result = { isValid: true, normalizedPath: '', issues: [], securityRisk: false, }; // Basic validation if (!inputPath || typeof inputPath !== 'string') { result.isValid = false; result.issues.push('Path must be a non-empty string'); return result; } // Trim whitespace but preserve if it's significant const trimmedPath = inputPath.trim(); if (trimmedPath !== inputPath) { result.issues.push('Path has leading or trailing whitespace'); result.securityRisk = true; } // Check length const maxLength = options.maxPathLength || this.MAX_PATH_LENGTH; if (trimmedPath.length > maxLength) { result.isValid = false; result.issues.push(`Path exceeds maximum length of ${maxLength} characters`); return result; } // Security pattern checks for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(trimmedPath)) { result.isValid = false; result.securityRisk = true; result.issues.push(`Path contains dangerous pattern: ${pattern.source}`); } } // Check for blocked directory names const pathSegments = trimmedPath.split(/[/\\]/); for (const segment of pathSegments) { if (this.BLOCKED_DIRECTORIES.includes(segment.toLowerCase())) { result.isValid = false; result.securityRisk = true; result.issues.push(`Path contains blocked directory: ${segment}`); } } // Absolute path check if (path.isAbsolute(trimmedPath) && !options.allowAbsolutePaths) { result.isValid = false; result.securityRisk = true; result.issues.push('Absolute paths are not allowed'); } // Parent directory traversal check const normalized = path.normalize(trimmedPath); if (normalized.startsWith('..') && !options.allowParentDirectory) { result.isValid = false; result.securityRisk = true; result.issues.push('Parent directory traversal is not allowed'); } // Extension validation if (options.requiredExtensions && options.requiredExtensions.length > 0) { const ext = path.extname(normalized).toLowerCase(); if (!options.requiredExtensions.includes(ext)) { result.isValid = false; result.issues.push(`File must have one of these extensions: ${options.requiredExtensions.join(', ')}`); } } // Custom pattern checks if (options.blockedPatterns) { for (const pattern of options.blockedPatterns) { if (pattern.test(normalized)) { result.isValid = false; result.issues.push(`Path matches blocked pattern: ${pattern.source}`); } } } result.normalizedPath = normalized; return result; } /** * Validate commit message */ static validateCommitMessage(message) { if (!message || typeof message !== 'string') { throw new specific_errors_1.MissingArgumentError('Commit message is required', 'commit'); } const trimmed = message.trim(); if (trimmed.length < 3) { throw new specific_errors_1.InvalidArgumentError('Commit message must be at least 3 characters', 'commit'); } if (trimmed.length > 500) { throw new specific_errors_1.InvalidArgumentError('Commit message must not exceed 500 characters', 'commit'); } // Check for dangerous characters // eslint-disable-next-line no-control-regex if (/[\x00-\x08\x0E-\x1F\x7F]/.test(trimmed)) { throw new specific_errors_1.SecurityError('Commit message contains invalid control characters', 'commit'); } } /** * Validate branch name */ static validateBranchName(branchName) { if (!branchName || typeof branchName !== 'string') { throw new specific_errors_1.MissingArgumentError('Branch name is required', 'branch'); } const trimmed = branchName.trim(); // Git branch name rules const validBranchName = /^[a-zA-Z0-9._/-]+$/; if (!validBranchName.test(trimmed)) { throw new specific_errors_1.InvalidArgumentError('Branch name contains invalid characters', 'branch'); } // Additional Git restrictions if (trimmed.startsWith('-') || trimmed.endsWith('.') || trimmed.includes('..')) { throw new specific_errors_1.InvalidArgumentError('Invalid branch name format', 'branch'); } if (trimmed.length > 250) { throw new specific_errors_1.InvalidArgumentError('Branch name is too long (max 250 characters)', 'branch'); } } /** * Validate numeric arguments */ static validateNumber(value, field, min, max) { const num = parseInt(value, 10); if (isNaN(num)) { throw new specific_errors_1.InvalidArgumentError(`${field} must be a valid number`, field); } if (min !== undefined && num < min) { throw new specific_errors_1.InvalidArgumentError(`${field} must be at least ${min}`, field); } if (max !== undefined && num > max) { throw new specific_errors_1.InvalidArgumentError(`${field} must be at most ${max}`, field); } return num; } /** * Validate command options using Zod schema */ static validateOptions(options, schema, command) { try { return schema.parse(options); } catch (error) { if (error instanceof zod_1.z.ZodError) { const issues = error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`); throw new specific_errors_1.InvalidInputError(`Invalid options for ${command}: ${issues.join(', ')}`); } throw error; } } /** * Sanitize string input for safe processing */ static sanitizeString(input, maxLength = 1000) { if (typeof input !== 'string') { throw new specific_errors_1.InvalidInputError('Input must be a string'); } // Remove control characters except newlines and tabs // eslint-disable-next-line no-control-regex let sanitized = input.replace(/[\x00-\x08\x0E-\x1F\x7F]/g, ''); // Trim and limit length sanitized = sanitized.trim(); if (sanitized.length > maxLength) { sanitized = sanitized.substring(0, maxLength); } return sanitized; } /** * Validate environment and prerequisites */ static validateEnvironment() { // Check Node.js version const nodeVersion = process.version; const major = parseInt(nodeVersion.slice(1).split('.')[0], 10); if (major < 18) { throw new specific_errors_1.InvalidInputError(`Node.js version ${nodeVersion} is not supported. Minimum required: 18.0.0`); } // Check platform support const supportedPlatforms = ['linux', 'darwin', 'win32']; if (!supportedPlatforms.includes(process.platform)) { throw new specific_errors_1.InvalidInputError(`Platform ${process.platform} is not supported`); } } /** * Validate working directory safety */ static validateWorkingDirectory(workingDir) { const normalized = path.resolve(workingDir); // Check for system directories const systemDirs = [ '/bin', '/sbin', '/usr/bin', '/usr/sbin', '/System', '/Windows', '/Program Files', process.env['SystemRoot'] || '', ].filter(dir => dir.length > 0); for (const systemDir of systemDirs) { if (normalized.startsWith(path.resolve(systemDir))) { throw new specific_errors_1.SecurityError(`Cannot operate in system directory: ${normalized}`); } } // Check for root directory if (normalized === '/' || /^[A-Z]:\\?$/.test(normalized)) { throw new specific_errors_1.SecurityError('Cannot operate in root directory'); } } /** * Create safe file path within working directory */ static createSafePath(workingDir, relativePath) { const validation = this.validatePath(relativePath, { allowAbsolutePaths: false, allowParentDirectory: false, }); if (!validation.isValid) { if (validation.securityRisk) { throw new specific_errors_1.UnsafePathError(relativePath, validation.issues.join(', ')); } else { throw new specific_errors_1.InvalidInputError(`Invalid path: ${validation.issues.join(', ')}`); } } const safePath = path.resolve(workingDir, validation.normalizedPath); // Ensure the resolved path is still within the working directory if (!safePath.startsWith(path.resolve(workingDir))) { throw new specific_errors_1.UnsafePathError(relativePath, 'Path escapes working directory'); } return safePath; } } exports.InputValidator = InputValidator; // Security patterns to block InputValidator.DANGEROUS_PATTERNS = [ /\.\.\//, // Parent directory traversal /\.\.\\/, // Windows parent directory traversal /^\//, // Absolute Unix paths (when not allowed) /^[a-zA-Z]:/, // Windows drive letters (when not allowed) /\0/, // Null byte injection /[<>:|"*?]/, // Windows forbidden characters /^\.+$/, // Only dots (., .., ...) /\s$/, // Trailing whitespace /^\s/, // Leading whitespace ]; // Blocked directory names InputValidator.BLOCKED_DIRECTORIES = [ '.git', '.private-storage', '.private-config.json', 'node_modules', 'System Volume Information', '$Recycle.Bin', 'System32', ]; // Maximum safe path length InputValidator.MAX_PATH_LENGTH = 255; /** * Zod schemas for common validation patterns */ exports.ValidationSchemas = { /** * Basic command options */ basicOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), }), /** * Log command options */ logOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), maxCount: zod_1.z.number().min(1).max(1000).optional(), oneline: zod_1.z.boolean().optional(), }), /** * Diff command options */ diffOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), cached: zod_1.z.boolean().optional(), nameOnly: zod_1.z.boolean().optional(), }), /** * Branch command options */ branchOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), create: zod_1.z.boolean().optional(), }), /** * Commit options */ commitOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), message: zod_1.z.string().min(3).max(500).optional(), }), /** * Cleanup options */ cleanupOptions: zod_1.z.object({ verbose: zod_1.z.boolean().optional(), force: zod_1.z.boolean().optional(), }), }; //# sourceMappingURL=input.validator.js.map