beeline-cli
Version:
A terminal wallet for the Hive blockchain - type, sign, rule the chain
510 lines • 23.1 kB
JavaScript
;
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