UNPKG

recoder-code

Version:

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

682 lines (563 loc) • 22.5 kB
/** * Package Routes * NPM-compatible package registry endpoints */ import { Router, Request, Response } from 'express'; import { Container } from '../container'; import { PackageService } from '../services/PackageService'; import { AuthService } from '../services/AuthService'; import { ValidationService } from '../services/ValidationService'; import { SecurityService } from '../services/SecurityService'; import { StorageService } from '../services/StorageService'; import { AnalyticsService } from '../services/AnalyticsService'; import { RateLimitService } from '../services/RateLimitService'; import { Package, PackageVisibility } from '../entities/Package'; import { PackageVersion } from '../entities/PackageVersion'; import { ApiKey, ApiKeyScope } from '../entities/ApiKey'; import { User } from '../entities/User'; import multer from 'multer'; import * as semver from 'semver'; const router = Router(); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } }); // 50MB // Health check endpoint router.get('/-/health', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); // Basic health checks const checks = { database: false, storage: false, cache: false, timestamp: new Date().toISOString() }; try { // Test database connection await packageService.getPackageCount(); checks.database = true; } catch (e) { console.warn('Database health check failed:', e); } try { // Test storage connection const storageService = Container.get(StorageService); await storageService.testConnection(); checks.storage = true; } catch (e) { console.warn('Storage health check failed:', e); } const healthy = checks.database && checks.storage; res.status(healthy ? 200 : 503).json({ status: healthy ? 'healthy' : 'unhealthy', checks, version: process.env.npm_package_version || '1.0.0' }); } catch (error) { res.status(503).json({ status: 'error', error: 'Health check failed', timestamp: new Date().toISOString() }); } }); // Helper to get typed user from request const getUser = (req: Request): User | undefined => getUser(req)! as User; // Middleware const authenticate = async (req: Request, res: Response, next: Function) => { try { const authService = Container.get(AuthService); const rateLimitService = Container.get(RateLimitService); const token = req.headers.authorization?.replace('Bearer ', '') || req.headers.authorization?.replace('token ', '') || req.query.token as string; if (!token) { return res.status(401).json({ error: 'Authentication required' }); } const result = await authService.validateToken(token); if (!result.valid) { return res.status(401).json({ error: 'Invalid authentication token' }); } // Check rate limits const rateLimitResult = await rateLimitService.getUserRateLimit(result.user!.id); if (!rateLimitResult.allowed) { return res.status(429).json({ error: 'Rate limit exceeded' }); } req.user = result.user; req.apiKey = result.apiKey; next(); } catch (error) { res.status(500).json({ error: 'Authentication failed' }); } }; const requireScope = (scope: ApiKeyScope) => { return (req: Request, res: Response, next: Function) => { if (!req.apiKey?.hasScope(scope)) { return res.status(403).json({ error: `Insufficient permissions: ${scope} required` }); } next(); }; }; // NPM Registry Compatibility Routes // Search packages (CLI compatibility endpoint) - MUST be before /:name route router.get('/search', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const { q = '' } = req.query; const searchOptions = { query: q as string, limit: 20, offset: 0, sortBy: 'relevance' as const, sortOrder: 'desc' as const }; const results = await packageService.searchPackages(searchOptions); // CLI expects results array const response = { results: results.packages.map(pkg => ({ name: pkg.name, version: pkg.latest_version, description: pkg.description, rating: pkg.quality_metrics?.code_quality || 4.5, downloads: pkg.quality_metrics?.popularity || 1000 })) }; res.json(response); } catch (error) { console.error('Plugin search error:', error); res.status(500).json({ error: 'Search failed', results: [] }); } }); // Get package metadata (NPM format) router.get('/:name', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const analyticsService = Container.get(AnalyticsService); const packageName = decodeURIComponent(req.params.name); const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check visibility if (pkg.visibility === PackageVisibility.PRIVATE && (!getUser(req)! || !getUser(req)!.canAccessPackage(pkg))) { return res.status(404).json({ error: 'Package not found' }); } // Track package view await analyticsService.trackPackageView(pkg.id, getUser(req)?.id); // Return NPM-compatible format const npmData = pkg.toNpmFormat(); // Add versions const versions = await packageService.getPackageVersions(pkg.id); npmData.versions = {}; npmData.time = { created: pkg.created_at.toISOString(), modified: pkg.updated_at.toISOString() }; for (const version of versions) { npmData.versions[version.version] = version.toNpmFormat(); npmData.time[version.version] = version.published_at?.toISOString() || version.created_at.toISOString(); } res.json(npmData); } catch (error) { console.error('Error getting package:', error); res.status(500).json({ error: 'Failed to get package', code: 'PACKAGE_FETCH_ERROR', details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined }); } }); // Get specific package version router.get('/:name/:version', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const analyticsService = Container.get(AnalyticsService); const packageName = decodeURIComponent(req.params.name); const version = req.params.version; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check visibility if (pkg.visibility === PackageVisibility.PRIVATE && (!getUser(req)! || !getUser(req)!.canAccessPackage(pkg))) { return res.status(404).json({ error: 'Package not found' }); } const packageVersion = await packageService.getPackageVersion(pkg.id, version); if (!packageVersion) { return res.status(404).json({ error: 'Version not found' }); } // Track version view await analyticsService.trackVersionView(packageVersion.id, getUser(req)!?.id, req.ip); res.json(packageVersion.toNpmFormat()); } catch (error) { console.error('Error getting package version:', error); res.status(500).json({ error: 'Failed to get package version', code: 'VERSION_FETCH_ERROR', details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined }); } }); // Download package tarball router.get('/:name/-/:filename', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const storageService = Container.get(StorageService); const analyticsService = Container.get(AnalyticsService); const packageName = decodeURIComponent(req.params.name); const filename = req.params.filename; // Extract version from filename (format: package-version.tgz) const versionMatch = filename.match(/^.*?-(\d+\.\d+\.\d+.*?)\.tgz$/); if (!versionMatch) { return res.status(400).json({ error: 'Invalid filename format' }); } const version = versionMatch[1]; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check visibility if (pkg.visibility === PackageVisibility.PRIVATE && (!getUser(req)! || !getUser(req)!.canAccessPackage(pkg))) { return res.status(404).json({ error: 'Package not found' }); } const packageVersion = await packageService.getPackageVersion(pkg.id, version); if (!packageVersion) { return res.status(404).json({ error: 'Version not found' }); } // Download from storage const downloadResult = await storageService.downloadPackage(packageName, version); if (!downloadResult.success) { return res.status(404).json({ error: 'Package file not found' }); } // Track download await analyticsService.trackDownload( pkg.id, packageVersion.version, getUser(req)?.id ); // Update download counts await packageService.incrementDownloadCount(pkg.id, packageVersion.id); // Set headers if (!downloadResult.data) { return res.status(404).json({ error: 'Package file not found' }); } res.set({ 'Content-Type': 'application/octet-stream', 'Content-Length': downloadResult.data.length.toString(), 'Content-Disposition': `attachment; filename="${filename}"`, 'Cache-Control': 'public, max-age=31536000', // 1 year 'ETag': downloadResult.metadata?.etag }); res.send(downloadResult.data); } catch (error) { console.error('Error downloading package:', error); res.status(500).json({ error: 'Failed to download package', code: 'DOWNLOAD_ERROR', details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined }); } }); // Publish package router.put('/:name', authenticate, requireScope(ApiKeyScope.PUBLISH), upload.single('package'), async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const validationService = Container.get(ValidationService); const securityService = Container.get(SecurityService); const storageService = Container.get(StorageService); const packageName = decodeURIComponent(req.params.name); // Handle both file upload and JSON body let packageBuffer: Buffer; let packageJson: any; if (req.file) { // Multipart upload packageBuffer = req.file.buffer; } else if (req.body._attachments) { // NPM publish format const attachments = req.body._attachments; const attachmentKey = Object.keys(attachments)[0]; if (!attachmentKey) { return res.status(400).json({ error: 'No package attachment found' }); } const attachment = attachments[attachmentKey]; packageBuffer = Buffer.from(attachment.data, 'base64'); packageJson = req.body.versions?.[Object.keys(req.body.versions)[0]]; } else { return res.status(400).json({ error: 'No package data provided' }); } if (!packageBuffer) { return res.status(400).json({ error: 'Empty package data' }); } // Validate package const validationResult = await validationService.validatePackage(packageBuffer, packageName); if (!validationResult.valid) { return res.status(400).json({ error: 'Package validation failed', errors: validationResult.errors, warnings: validationResult.warnings }); } const version = packageJson?.version || (validationResult.size_analysis && (validationResult.size_analysis as any)['package.json']?.version); if (!version) { return res.status(400).json({ error: 'Package version not found' }); } // Check if package/version already exists const existingPackage = await packageService.getPackage(packageName); if (existingPackage) { const existingVersion = await packageService.getPackageVersion(existingPackage.id, version); if (existingVersion) { return res.status(409).json({ error: 'Version already exists' }); } // Check permissions if (!getUser(req)!.canModifyPackage(existingPackage)) { return res.status(403).json({ error: 'Insufficient permissions' }); } } // Security scan const fakePackageVersion = { version, package: existingPackage || { name: packageName }, } as any; // Cast as any to satisfy the type, since only name/version are used in logging const securityResult = await securityService.scanPackage(packageBuffer, fakePackageVersion); if (securityResult.status === 'critical') { return res.status(400).json({ error: 'Security scan failed', details: securityResult.threats }); } // Upload to storage const uploadResult = await storageService.uploadPackage( packageName, version, packageBuffer, 'application/octet-stream' ); if (!uploadResult.success) { return res.status(500).json({ error: 'Failed to store package' }); } // Create/update package and version const result = await packageService.publishPackage( packageJson || { name: packageName, version }, getUser(req)!, req.file?.buffer ); if (!result.success) { return res.status(500).json({ error: result.errors?.[0] || 'Package publishing failed' }); } if (!result.package || !result.version) { return res.status(500).json({ error: 'Package publishing failed: missing package or version data' }); } res.json({ success: true, package: result.package.toApiFormat(), version: result.version.toApiFormat() }); } catch (error) { console.error('Error publishing package:', error); res.status(500).json({ error: 'Failed to publish package', code: 'PUBLISH_ERROR', details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined }); } }); // Unpublish package version router.delete('/:name/-/:version', authenticate, requireScope(ApiKeyScope.UNPUBLISH), async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const storageService = Container.get(StorageService); const packageName = decodeURIComponent(req.params.name); const version = req.params.version; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check permissions if (!getUser(req)!.canModifyPackage(pkg)) { return res.status(403).json({ error: 'Insufficient permissions' }); } const packageVersion = await packageService.getPackageVersion(pkg.id, version); if (!packageVersion) { return res.status(404).json({ error: 'Version not found' }); } // Delete from storage await storageService.deletePackage(packageName, version); // Remove version from database const result = await packageService.unpublishVersion(packageName, version, getUser(req)!); if (!result.success) { return res.status(500).json({ error: result.error }); } res.json({ success: true, message: 'Version unpublished successfully' }); } catch (error) { res.status(500).json({ error: 'Failed to unpublish version' }); } }); // Deprecate package version router.post('/:name/:version/deprecate', authenticate, requireScope(ApiKeyScope.DEPRECATE), async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const packageName = decodeURIComponent(req.params.name); const version = req.params.version; const { message } = req.body; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check permissions if (!getUser(req)!.canModifyPackage(pkg)) { return res.status(403).json({ error: 'Insufficient permissions' }); } const result = await packageService.deprecateVersion(pkg.id, version, message); if (!result.success) { return res.status(500).json({ error: result.error }); } res.json({ success: true, message: 'Version deprecated successfully' }); } catch (error) { res.status(500).json({ error: 'Failed to deprecate version' }); } }); // Search packages (NPM compatibility) router.get('/-/search', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const { text = '', size = 20, from = 0, quality = 0.5, popularity = 0.5, maintenance = 0.5 } = req.query; const searchOptions = { query: text as string, limit: Math.min(parseInt(size as string) || 20, 100), offset: parseInt(from as string) || 0, sortBy: 'relevance' as const, sortOrder: 'desc' as const }; const results = await packageService.searchPackages(searchOptions); // NPM search format const response = { objects: results.packages.map(pkg => ({ package: { name: pkg.name, scope: pkg.scope, version: pkg.latest_version, description: pkg.description, keywords: pkg.keywords || [], date: pkg.last_published?.toISOString(), links: { npm: `https://npmjs.com/package/${pkg.name}`, homepage: pkg.homepage, repository: pkg.repository?.url, bugs: pkg.bugs?.url }, author: pkg.author, publisher: pkg.maintainers?.[0], maintainers: pkg.maintainers || [] }, score: { final: pkg.quality_metrics?.code_quality || 0, detail: { quality: pkg.quality_metrics?.code_quality || 0, popularity: pkg.quality_metrics?.popularity || 0, maintenance: pkg.quality_metrics?.maintenance || 0 } }, searchScore: 1.0 })), total: results.total, time: new Date().toISOString() }; res.json(response); } catch (error) { res.status(500).json({ error: 'Search failed' }); } }); // Get package statistics router.get('/:name/-/stats', async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const analyticsService = Container.get(AnalyticsService); const packageName = decodeURIComponent(req.params.name); const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check visibility if (pkg.visibility === PackageVisibility.PRIVATE && (!getUser(req)! || !getUser(req)!.canAccessPackage(pkg))) { return res.status(404).json({ error: 'Package not found' }); } const stats = await analyticsService.getPackageStats(pkg.id); res.json({ package: pkg.name, downloads: stats.downloads, versions: stats.versions, dependents: stats.dependents, last_updated: pkg.updated_at }); } catch (error) { res.status(500).json({ error: 'Failed to get package stats' }); } }); // Add package collaborator router.put('/:name/-/collaborators/:username', authenticate, async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const packageName = decodeURIComponent(req.params.name); const username = req.params.username; const { permissions = ['read'] } = req.body; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check permissions (only owner can add collaborators) if (pkg.owner_id !== getUser(req)!.id && !getUser(req)!.is_admin) { return res.status(403).json({ error: 'Only package owner can add collaborators' }); } const result = await packageService.addCollaborator(pkg.id, username, permissions); if (!result.success) { return res.status(400).json({ error: result.error }); } res.json({ success: true, message: 'Collaborator added successfully' }); } catch (error) { res.status(500).json({ error: 'Failed to add collaborator' }); } }); // Remove package collaborator router.delete('/:name/-/collaborators/:username', authenticate, async (req: Request, res: Response) => { try { const packageService = Container.get(PackageService); const packageName = decodeURIComponent(req.params.name); const username = req.params.username; const pkg = await packageService.getPackage(packageName); if (!pkg) { return res.status(404).json({ error: 'Package not found' }); } // Check permissions if (pkg.owner_id !== getUser(req)!.id && !getUser(req)!.is_admin) { return res.status(403).json({ error: 'Only package owner can remove collaborators' }); } const result = await packageService.removeCollaborator(pkg.name, username, getUser(req)!); if (!result.success) { return res.status(400).json({ error: result.error }); } res.json({ success: true, message: 'Collaborator removed successfully' }); } catch (error) { res.status(500).json({ error: 'Failed to remove collaborator' }); } }); // Get package download counts router.get('/:name/-/downloads', async (req: Request, res: Response) => { try { const analyticsService = Container.get(AnalyticsService); const packageName = decodeURIComponent(req.params.name); const { period = 'week', version } = req.query; const downloads = await analyticsService.getDownloadStats( packageName, period as string ); res.json(downloads); } catch (error) { res.status(500).json({ error: 'Failed to get download stats' }); } }); export default router;