UNPKG

alepm

Version:

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

530 lines (440 loc) 13.8 kB
const path = require('path'); const fs = require('fs-extra'); const os = require('os'); class ConfigManager { constructor() { this.configDir = path.join(os.homedir(), '.alepm'); this.configFile = path.join(this.configDir, 'config.json'); this.globalConfigFile = '/etc/alepm/config.json'; this.defaultConfig = this.getDefaultConfig(); this.config = null; } getDefaultConfig() { return { // Registry settings registry: 'https://registry.npmjs.org', registries: { npm: 'https://registry.npmjs.org', yarn: 'https://registry.yarnpkg.com' }, scopes: {}, // Cache settings cache: { enabled: true, maxSize: '1GB', maxAge: '30d', cleanupInterval: '7d', compression: true, verifyIntegrity: true }, // Security settings security: { enableAudit: true, enableIntegrityCheck: true, enableSignatureVerification: false, allowedHashAlgorithms: ['sha512', 'sha256'], requireSignedPackages: false, blockedPackages: [], trustedPublishers: [], maxPackageSize: '100MB', scanPackageContent: true }, // Storage settings storage: { compression: 9, binaryFormat: true, deduplication: true, compactInterval: '30d' }, // Network settings network: { timeout: 30000, retries: 3, userAgent: 'alepm/1.0.0', proxy: null, httpsProxy: null, noProxy: 'localhost,127.0.0.1', strictSSL: true, cafile: null, cert: null, key: null }, // Installation settings install: { saveExact: false, savePrefix: '^', production: false, optional: true, dev: false, globalFolder: path.join(os.homedir(), '.alepm', 'global'), binLinks: true, rebuildBundle: true, ignoreScripts: false, packageLock: true, packageLockOnly: false, shrinkwrap: true, dryRun: false, force: false }, // Output settings output: { loglevel: 'info', silent: false, json: false, parseable: false, progress: true, color: 'auto', unicode: true, timing: false }, // Performance settings performance: { maxConcurrency: 10, maxSockets: 50, fetchRetryFactor: 10, fetchRetryMintimeout: 10000, fetchRetryMaxtimeout: 60000, fetchTimeout: 300000 }, // Lock file settings lockfile: { enabled: true, filename: 'alepm.lock', autoUpdate: true, verifyIntegrity: true, includeMetadata: true }, // Script settings scripts: { shellPositional: false, shell: process.platform === 'win32' ? 'cmd' : 'sh', ifPresent: false, ignoreScripts: false, scriptShell: null } }; } async init() { await fs.ensureDir(this.configDir); if (!fs.existsSync(this.configFile)) { await this.saveConfig(this.defaultConfig); } await this.loadConfig(); } async loadConfig() { let userConfig = {}; let globalConfig = {}; // Load global config if exists if (fs.existsSync(this.globalConfigFile)) { try { globalConfig = await fs.readJson(this.globalConfigFile); } catch (error) { console.warn(`Warning: Could not load global config: ${error.message}`); } } // Load user config if exists if (fs.existsSync(this.configFile)) { try { userConfig = await fs.readJson(this.configFile); } catch (error) { console.warn(`Warning: Could not load user config: ${error.message}`); userConfig = {}; } } // Merge configs: default < global < user this.config = this.deepMerge( this.defaultConfig, globalConfig, userConfig ); return this.config; } async saveConfig(config = null) { const configToSave = config || this.config || this.defaultConfig; await fs.writeJson(this.configFile, configToSave, { spaces: 2 }); this.config = configToSave; } async get(key, defaultValue = undefined) { if (!this.config) { await this.loadConfig(); } return this.getNestedValue(this.config, key) ?? defaultValue; } async set(key, value) { if (!this.config) { await this.loadConfig(); } this.setNestedValue(this.config, key, value); await this.saveConfig(); } async unset(key) { if (!this.config) { await this.loadConfig(); } this.unsetNestedValue(this.config, key); await this.saveConfig(); } async list() { if (!this.config) { await this.loadConfig(); } return this.config; } async reset() { this.config = { ...this.defaultConfig }; await this.saveConfig(); } async resetKey(key) { const defaultValue = this.getNestedValue(this.defaultConfig, key); await this.set(key, defaultValue); } getNestedValue(obj, path) { const keys = path.split('.'); let current = obj; for (const key of keys) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = current[key]; } return current; } setNestedValue(obj, path, value) { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current) || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = value; } unsetNestedValue(obj, path) { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current) || typeof current[key] !== 'object') { return; // Path doesn't exist } current = current[key]; } delete current[keys[keys.length - 1]]; } deepMerge(...objects) { const result = {}; for (const obj of objects) { for (const [key, value] of Object.entries(obj || {})) { if (value !== null && typeof value === 'object' && !Array.isArray(value)) { result[key] = this.deepMerge(result[key] || {}, value); } else { result[key] = value; } } } return result; } // Utility methods for common config operations async addRegistry(name, url, options = {}) { const registries = await this.get('registries', {}); registries[name] = url; await this.set('registries', registries); if (options.scope) { const scopes = await this.get('scopes', {}); scopes[options.scope] = url; await this.set('scopes', scopes); } } async removeRegistry(name) { const registries = await this.get('registries', {}); const url = registries[name]; if (url) { delete registries[name]; await this.set('registries', registries); // Remove associated scopes const scopes = await this.get('scopes', {}); for (const [scope, scopeUrl] of Object.entries(scopes)) { if (scopeUrl === url) { delete scopes[scope]; } } await this.set('scopes', scopes); } } async setScope(scope, registry) { const scopes = await this.get('scopes', {}); scopes[scope] = registry; await this.set('scopes', scopes); } async removeScope(scope) { const scopes = await this.get('scopes', {}); delete scopes[scope]; await this.set('scopes', scopes); } async addTrustedPublisher(publisherId, publicKey) { const trusted = await this.get('security.trustedPublishers', []); trusted.push({ id: publisherId, publicKey, addedAt: Date.now() }); await this.set('security.trustedPublishers', trusted); } async removeTrustedPublisher(publisherId) { const trusted = await this.get('security.trustedPublishers', []); const filtered = trusted.filter(p => p.id !== publisherId); await this.set('security.trustedPublishers', filtered); } async blockPackage(packageName, reason) { const blocked = await this.get('security.blockedPackages', []); blocked.push({ name: packageName, reason, blockedAt: Date.now() }); await this.set('security.blockedPackages', blocked); } async unblockPackage(packageName) { const blocked = await this.get('security.blockedPackages', []); const filtered = blocked.filter(p => p.name !== packageName); await this.set('security.blockedPackages', filtered); } async setProxy(proxy, httpsProxy = null) { await this.set('network.proxy', proxy); if (httpsProxy) { await this.set('network.httpsProxy', httpsProxy); } } async removeProxy() { await this.set('network.proxy', null); await this.set('network.httpsProxy', null); } // Configuration validation validateConfig(config = null) { const configToValidate = config || this.config; const errors = []; const warnings = []; // Validate registry URLs if (configToValidate.registry) { if (!this.isValidUrl(configToValidate.registry)) { errors.push(`Invalid registry URL: ${configToValidate.registry}`); } } // Validate cache settings if (configToValidate.cache) { if (configToValidate.cache.maxSize) { if (!this.isValidSize(configToValidate.cache.maxSize)) { errors.push(`Invalid cache maxSize: ${configToValidate.cache.maxSize}`); } } if (configToValidate.cache.maxAge) { if (!this.isValidDuration(configToValidate.cache.maxAge)) { errors.push(`Invalid cache maxAge: ${configToValidate.cache.maxAge}`); } } } // Validate security settings if (configToValidate.security) { if (configToValidate.security.maxPackageSize) { if (!this.isValidSize(configToValidate.security.maxPackageSize)) { errors.push(`Invalid maxPackageSize: ${configToValidate.security.maxPackageSize}`); } } } // Validate network settings if (configToValidate.network) { if (configToValidate.network.timeout < 0) { errors.push('Network timeout must be positive'); } if (configToValidate.network.retries < 0) { errors.push('Network retries must be non-negative'); } } return { valid: errors.length === 0, errors, warnings }; } isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } isValidSize(size) { return /^\d+(?:\.\d+)?[KMGT]?B$/i.test(size); } isValidDuration(duration) { return /^\d+[smhdwMy]$/.test(duration); } parseSize(size) { const match = size.match(/^(\d+(?:\.\d+)?)([KMGT]?)B$/i); if (!match) return 0; const [, value, unit] = match; const multipliers = { '': 1, K: 1024, M: 1024**2, G: 1024**3, T: 1024**4 }; return parseFloat(value) * (multipliers[unit.toUpperCase()] || 1); } parseDuration(duration) { const match = duration.match(/^(\d+)([smhdwMy])$/); if (!match) return 0; const [, value, unit] = match; const multipliers = { s: 1000, m: 60 * 1000, h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000, w: 7 * 24 * 60 * 60 * 1000, M: 30 * 24 * 60 * 60 * 1000, y: 365 * 24 * 60 * 60 * 1000 }; return parseInt(value) * multipliers[unit]; } // Environment variable overrides applyEnvironmentOverrides() { const envMappings = { 'ALEPM_REGISTRY': 'registry', 'ALEPM_CACHE': 'cache.enabled', 'ALEPM_CACHE_DIR': 'cache.directory', 'ALEPM_LOGLEVEL': 'output.loglevel', 'ALEPM_PROXY': 'network.proxy', 'ALEPM_HTTPS_PROXY': 'network.httpsProxy', 'ALEPM_NO_PROXY': 'network.noProxy', 'ALEPM_TIMEOUT': 'network.timeout', 'ALEPM_RETRIES': 'network.retries' }; for (const [envVar, configPath] of Object.entries(envMappings)) { const envValue = process.env[envVar]; if (envValue !== undefined) { // Convert string values to appropriate types let value = envValue; if (envValue === 'true') value = true; else if (envValue === 'false') value = false; else if (/^\d+$/.test(envValue)) value = parseInt(envValue); this.setNestedValue(this.config, configPath, value); } } } // Export/import configuration async export(format = 'json') { const config = await this.list(); switch (format.toLowerCase()) { case 'json': return JSON.stringify(config, null, 2); case 'yaml': // Would need yaml library throw new Error('YAML export not implemented'); default: throw new Error(`Unsupported export format: ${format}`); } } async import(data, format = 'json') { let importedConfig; switch (format.toLowerCase()) { case 'json': importedConfig = JSON.parse(data); break; default: throw new Error(`Unsupported import format: ${format}`); } const validation = this.validateConfig(importedConfig); if (!validation.valid) { throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`); } this.config = importedConfig; await this.saveConfig(); } } module.exports = ConfigManager;