UNPKG

alepm

Version:

Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features

506 lines (418 loc) 14.9 kB
const fetch = require('node-fetch'); const semver = require('semver'); class Registry { constructor() { this.defaultRegistry = 'https://registry.npmjs.org'; this.registries = new Map(); this.cache = new Map(); this.config = this.loadConfig(); // Default registries this.registries.set('npm', 'https://registry.npmjs.org'); this.registries.set('yarn', 'https://registry.yarnpkg.com'); } loadConfig() { return { registry: this.defaultRegistry, timeout: 30000, retries: 3, userAgent: 'alepm/1.0.0 node/' + process.version, auth: {}, scopes: {}, cache: true, offline: false }; } async getPackageInfo(packageName, version = 'latest') { if (this.config.offline) { throw new Error('Cannot fetch package info in offline mode'); } const cacheKey = `info:${packageName}@${version}`; if (this.config.cache && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (Date.now() - cached.timestamp < 300000) { // 5 minutes return cached.data; } } const registry = this.getRegistryForPackage(packageName); const url = `${registry}/${encodeURIComponent(packageName)}`; const response = await this.fetchWithRetry(url); if (!response.ok) { if (response.status === 404) { throw new Error(`Package "${packageName}" not found`); } throw new Error(`Failed to fetch package info: ${response.status} ${response.statusText}`); } const data = await response.json(); // Validate the response structure if (!data || typeof data !== 'object') { throw new Error(`Invalid response format for package "${packageName}"`); } // Cache the result if (this.config.cache) { this.cache.set(cacheKey, { data, timestamp: Date.now() }); } return data; } async getLatestVersion(packageName) { const info = await this.getPackageInfo(packageName); if (!info || !info['dist-tags'] || !info['dist-tags'].latest) { throw new Error(`Unable to find latest version for package "${packageName}"`); } return info['dist-tags'].latest; } async getVersions(packageName) { const info = await this.getPackageInfo(packageName); if (!info || !info.versions) { throw new Error(`Unable to find versions for package "${packageName}"`); } return Object.keys(info.versions).sort(semver.rcompare); } async resolveVersion(packageName, versionSpec) { if (versionSpec === 'latest') { return await this.getLatestVersion(packageName); } const versions = await this.getVersions(packageName); // Handle exact version if (versions.includes(versionSpec)) { return versionSpec; } // Handle semver range const resolved = semver.maxSatisfying(versions, versionSpec); if (!resolved) { throw new Error(`No version of "${packageName}" satisfies "${versionSpec}"`); } return resolved; } async download(pkg) { if (this.config.offline) { throw new Error('Cannot download packages in offline mode'); } const packageInfo = await this.getPackageInfo(pkg.name, pkg.version); if (!packageInfo.versions || !packageInfo.versions[pkg.version]) { throw new Error(`Version ${pkg.version} of package ${pkg.name} not found`); } const versionInfo = packageInfo.versions[pkg.version]; const tarballUrl = versionInfo.dist.tarball; if (!tarballUrl) { throw new Error(`No tarball URL found for ${pkg.name}@${pkg.version}`); } const response = await this.fetchWithRetry(tarballUrl); if (!response.ok) { throw new Error(`Failed to download ${pkg.name}@${pkg.version}: ${response.status}`); } const buffer = await response.buffer(); // Verify integrity if available if (versionInfo.dist.integrity) { await this.verifyIntegrity(buffer, versionInfo.dist.integrity); } return { data: buffer, integrity: versionInfo.dist.integrity, shasum: versionInfo.dist.shasum, size: buffer.length, tarball: tarballUrl, resolved: tarballUrl, packageInfo: versionInfo }; } async search(query, options = {}) { if (this.config.offline) { throw new Error('Cannot search packages in offline mode'); } const registry = this.config.registry; const limit = options.limit || 20; const offset = options.offset || 0; const searchUrl = `${registry}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}&from=${offset}`; const response = await this.fetchWithRetry(searchUrl); if (!response.ok) { throw new Error(`Search failed: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.objects.map(obj => ({ name: obj.package.name, version: obj.package.version, description: obj.package.description, keywords: obj.package.keywords, author: obj.package.author, publisher: obj.package.publisher, maintainers: obj.package.maintainers, repository: obj.package.links?.repository, homepage: obj.package.links?.homepage, npm: obj.package.links?.npm, downloadScore: obj.score?.detail?.downloads || 0, popularityScore: obj.score?.detail?.popularity || 0, qualityScore: obj.score?.detail?.quality || 0, maintenanceScore: obj.score?.detail?.maintenance || 0, finalScore: obj.score?.final || 0 })); } async getMetadata(packageName, version) { const info = await this.getPackageInfo(packageName, version); if (version === 'latest') { version = info['dist-tags'].latest; } const versionInfo = info.versions[version]; if (!versionInfo) { throw new Error(`Version ${version} not found for ${packageName}`); } return { name: versionInfo.name, version: versionInfo.version, description: versionInfo.description, keywords: versionInfo.keywords || [], homepage: versionInfo.homepage, repository: versionInfo.repository, bugs: versionInfo.bugs, license: versionInfo.license, author: versionInfo.author, contributors: versionInfo.contributors || [], maintainers: versionInfo.maintainers || [], dependencies: versionInfo.dependencies || {}, devDependencies: versionInfo.devDependencies || {}, peerDependencies: versionInfo.peerDependencies || {}, optionalDependencies: versionInfo.optionalDependencies || {}, bundledDependencies: versionInfo.bundledDependencies || [], engines: versionInfo.engines || {}, os: versionInfo.os || [], cpu: versionInfo.cpu || [], scripts: versionInfo.scripts || {}, bin: versionInfo.bin || {}, man: versionInfo.man || [], directories: versionInfo.directories || {}, files: versionInfo.files || [], main: versionInfo.main, browser: versionInfo.browser, module: versionInfo.module, types: versionInfo.types, typings: versionInfo.typings, exports: versionInfo.exports, imports: versionInfo.imports, funding: versionInfo.funding, dist: { tarball: versionInfo.dist.tarball, shasum: versionInfo.dist.shasum, integrity: versionInfo.dist.integrity, fileCount: versionInfo.dist.fileCount, unpackedSize: versionInfo.dist.unpackedSize }, time: { created: info.time.created, modified: info.time.modified, version: info.time[version] }, readme: versionInfo.readme, readmeFilename: versionInfo.readmeFilename, deprecated: versionInfo.deprecated }; } async getDependencies(packageName, version) { const metadata = await this.getMetadata(packageName, version); return { dependencies: metadata.dependencies, devDependencies: metadata.devDependencies, peerDependencies: metadata.peerDependencies, optionalDependencies: metadata.optionalDependencies, bundledDependencies: metadata.bundledDependencies }; } async getDownloadStats(packageName, period = 'last-week') { const registry = 'https://api.npmjs.org'; const url = `${registry}/downloads/point/${period}/${encodeURIComponent(packageName)}`; try { const response = await this.fetchWithRetry(url); if (!response.ok) { return { downloads: 0, period }; } const data = await response.json(); return data; } catch (error) { return { downloads: 0, period }; } } async getUserPackages(username) { const registry = this.config.registry; const url = `${registry}/-/user/${encodeURIComponent(username)}/package`; const response = await this.fetchWithRetry(url); if (!response.ok) { throw new Error(`Failed to get user packages: ${response.status}`); } const data = await response.json(); return Object.keys(data); } async addRegistry(name, url, options = {}) { this.registries.set(name, url); if (options.auth) { this.config.auth[url] = options.auth; } if (options.scope) { this.config.scopes[options.scope] = url; } } async removeRegistry(name) { const url = this.registries.get(name); if (url) { this.registries.delete(name); delete this.config.auth[url]; // Remove scope mappings for (const [scope, registryUrl] of Object.entries(this.config.scopes)) { if (registryUrl === url) { delete this.config.scopes[scope]; } } } } getRegistryForPackage(packageName) { // Check for scoped packages if (packageName.startsWith('@')) { const scope = packageName.split('/')[0]; if (this.config.scopes[scope]) { return this.config.scopes[scope]; } } return this.config.registry; } async fetchWithRetry(url, options = {}) { const requestOptions = { timeout: this.config.timeout, headers: { 'User-Agent': this.config.userAgent, 'Accept': 'application/json', ...options.headers }, ...options }; // Add authentication if available const registry = new URL(url).origin; if (this.config.auth[registry]) { const auth = this.config.auth[registry]; if (auth.token) { requestOptions.headers['Authorization'] = `Bearer ${auth.token}`; } else if (auth.username && auth.password) { const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64'); requestOptions.headers['Authorization'] = `Basic ${credentials}`; } } let lastError; for (let attempt = 0; attempt < this.config.retries; attempt++) { try { const response = await fetch(url, requestOptions); return response; } catch (error) { lastError = error; if (attempt < this.config.retries - 1) { // Exponential backoff const delay = Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } async verifyIntegrity(data, integrity) { const crypto = require('crypto'); // Parse integrity string (algorithm-hash) const match = integrity.match(/^(sha\d+)-(.+)$/); if (!match) { throw new Error(`Invalid integrity format: ${integrity}`); } const [, algorithm, expectedHash] = match; const actualHash = crypto.createHash(algorithm.replace('sha', 'sha')).update(data).digest('base64'); if (actualHash !== expectedHash) { throw new Error('Package integrity verification failed'); } } async publishPackage(_packagePath, _options = {}) { // This would implement package publishing throw new Error('Package publishing not yet implemented'); } async unpublishPackage(_packageName, _version, _options = {}) { // This would implement package unpublishing throw new Error('Package unpublishing not yet implemented'); } async deprecatePackage(_packageName, _version, _message, _options = {}) { // This would implement package deprecation throw new Error('Package deprecation not yet implemented'); } async login(_username, _password, _email, _registry) { // This would implement user authentication throw new Error('Login not yet implemented'); } async logout(_registry) { // This would implement logout const registryUrl = _registry || this.config.registry; delete this.config.auth[registryUrl]; } async whoami(_registry) { // This would return current user info throw new Error('whoami not yet implemented'); } // Utility methods async ping(registry) { const registryUrl = registry || this.config.registry; try { const response = await this.fetchWithRetry(`${registryUrl}/-/ping`); return { registry: registryUrl, ok: response.ok, status: response.status, time: Date.now() }; } catch (error) { return { registry: registryUrl, ok: false, error: error.message, time: Date.now() }; } } async getRegistryInfo(registry) { const registryUrl = registry || this.config.registry; try { const response = await this.fetchWithRetry(registryUrl); if (!response.ok) { throw new Error(`Registry not accessible: ${response.status}`); } const data = await response.json(); return { registry: registryUrl, db_name: data.db_name, doc_count: data.doc_count, doc_del_count: data.doc_del_count, update_seq: data.update_seq, purge_seq: data.purge_seq, compact_running: data.compact_running, disk_size: data.disk_size, data_size: data.data_size, instance_start_time: data.instance_start_time, disk_format_version: data.disk_format_version, committed_update_seq: data.committed_update_seq }; } catch (error) { throw new Error(`Failed to get registry info: ${error.message}`); } } clearCache() { this.cache.clear(); } getCacheStats() { const entries = Array.from(this.cache.values()); const totalSize = JSON.stringify(entries).length; return { entries: this.cache.size, totalSize, oldestEntry: entries.length > 0 ? Math.min(...entries.map(e => e.timestamp)) : null, newestEntry: entries.length > 0 ? Math.max(...entries.map(e => e.timestamp)) : null }; } setOfflineMode(offline = true) { this.config.offline = offline; } isOffline() { return this.config.offline; } } module.exports = Registry;