// js-bridge.js - Safe JavaScript interop bridge for Baba Yaga /** * JavaScript Bridge for safe interop between Baba Yaga and JavaScript * Provides sandboxed execution with configurable security controls */ export class BabaYagaJSBridge { constructor(config = {}) { this.config = { allowedGlobals: new Set(config.allowedGlobals || [ 'console', 'JSON', 'Math', 'Date', 'performance' ]), allowedFunctions: new Set(config.allowedFunctions || [ 'JSON.parse', 'JSON.stringify', 'Math.abs', 'Math.floor', 'Math.ceil', 'Math.round', 'Math.min', 'Math.max', 'Math.random', 'console.log', 'console.warn', 'console.error', 'Date.now', 'performance.now' ]), maxExecutionTime: config.maxExecutionTime || 5000, maxMemoryUsage: config.maxMemoryUsage || 50_000_000, enableAsyncOps: config.enableAsyncOps !== false, enableFileSystem: config.enableFileSystem || false, enableNetwork: config.enableNetwork || false }; // Create sandbox after config is set up this.config.sandbox = config.sandbox || this.createDefaultSandbox(); this.lastError = null; this.stats = { totalCalls: 0, successfulCalls: 0, errorCalls: 0, totalExecutionTime: 0 }; } createDefaultSandbox() { const sandbox = { // Safe globals console: { log: console.log.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), time: console.time.bind(console), timeEnd: console.timeEnd.bind(console) }, JSON: { parse: JSON.parse.bind(JSON), stringify: JSON.stringify.bind(JSON) }, Math: Math, Date: { now: Date.now.bind(Date) }, performance: typeof performance !== 'undefined' ? { now: performance.now.bind(performance), mark: performance.mark?.bind(performance), measure: performance.measure?.bind(performance) } : undefined }; // Add conditional globals based on environment if (typeof fetch !== 'undefined' && this.config.enableNetwork) { sandbox.fetch = fetch; } if (typeof require !== 'undefined' && this.config.enableFileSystem) { sandbox.fs = require('fs'); sandbox.path = require('path'); } // Add global functions that are in the allowed list (for testing) if (typeof global !== 'undefined') { for (const functionName of this.config.allowedFunctions) { if (functionName.indexOf('.') === -1 && typeof global[functionName] === 'function') { sandbox[functionName] = global[functionName]; } } } return sandbox; } /** * Call a JavaScript function safely with error handling */ callFunction(functionName, args = []) { const startTime = performance.now(); this.stats.totalCalls++; try { if (!this.config.allowedFunctions.has(functionName)) { throw new Error(`Function ${functionName} is not allowed`); } const fn = this.resolveFunction(functionName); if (!fn) { throw new Error(`Function ${functionName} not found`); } // Execute with timeout protection const result = this.executeWithTimeout(() => { return fn.apply(this.config.sandbox, args); }, this.config.maxExecutionTime); const sanitized = this.sanitizeResult(result); this.stats.successfulCalls++; this.stats.totalExecutionTime += performance.now() - startTime; return { type: 'success', value: sanitized }; } catch (error) { this.lastError = error; this.stats.errorCalls++; return { type: 'error', error: error.message, errorType: error.constructor.name, stack: error.stack }; } } /** * Call a JavaScript function asynchronously */ async callFunctionAsync(functionName, args = []) { if (!this.config.enableAsyncOps) { return { type: 'error', error: 'Async operations are disabled' }; } const startTime = performance.now(); this.stats.totalCalls++; try { if (!this.config.allowedFunctions.has(functionName)) { throw new Error(`Function ${functionName} is not allowed`); } const fn = this.resolveFunction(functionName); if (!fn) { throw new Error(`Function ${functionName} not found`); } // Execute async with timeout protection const result = await this.executeAsyncWithTimeout(() => { return fn.apply(this.config.sandbox, args); }, this.config.maxExecutionTime); const sanitized = this.sanitizeResult(result); this.stats.successfulCalls++; this.stats.totalExecutionTime += performance.now() - startTime; return { type: 'success', value: sanitized }; } catch (error) { this.lastError = error; this.stats.errorCalls++; return { type: 'error', error: error.message, errorType: error.constructor.name, stack: error.stack }; } } /** * Resolve a function from the sandbox by dot-notation path */ resolveFunction(functionName) { const parts = functionName.split('.'); let current = this.config.sandbox; for (const part of parts) { if (!current || typeof current !== 'object') { return null; } current = current[part]; } return typeof current === 'function' ? current : null; } /** * Execute function with timeout protection */ executeWithTimeout(fn, timeout) { // For sync operations, we can't truly timeout in JS // This is a placeholder for potential future timeout implementation return fn(); } /** * Execute async function with timeout protection */ async executeAsyncWithTimeout(fn, timeout) { return Promise.race([ fn(), new Promise((_, reject) => setTimeout(() => reject(new Error('Operation timed out')), timeout) ) ]); } /** * Sanitize JavaScript results for Baba Yaga consumption */ sanitizeResult(value) { if (value === null || value === undefined) { return null; // Will be converted to Err by Baba Yaga } if (typeof value === 'function') { return '[Function]'; // Don't leak functions } if (value instanceof Error) { return { error: value.message, errorType: value.constructor.name, stack: value.stack }; } if (value instanceof Promise) { return '[Promise]'; // Don't leak promises } if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { return value.map(item => this.sanitizeResult(item)); } // Sanitize object properties const sanitized = {}; for (const [key, val] of Object.entries(value)) { if (typeof val !== 'function') { sanitized[key] = this.sanitizeResult(val); } } return sanitized; } return value; } /** * Get property from JavaScript object safely */ getProperty(obj, propName) { try { if (obj === null || obj === undefined) { return { type: 'error', error: 'Cannot get property of null/undefined' }; } if (typeof obj !== 'object') { return { type: 'error', error: 'Cannot get property of non-object' }; } const value = obj[propName]; const sanitized = this.sanitizeResult(value); return { type: 'success', value: sanitized }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Set property on JavaScript object safely */ setProperty(obj, propName, value) { try { if (obj === null || obj === undefined) { return { type: 'error', error: 'Cannot set property of null/undefined' }; } if (typeof obj !== 'object') { return { type: 'error', error: 'Cannot set property of non-object' }; } obj[propName] = value; return { type: 'success', value: obj }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Check if property exists on JavaScript object */ hasProperty(obj, propName) { try { if (obj === null || obj === undefined) { return false; } return propName in obj; } catch (error) { return false; } } /** * Convert JavaScript array to list safely */ jsArrayToList(jsArray) { try { if (!Array.isArray(jsArray)) { return { type: 'error', error: 'Value is not an array' }; } const sanitized = jsArray.map(item => this.sanitizeResult(item)); return { type: 'success', value: sanitized }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Convert Baba Yaga list to JavaScript array */ listToJSArray(babaList) { try { if (!Array.isArray(babaList)) { return { type: 'error', error: 'Value is not a list' }; } return { type: 'success', value: [...babaList] }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Convert Baba Yaga table to JavaScript object */ tableToObject(babaTable) { try { if (!babaTable || babaTable.type !== 'Object' || !babaTable.properties) { return { type: 'error', error: 'Value is not a Baba Yaga table' }; } const obj = {}; for (const [key, value] of babaTable.properties.entries()) { obj[key] = this.convertBabaValueToJS(value); } return { type: 'success', value: obj }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Convert JavaScript object to Baba Yaga table */ objectToTable(jsObj) { try { if (typeof jsObj !== 'object' || jsObj === null || Array.isArray(jsObj)) { return { type: 'error', error: 'Value is not a JavaScript object' }; } const properties = new Map(); for (const [key, value] of Object.entries(jsObj)) { properties.set(key, this.convertJSValueToBaba(value)); } return { type: 'success', value: { type: 'Object', properties } }; } catch (error) { return { type: 'error', error: error.message }; } } /** * Convert Baba Yaga value to JavaScript value */ convertBabaValueToJS(babaValue) { if (babaValue && typeof babaValue.value === 'number') { return babaValue.value; } if (Array.isArray(babaValue)) { return babaValue.map(item => this.convertBabaValueToJS(item)); } if (babaValue && babaValue.type === 'Object' && babaValue.properties instanceof Map) { const obj = {}; for (const [key, value] of babaValue.properties.entries()) { obj[key] = this.convertBabaValueToJS(value); } return obj; } // Handle JSValue objects from io.callJS if (babaValue && babaValue.type === 'JSValue') { return babaValue.value; } return babaValue; } /** * Convert JavaScript value to Baba Yaga value */ convertJSValueToBaba(jsValue) { if (typeof jsValue === 'number') { return { value: jsValue, isFloat: !Number.isInteger(jsValue) }; } if (Array.isArray(jsValue)) { return jsValue.map(item => this.convertJSValueToBaba(item)); } if (typeof jsValue === 'object' && jsValue !== null) { const properties = new Map(); for (const [key, value] of Object.entries(jsValue)) { properties.set(key, this.convertJSValueToBaba(value)); } return { type: 'Object', properties }; } return jsValue; } /** * Get bridge statistics */ getStats() { const successRate = this.stats.totalCalls > 0 ? this.stats.successfulCalls / this.stats.totalCalls : 0; const averageTime = this.stats.successfulCalls > 0 ? this.stats.totalExecutionTime / this.stats.successfulCalls : 0; return { ...this.stats, successRate, averageTime }; } /** * Get last JavaScript error */ getLastError() { return this.lastError ? { message: this.lastError.message, type: this.lastError.constructor.name, stack: this.lastError.stack } : null; } /** * Clear last JavaScript error */ clearLastError() { this.lastError = null; } /** * Reset statistics */ resetStats() { this.stats = { totalCalls: 0, successfulCalls: 0, errorCalls: 0, totalExecutionTime: 0 }; } } /** * Create a default JS bridge instance */ export function createDefaultJSBridge(config = {}) { return new BabaYagaJSBridge(config); }