recoder-code
Version:
🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!
659 lines • 26.1 kB
JavaScript
;
/**
* 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