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
JavaScript
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;