about summary refs log tree commit diff stats
path: root/js/baba-yaga/src/core/js-bridge.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/src/core/js-bridge.js')
-rw-r--r--js/baba-yaga/src/core/js-bridge.js507
1 files changed, 507 insertions, 0 deletions
diff --git a/js/baba-yaga/src/core/js-bridge.js b/js/baba-yaga/src/core/js-bridge.js
new file mode 100644
index 0000000..92a9972
--- /dev/null
+++ b/js/baba-yaga/src/core/js-bridge.js
@@ -0,0 +1,507 @@
+// 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);
+}