UNPKG

beeline-cli

Version:

A terminal wallet for the Hive blockchain - type, sign, rule the chain

510 lines 23.1 kB
"use strict"; 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginManager = void 0; exports.getPluginManager = getPluginManager; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const neon_js_1 = require("./neon.js"); // Plugin manager - main orchestrator class PluginManager { constructor(dataDir) { this.plugins = new Map(); this.eventBus = new events_1.EventEmitter(); this.dataDir = dataDir || path.join(process.env.HOME || '', '.beeline', 'plugins'); } async initialize() { await fs.ensureDir(this.dataDir); await this.loadInstalledPlugins(); } async installPlugin(source) { const theme = await (0, neon_js_1.getTheme)(); console.log(theme.chalk.info(`${neon_js_1.neonSymbols.download} Installing plugin from ${source}...`)); // Implementation for npm packages, git repos, or local paths // This would handle downloading, validation, and installation throw new Error('Plugin installation not yet implemented'); } async loadPlugin(pluginPath) { try { // Security: Validate plugin path is within allowed directories const resolvedPath = path.resolve(pluginPath); if (!this.isAllowedPluginPath(resolvedPath)) { throw new Error('Plugin path not allowed - must be within plugin directory or explicitly trusted'); } const manifestPath = path.join(resolvedPath, 'package.json'); const manifest = await fs.readJson(manifestPath); // Validate manifest this.validateManifest(manifest); // Security: Check if plugin already loaded if (this.plugins.has(manifest.name)) { throw new Error(`Plugin ${manifest.name} is already loaded`); } // Check permissions if (!await this.checkPermissions(manifest)) { throw new Error(`Plugin ${manifest.name} requires permissions that are not granted`); } // Security: Validate main file exists and has safe extension const mainPath = path.join(resolvedPath, manifest.main); if (!await fs.pathExists(mainPath)) { throw new Error(`Plugin main file not found: ${manifest.main}`); } if (!this.isSafePluginFile(mainPath)) { throw new Error(`Plugin main file has unsafe extension: ${manifest.main}`); } // Load plugin code with timeout const pluginModule = await Promise.race([ Promise.resolve(`${mainPath}`).then(s => __importStar(require(s))), new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin loading timeout')), 5000)) ]); const plugin = pluginModule.default || pluginModule; // Validate plugin interface if (!plugin || typeof plugin.activate !== 'function') { throw new Error('Plugin must export an object with activate() method'); } // Create sandboxed context const context = this.createPluginContext(manifest, resolvedPath); // Activate plugin with timeout await Promise.race([ plugin.activate(context), new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin activation timeout')), 10000)) ]); this.plugins.set(manifest.name, { manifest, plugin, context, path: resolvedPath, active: true }); const theme = await (0, neon_js_1.getTheme)(); console.log(theme.chalk.success(`${neon_js_1.neonSymbols.check} Loaded plugin: ${manifest.name} v${manifest.version}`)); } catch (error) { const theme = await (0, neon_js_1.getTheme)(); console.error(theme.chalk.error(`${neon_js_1.neonSymbols.cross} Failed to load plugin: ${error instanceof Error ? error.message : 'Unknown error'}`)); throw error; } } async unloadPlugin(name) { const loadedPlugin = this.plugins.get(name); if (!loadedPlugin) return; try { if (loadedPlugin.plugin.deactivate) { await loadedPlugin.plugin.deactivate(); } this.plugins.delete(name); const theme = await (0, neon_js_1.getTheme)(); console.log(theme.chalk.info(`${neon_js_1.neonSymbols.bullet} Unloaded plugin: ${name}`)); } catch (error) { const theme = await (0, neon_js_1.getTheme)(); console.error(theme.chalk.error(`${neon_js_1.neonSymbols.cross} Error unloading plugin ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } listPlugins() { return Array.from(this.plugins.values()); } getPlugin(name) { return this.plugins.get(name); } async loadInstalledPlugins() { try { const pluginDirs = await fs.readdir(this.dataDir); for (const dir of pluginDirs) { const pluginPath = path.join(this.dataDir, dir); const stat = await fs.stat(pluginPath); if (stat.isDirectory()) { await this.loadPlugin(pluginPath); } } } catch (error) { // Directory doesn't exist yet or other error // This is normal for first run } } validateManifest(manifest) { const required = ['name', 'version', 'description', 'author', 'main', 'permissions', 'beeline']; for (const field of required) { if (!(field in manifest)) { throw new Error(`Plugin manifest missing required field: ${field}`); } } // Validate semver if (!manifest.version.match(/^\d+\.\d+\.\d+/)) { throw new Error('Plugin version must be valid semver'); } // Validate beeline version compatibility // This would check against current beeline version } async checkPermissions(manifest) { const dangerousPermissions = ['system:exec', 'hive:write']; const highRiskPermissions = ['keys:read', 'network:http']; // Check for dangerous permissions const dangerous = manifest.permissions.filter(p => dangerousPermissions.includes(p)); const highRisk = manifest.permissions.filter(p => highRiskPermissions.includes(p)); if (dangerous.length > 0) { const theme = await (0, neon_js_1.getTheme)(); console.log(''); console.log(theme.chalk.error(`${neon_js_1.neonSymbols.warning} SECURITY WARNING: Plugin ${manifest.name} requests DANGEROUS permissions:`)); dangerous.forEach(p => { const description = this.getPermissionDescription(p); console.log(theme.chalk.error(` ${neon_js_1.neonSymbols.cross} ${p} - ${description}`)); }); console.log(''); console.log(theme.chalk.warning('These permissions allow the plugin to:')); console.log(theme.chalk.warning('- Access private keys and send transactions')); console.log(theme.chalk.warning('- Execute system commands')); console.log(theme.chalk.warning('- Potentially steal funds or compromise security')); console.log(''); // For now, auto-deny dangerous permissions // In a full implementation, would prompt with strong warnings console.log(theme.chalk.error('Dangerous permissions are currently disabled for security.')); return false; } if (highRisk.length > 0) { const theme = await (0, neon_js_1.getTheme)(); console.log(''); console.log(theme.chalk.warning(`${neon_js_1.neonSymbols.warning} Plugin ${manifest.name} requests elevated permissions:`)); highRisk.forEach(p => { const description = this.getPermissionDescription(p); console.log(theme.chalk.warning(` ${neon_js_1.neonSymbols.bullet} ${p} - ${description}`)); }); console.log(''); console.log(theme.chalk.info('These permissions are granted automatically but monitored.')); } // Auto-grant safe permissions const safePermissions = ['hive:read', 'accounts:read', 'storage:read', 'storage:write', 'ui:commands', 'ui:hooks']; const allSafe = manifest.permissions.every(p => safePermissions.includes(p) || highRiskPermissions.includes(p)); if (allSafe) { return true; } // Block unknown permissions const unknownPermissions = manifest.permissions.filter(p => !safePermissions.includes(p) && !highRiskPermissions.includes(p) && !dangerousPermissions.includes(p)); if (unknownPermissions.length > 0) { const theme = await (0, neon_js_1.getTheme)(); console.log(theme.chalk.error(`${neon_js_1.neonSymbols.cross} Plugin requests unknown permissions: ${unknownPermissions.join(', ')}`)); return false; } return true; } getPermissionDescription(permission) { const descriptions = { 'hive:read': 'Read blockchain data and account information', 'hive:write': 'Send transactions and operations to the blockchain', 'keys:read': 'Access key vault and account list', 'accounts:read': 'View account information', 'network:http': 'Make HTTP requests to external services', 'storage:read': 'Read plugin data from storage', 'storage:write': 'Write plugin data to storage', 'ui:commands': 'Register new CLI commands', 'ui:hooks': 'Hook into existing commands', 'system:exec': 'Execute system commands (VERY DANGEROUS)' }; return descriptions[permission] || 'Unknown permission'; } isAllowedPluginPath(pluginPath) { const allowedPaths = [ this.dataDir, // ~/.beeline/plugins path.resolve('./examples/plugins'), // Development examples path.resolve('./plugins'), // Local plugins ]; // Check if path is within allowed directories return allowedPaths.some(allowedPath => { const relative = path.relative(allowedPath, pluginPath); return !relative.startsWith('..') && !path.isAbsolute(relative); }); } isSafePluginFile(filePath) { const allowedExtensions = ['.js', '.mjs', '.cjs']; const ext = path.extname(filePath).toLowerCase(); return allowedExtensions.includes(ext); } createPluginContext(manifest, pluginPath) { return { hive: this.createHiveAPI(manifest.permissions), keys: this.createKeysAPI(manifest.permissions), accounts: this.createAccountsAPI(manifest.permissions), ui: this.createUIAPI(manifest.permissions), storage: this.createStorageAPI(manifest.name), events: this.createEventBus(), plugin: { name: manifest.name, version: manifest.version, dataPath: path.join(this.dataDir, manifest.name, 'data') } }; } createHiveAPI(permissions) { const hasReadPermission = permissions.includes('hive:read'); const hasWritePermission = permissions.includes('hive:write'); return { async getBalance(account) { if (!hasReadPermission) { throw new Error('Plugin does not have hive:read permission'); } // Import HiveClient lazily to avoid circular dependencies const { HiveClient } = await Promise.resolve().then(() => __importStar(require('./hive.js'))); const { KeyManager } = await Promise.resolve().then(() => __importStar(require('./crypto.js'))); const keyManager = new KeyManager(); await keyManager.initialize(); const hiveClient = new HiveClient(keyManager); return hiveClient.getBalance(account); }, async getAccount(account) { if (!hasReadPermission) { throw new Error('Plugin does not have hive:read permission'); } const { HiveClient } = await Promise.resolve().then(() => __importStar(require('./hive.js'))); const { KeyManager } = await Promise.resolve().then(() => __importStar(require('./crypto.js'))); const keyManager = new KeyManager(); await keyManager.initialize(); const hiveClient = new HiveClient(keyManager); return hiveClient.getAccount(account); }, async getDynamicGlobalProperties() { if (!hasReadPermission) { throw new Error('Plugin does not have hive:read permission'); } const { HiveClient } = await Promise.resolve().then(() => __importStar(require('./hive.js'))); const { KeyManager } = await Promise.resolve().then(() => __importStar(require('./crypto.js'))); const keyManager = new KeyManager(); await keyManager.initialize(); const hiveClient = new HiveClient(keyManager); // Access the underlying client for global properties return hiveClient['client'].database.getDynamicGlobalProperties(); }, async sendOperation(op, key) { if (!hasWritePermission) { throw new Error('Plugin does not have hive:write permission'); } // Plugins should not have direct transaction access for security // This would require specific wrapper methods for safe operations throw new Error('Direct transaction sending not available to plugins. Use specific transfer methods.'); } }; } createKeysAPI(permissions) { const hasReadPermission = permissions.includes('keys:read'); return { listAccounts() { if (!hasReadPermission) { throw new Error('Plugin does not have keys:read permission'); } // Safe read-only access to account list try { const { KeyManager } = require('./crypto.js'); const keyManager = new KeyManager(); // This would return account names only, no keys return keyManager.listAccounts(); } catch { return []; } }, hasKey(account, role) { if (!hasReadPermission) { throw new Error('Plugin does not have keys:read permission'); } try { const { KeyManager } = require('./crypto.js'); const keyManager = new KeyManager(); return keyManager.hasKey(account, role); } catch { return false; } } }; } createAccountsAPI(permissions) { const hasReadPermission = permissions.includes('accounts:read'); return { list() { if (!hasReadPermission) { throw new Error('Plugin does not have accounts:read permission'); } try { const { KeyManager } = require('./crypto.js'); const keyManager = new KeyManager(); return keyManager.listAccounts(); } catch { return []; } }, getCurrent() { if (!hasReadPermission) { throw new Error('Plugin does not have accounts:read permission'); } try { const { KeyManager } = require('./crypto.js'); const keyManager = new KeyManager(); return keyManager.getDefaultAccount(); } catch { return null; } }, async switch(account) { if (!hasReadPermission) { throw new Error('Plugin does not have accounts:read permission'); } // Plugins cannot switch accounts - read-only access throw new Error('Plugins cannot switch accounts - read-only access'); } }; } createUIAPI(permissions) { const hasCommandsPermission = permissions.includes('ui:commands'); const hasHooksPermission = permissions.includes('ui:hooks'); return { registerCommand(command) { if (!hasCommandsPermission) { throw new Error('Plugin does not have ui:commands permission'); } // Store command registration for later processing // This would integrate with OCLIF command system console.log(`Plugin registered command: ${command.name}`); }, registerHook(event, handler) { if (!hasHooksPermission) { throw new Error('Plugin does not have ui:hooks permission'); } // Store hook registration this.eventBus.on(`hook:${event}`, handler); }, showMessage(message, type = 'info') { // Always allowed - safe operation const colors = { info: '\x1b[36m', // cyan success: '\x1b[32m', // green warning: '\x1b[33m', // yellow error: '\x1b[31m' // red }; const reset = '\x1b[0m'; console.log(`${colors[type]}[Plugin] ${message}${reset}`); }, async prompt(message, options) { // Always allowed but sanitized const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer'))); return inquirer.default.prompt({ type: options?.type || 'input', name: 'answer', message: `[Plugin] ${message}`, choices: options?.choices }).then(answers => answers.answer); } }; } createStorageAPI(pluginName) { const storageDir = path.join(this.dataDir, pluginName, 'data'); return { async get(key) { this.validateStorageKey(key); try { await fs.ensureDir(storageDir); const filePath = path.join(storageDir, `${key}.json`); if (await fs.pathExists(filePath)) { return await fs.readJson(filePath); } return undefined; } catch { return undefined; } }, async set(key, value) { this.validateStorageKey(key); this.validateStorageValue(value); await fs.ensureDir(storageDir); const filePath = path.join(storageDir, `${key}.json`); await fs.writeJson(filePath, value, { spaces: 2 }); }, async delete(key) { this.validateStorageKey(key); const filePath = path.join(storageDir, `${key}.json`); if (await fs.pathExists(filePath)) { await fs.remove(filePath); } }, async clear() { if (await fs.pathExists(storageDir)) { await fs.emptyDir(storageDir); } } }; } validateStorageKey(key) { if (!key || typeof key !== 'string') { throw new Error('Storage key must be a non-empty string'); } if (key.includes('..') || key.includes('/') || key.includes('\\')) { throw new Error('Storage key cannot contain path separators'); } if (key.length > 100) { throw new Error('Storage key too long (max 100 characters)'); } } validateStorageValue(value) { try { const serialized = JSON.stringify(value); if (serialized.length > 1024 * 1024) { // 1MB limit throw new Error('Storage value too large (max 1MB)'); } } catch { throw new Error('Storage value must be JSON serializable'); } } createEventBus() { return { on: (event, handler) => this.eventBus.on(event, handler), off: (event, handler) => this.eventBus.off(event, handler), emit: (event, ...args) => this.eventBus.emit(event, ...args) }; } } exports.PluginManager = PluginManager; // Global plugin manager instance let pluginManagerInstance = null; function getPluginManager() { if (!pluginManagerInstance) { pluginManagerInstance = new PluginManager(); } return pluginManagerInstance; } //# sourceMappingURL=plugin-system.js.map