UNPKG

recoder-code

Version:

🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!

659 lines • 26.1 kB
"use strict"; /** * ValidationService * Handles package validation, integrity checks, and quality analysis */ 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidationService = void 0; const tar = __importStar(require("tar")); const crypto = __importStar(require("crypto")); const semver = __importStar(require("semver")); class ValidationService { constructor(config) { this.config = config; this.maxPackageSize = 50 * 1024 * 1024; // 50MB this.maxFileCount = 10000; this.maxFileSize = 10 * 1024 * 1024; // 10MB this.logger = { log: (message) => console.log(`[ValidationService] ${message}`), warn: (message, error) => console.warn(`[ValidationService] ${message}`, error), error: (message, error) => console.error(`[ValidationService] ${message}`, error) }; } async validatePackage(packageBuffer, expectedName, expectedVersion) { console.log(`Starting package validation (size: ${packageBuffer.length} bytes)`); const result = { valid: true, errors: [], warnings: [], quality_score: 0, size_analysis: { total_size: 0, unpacked_size: 0, file_count: 0, large_files: [], excluded_files: [], size_score: 0 }, dependency_analysis: { dependency_count: 0, dev_dependency_count: 0, peer_dependency_count: 0, outdated_dependencies: [], circular_dependencies: [], dependency_score: 0 }, metadata_analysis: { has_readme: false, has_license: false, has_changelog: false, has_tests: false, has_types: false, description_quality: 0, keyword_relevance: 0, metadata_score: 0 } }; try { // Step 1: Basic package structure validation const packageData = await this.extractAndParsePackage(packageBuffer); // Step 2: Package.json validation this.validatePackageJson(packageData.packageJson, result, expectedName, expectedVersion); // Step 3: Size analysis result.size_analysis = await this.analyzeSizes(packageData); // Step 4: Dependency analysis result.dependency_analysis = await this.analyzeDependencies(packageData.packageJson); // Step 5: Metadata analysis result.metadata_analysis = await this.analyzeMetadata(packageData); // Step 6: File structure validation await this.validateFileStructure(packageData, result); // Step 7: Security validation await this.validateSecurity(packageData, result); // Step 8: Calculate quality score result.quality_score = this.calculateQualityScore(result); // Determine if package is valid result.valid = result.errors.length === 0; this.logger.log(`Package validation completed: ${result.valid ? 'VALID' : 'INVALID'} (score: ${result.quality_score})`); return result; } catch (error) { if (error instanceof Error) { console.error(`Package validation failed: ${error.message}`, error.stack); result.errors.push({ code: 'VALIDATION_FAILED', message: `Validation failed: ${error.message}`, severity: 'critical' }); } else { console.error(`Package validation failed: ${String(error)}`); result.errors.push({ code: 'VALIDATION_FAILED', message: `Validation failed: ${String(error)}`, severity: 'critical' }); } result.valid = false; return result; } } async validateTarball(tarballBuffer) { // Simple tarball validation - just return valid for now return { valid: true, errors: [], warnings: [], quality_score: 100, size_analysis: { total_size: tarballBuffer.length, unpacked_size: 0, file_count: 0, large_files: [], excluded_files: [], size_score: 100 }, dependency_analysis: { dependency_count: 0, dev_dependency_count: 0, peer_dependency_count: 0, outdated_dependencies: [], circular_dependencies: [], dependency_score: 100 }, metadata_analysis: { has_readme: false, has_license: false, has_changelog: false, has_tests: false, has_types: false, description_quality: 0, keyword_relevance: 0, metadata_score: 100 } }; } async extractAndParsePackage(packageBuffer) { const files = new Map(); let packageJson = null; let totalSize = 0; // Calculate integrity const integrity = crypto.createHash('sha512').update(packageBuffer).digest('base64'); return new Promise((resolve, reject) => { const parser = new tar.Parse(); parser.on('entry', (entry) => { const chunks = []; entry.on('data', (chunk) => { chunks.push(chunk); totalSize += chunk.length; // Prevent zip bombs if (totalSize > this.maxPackageSize * 10) { reject(new Error('Package too large when extracted')); return; } }); entry.on('end', () => { const content = Buffer.concat(chunks); const relativePath = entry.path.replace(/^[^/]+\//, ''); // Remove top-level directory files.set(relativePath, content); // Parse package.json if (relativePath === 'package.json') { try { packageJson = JSON.parse(content.toString('utf8')); } catch (error) { if (error instanceof Error) { reject(new Error(`Invalid package.json: ${error.message}`)); } else { reject(new Error(`Invalid package.json: ${String(error)}`)); } return; } } }); }); parser.on('end', () => { if (!packageJson) { reject(new Error('Missing package.json')); return; } resolve({ packageJson, files, size: packageBuffer.length, integrity }); }); parser.on('error', reject); parser.write(packageBuffer); parser.end(); }); } validatePackageJson(packageJson, result, expectedName, expectedVersion) { // Required fields const requiredFields = ['name', 'version']; for (const field of requiredFields) { if (!packageJson[field]) { result.errors.push({ code: 'MISSING_REQUIRED_FIELD', message: `Missing required field: ${field}`, field, severity: 'error' }); } } // Name validation if (packageJson.name) { if (expectedName && packageJson.name !== expectedName) { result.errors.push({ code: 'NAME_MISMATCH', message: `Package name mismatch: expected ${expectedName}, got ${packageJson.name}`, field: 'name', severity: 'error' }); } if (!this.isValidPackageName(packageJson.name)) { result.errors.push({ code: 'INVALID_NAME', message: 'Invalid package name format', field: 'name', severity: 'error' }); } } // Version validation if (packageJson.version) { if (expectedVersion && packageJson.version !== expectedVersion) { result.errors.push({ code: 'VERSION_MISMATCH', message: `Version mismatch: expected ${expectedVersion}, got ${packageJson.version}`, field: 'version', severity: 'error' }); } if (!semver.valid(packageJson.version)) { result.errors.push({ code: 'INVALID_VERSION', message: 'Invalid semver version', field: 'version', severity: 'error' }); } } // Description validation if (!packageJson.description) { result.warnings.push({ code: 'MISSING_DESCRIPTION', message: 'Package description is missing', field: 'description', suggestion: 'Add a clear description of what your package does' }); } else if (packageJson.description.length < 10) { result.warnings.push({ code: 'SHORT_DESCRIPTION', message: 'Package description is too short', field: 'description', suggestion: 'Provide a more detailed description' }); } // License validation if (!packageJson.license) { result.warnings.push({ code: 'MISSING_LICENSE', message: 'Package license is missing', field: 'license', suggestion: 'Add a valid SPDX license identifier' }); } // Keywords validation if (!packageJson.keywords || packageJson.keywords.length === 0) { result.warnings.push({ code: 'MISSING_KEYWORDS', message: 'Package keywords are missing', field: 'keywords', suggestion: 'Add relevant keywords to improve discoverability' }); } // Repository validation if (!packageJson.repository) { result.warnings.push({ code: 'MISSING_REPOSITORY', message: 'Repository information is missing', field: 'repository', suggestion: 'Add repository URL for better transparency' }); } // Main/entry point validation if (packageJson.main && !packageJson.main.endsWith('.js')) { result.warnings.push({ code: 'UNUSUAL_MAIN_ENTRY', message: 'Main entry point is not a .js file', field: 'main', suggestion: 'Ensure main entry point is correct' }); } // Scripts validation if (!packageJson.scripts) { result.warnings.push({ code: 'MISSING_SCRIPTS', message: 'No npm scripts defined', field: 'scripts', suggestion: 'Add test and build scripts' }); } else { if (!packageJson.scripts.test) { result.warnings.push({ code: 'MISSING_TEST_SCRIPT', message: 'No test script defined', field: 'scripts.test', suggestion: 'Add a test script' }); } } // Dependencies validation this.validateDependencies(packageJson, result); } validateDependencies(packageJson, result) { const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies, ...packageJson.optionalDependencies }; for (const [depName, depVersion] of Object.entries(allDeps)) { if (!semver.validRange(depVersion)) { result.warnings.push({ code: 'INVALID_DEPENDENCY_VERSION', message: `Invalid version range for dependency ${depName}: ${depVersion}`, field: 'dependencies', suggestion: 'Use valid semver ranges' }); } // Check for potentially dangerous dependencies if (this.isDangerousDependency(depName)) { result.warnings.push({ code: 'DANGEROUS_DEPENDENCY', message: `Potentially dangerous dependency: ${depName}`, field: 'dependencies', suggestion: 'Review if this dependency is necessary' }); } } } async analyzeSizes(packageData) { const largeFiles = []; let unpackedSize = 0; const excludedFiles = []; for (const [filePath, content] of packageData.files) { unpackedSize += content.length; if (content.length > this.maxFileSize) { largeFiles.push({ path: filePath, size: content.length }); } // Check for unnecessary files if (this.isUnnecessaryFile(filePath)) { excludedFiles.push(filePath); } } const sizeScore = this.calculateSizeScore(packageData.size, unpackedSize, packageData.files.size); return { total_size: packageData.size, unpacked_size: unpackedSize, file_count: packageData.files.size, large_files: largeFiles, excluded_files: excludedFiles, size_score: sizeScore }; } async analyzeDependencies(packageJson) { const dependencyCount = Object.keys(packageJson.dependencies || {}).length; const devDependencyCount = Object.keys(packageJson.devDependencies || {}).length; const peerDependencyCount = Object.keys(packageJson.peerDependencies || {}).length; // In a real implementation, these would make API calls to check for updates const outdatedDependencies = []; const circularDependencies = []; const dependencyScore = this.calculateDependencyScore(dependencyCount, outdatedDependencies.length, circularDependencies.length); return { dependency_count: dependencyCount, dev_dependency_count: devDependencyCount, peer_dependency_count: peerDependencyCount, outdated_dependencies: outdatedDependencies, circular_dependencies: circularDependencies, dependency_score: dependencyScore }; } async analyzeMetadata(packageData) { const hasReadme = packageData.files.has('README.md') || packageData.files.has('readme.md') || packageData.files.has('README.txt'); const hasLicense = packageData.files.has('LICENSE') || packageData.files.has('LICENSE.md') || packageData.files.has('LICENSE.txt') || !!packageData.packageJson.license; const hasChangelog = packageData.files.has('CHANGELOG.md') || packageData.files.has('CHANGELOG.txt') || packageData.files.has('HISTORY.md'); const hasTests = Array.from(packageData.files.keys()).some(path => path.includes('test') || path.includes('spec') || path.includes('__tests__')); const hasTypes = packageData.files.has('index.d.ts') || !!packageData.packageJson.types || !!packageData.packageJson.typings; const descriptionQuality = this.calculateDescriptionQuality(packageData.packageJson.description); const keywordRelevance = this.calculateKeywordRelevance(packageData.packageJson.keywords); const metadataScore = this.calculateMetadataScore({ hasReadme, hasLicense, hasChangelog, hasTests, hasTypes, descriptionQuality, keywordRelevance }); return { has_readme: hasReadme, has_license: hasLicense, has_changelog: hasChangelog, has_tests: hasTests, has_types: hasTypes, description_quality: descriptionQuality, keyword_relevance: keywordRelevance, metadata_score: metadataScore }; } async validateFileStructure(packageData, result) { // Check for required files const mainFile = packageData.packageJson.main || 'index.js'; if (mainFile && !packageData.files.has(mainFile)) { result.errors.push({ code: 'MISSING_MAIN_FILE', message: `Main file not found: ${mainFile}`, field: 'main', severity: 'error' }); } // Check file count if (packageData.files.size > this.maxFileCount) { result.errors.push({ code: 'TOO_MANY_FILES', message: `Package contains too many files (${packageData.files.size} > ${this.maxFileCount})`, severity: 'error' }); } // Check for suspicious files for (const filePath of packageData.files.keys()) { if (this.isSuspiciousFile(filePath)) { result.warnings.push({ code: 'SUSPICIOUS_FILE', message: `Suspicious file detected: ${filePath}`, suggestion: 'Review if this file should be included' }); } } } async validateSecurity(packageData, result) { // Check for common security issues for (const [filePath, content] of packageData.files) { if (filePath.endsWith('.js') || filePath.endsWith('.ts')) { const contentStr = content.toString('utf8'); // Check for eval usage if (contentStr.includes('eval(')) { result.warnings.push({ code: 'UNSAFE_EVAL', message: `Unsafe eval() usage detected in ${filePath}`, suggestion: 'Avoid using eval() for security reasons' }); } // Check for subprocess execution if (contentStr.includes('child_process') || contentStr.includes('exec(')) { result.warnings.push({ code: 'SUBPROCESS_EXECUTION', message: `Subprocess execution detected in ${filePath}`, suggestion: 'Review subprocess usage for security implications' }); } } } } isValidPackageName(name) { // NPM package name rules const nameRegex = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; return nameRegex.test(name) && name.length <= 214 && !name.startsWith('.') && !name.startsWith('_') && !name.includes(' '); } isDangerousDependency(name) { const dangerousDeps = [ 'eval', 'vm2', 'node-serialize', 'serialize-javascript' ]; return dangerousDeps.includes(name); } isUnnecessaryFile(filePath) { const unnecessaryPatterns = [ /\.DS_Store$/, /Thumbs\.db$/, /\.git\//, /\.svn\//, /\.hg\//, /\.vscode\//, /\.idea\//, /node_modules\//, /coverage\//, /\.nyc_output\//, /\.cache\//, /\.tmp\//, /\.temp\// ]; return unnecessaryPatterns.some(pattern => pattern.test(filePath)); } isSuspiciousFile(filePath) { const suspiciousPatterns = [ /\.exe$/, /\.bat$/, /\.cmd$/, /\.sh$/, /\.ps1$/, /\.dll$/, /\.so$/, /\.dylib$/ ]; return suspiciousPatterns.some(pattern => pattern.test(filePath)); } calculateSizeScore(totalSize, unpackedSize, fileCount) { let score = 100; // Penalize large packages if (totalSize > 10 * 1024 * 1024) score -= 30; // 10MB else if (totalSize > 5 * 1024 * 1024) score -= 20; // 5MB else if (totalSize > 1 * 1024 * 1024) score -= 10; // 1MB // Penalize excessive file count if (fileCount > 1000) score -= 20; else if (fileCount > 500) score -= 10; // Penalize large unpacked size const compressionRatio = totalSize / unpackedSize; if (compressionRatio < 0.1) score -= 10; // Poor compression return Math.max(0, score); } calculateDependencyScore(depCount, outdatedCount, circularCount) { let score = 100; // Penalize too many dependencies if (depCount > 50) score -= 30; else if (depCount > 20) score -= 15; else if (depCount > 10) score -= 5; // Penalize outdated dependencies score -= outdatedCount * 5; // Penalize circular dependencies score -= circularCount * 10; return Math.max(0, score); } calculateDescriptionQuality(description) { if (!description) return 0; let score = 0; // Length scoring if (description.length > 20) score += 20; if (description.length > 50) score += 20; if (description.length > 100) score += 10; // Content quality if (/[.!?]$/.test(description)) score += 10; // Proper punctuation if (description.split(' ').length > 5) score += 20; // Adequate detail if (/\b(build|create|help|manage|parse|render|transform)\b/i.test(description)) score += 20; // Action words return Math.min(100, score); } calculateKeywordRelevance(keywords) { if (!keywords || keywords.length === 0) return 0; let score = 0; // Quantity scoring if (keywords.length >= 3) score += 30; if (keywords.length >= 5) score += 20; // Quality scoring const hasFrameworkKeywords = keywords.some(k => ['react', 'vue', 'angular', 'node', 'express', 'typescript'].includes(k.toLowerCase())); if (hasFrameworkKeywords) score += 25; const hasTypeKeywords = keywords.some(k => ['cli', 'api', 'library', 'framework', 'plugin', 'tool'].includes(k.toLowerCase())); if (hasTypeKeywords) score += 25; return Math.min(100, score); } calculateMetadataScore(metadata) { let score = 0; if (metadata.hasReadme) score += 20; if (metadata.hasLicense) score += 15; if (metadata.hasChangelog) score += 10; if (metadata.hasTests) score += 20; if (metadata.hasTypes) score += 15; score += metadata.descriptionQuality * 0.1; score += metadata.keywordRelevance * 0.1; return Math.min(100, score); } calculateQualityScore(result) { if (result.errors.length > 0) return 0; const weights = { size: 0.2, dependency: 0.3, metadata: 0.3, warnings: 0.2 }; const sizeScore = result.size_analysis?.size_score || 0; const dependencyScore = result.dependency_analysis?.dependency_score || 0; const metadataScore = result.metadata_analysis?.metadata_score || 0; const warningPenalty = Math.max(0, 100 - (result.warnings.length * 5)); const totalScore = sizeScore * weights.size + dependencyScore * weights.dependency + metadataScore * weights.metadata + warningPenalty * weights.warnings; return Math.round(totalScore); } } exports.ValidationService = ValidationService; //# sourceMappingURL=ValidationService.js.map