Source: lib/core/security/sandbox.js

import { AvenxError, AvenxErrorCodes } from '../runtime/AvenxError.js';
import { resolveSandboxGlobal } from '../trace/globals.js';
/**
 * @file sandbox.js
 * @description Secure Template Expression Sandbox module for the Avenx-JS framework.
 * Located at `lib/core/security/sandbox.js`.
 * To prevent critical security vulnerabilities such as prototype pollution and Cross-Site Scripting (XSS),
 * all template expressions and computed properties execute within an isolated execution sandbox wrapper.
 * The sandbox dynamically guards evaluation context by blocking access to structural object properties
 * (`__proto__`, `constructor`, `prototype`) and restricts active identifier scopes strictly to the
 * `ALLOWED_GLOBALS` whitelist map.
 * If an expression attempts to invoke an unauthorized standard window API environment variable
 * (e.g., calling `alert()` or checking `localStorage` directly in an HTML attribute binding),
 * the application state will gracefully halt and trigger an execution failure throw code: **`AVX_R15`**.
 * @example
 * // ❌ ANTI-PATTERN (Will trigger an AVX_R15 Sandbox Violation at runtime)
 * // <button onclick="alert('Operation successful!')">Submit</button>
 * // <div v-if="localStorage.getItem('user_token')">Profile Content</div>
 * @example
 * // ✅ PROPER ARCHITECTURAL PATTERN
 * // Decouple browser environment window APIs into standard component method actions:
 * export default {
 * name: 'SecureActionComponent',
 * methods: {
 * handleSubmit() {
 * // Native browser ecosystem APIs are fully available here
 * alert('Operation successful!');
 * localStorage.setItem('user_token', 'validated_hash');
 * }
 * }
 * };
 */

/**
 * Whitelist allocation set tracking global identifiers authorized for inline evaluation scope.
 * Global scopes outside this tracking collection are rejected with a sandbox exception.
 * @type {Set<string>}
 * @property {string} Math - Native mathematical calculations and constants.
 * @property {string} JSON - Structured string serialization and parsing tools.
 * @property {string} Array - Array generation constructors.
 * @property {string} Object - Standard JavaScript object manipulators.
 * @property {string} String - Text parsing constructors.
 * @property {string} console - Core debugging console methods.
 * @property {string} parseInt - Numeric text converter algorithms.
 * @property {string} parseFloat - Decimal text converter algorithms.
 */

const ALLOWED_GLOBALS = new Set([
  'Math',
  'JSON',
  'Array',
  'Object',
  'String',
  'Number',
  'Boolean',
  'Date',
  'Error',
  'Map',
  'Set',
  'Promise',
  'console',
  'parseInt',
  'parseFloat',
  'isNaN',
  'isFinite',
  'decodeURI',
  'decodeURIComponent',
  'encodeURI',
  'encodeURIComponent',
  'undefined',
  'NaN',
  'Infinity',
]);

const RESTRICTED_GLOBALS = new Set([
  'window',
  'document',
  'localStorage',
  'sessionStorage',
  'location',
  'navigator',
  'history',
  'fetch',
  'alert',
  'confirm',
  'prompt',
  'setTimeout',
  'setInterval',
  'clearTimeout',
  'clearInterval',
  'XMLHttpRequest',
  'WebSocket',
  'global',
  'globalThis',
  'process',
  'eval',
  'Function',
]);

/**
 * Determines whether a property key refers to a restricted global object.
 * @param {string|symbol} key - Property key to check.
 * @returns {boolean}
 */
function isRestrictedGlobal(key) {
  if (typeof key !== 'string') return false;
  if (ALLOWED_GLOBALS.has(key)) return false;
  if (RESTRICTED_GLOBALS.has(key)) return true;
  try {
    return typeof globalThis !== 'undefined' && key in globalThis;
  } catch {
    return false;
  }
}

const RAW_TARGET = Symbol.for('rawTarget');
const proxyCache = new WeakMap();

// NOTE: this module deliberately does not patch `Function.prototype.constructor`.
// Doing so at import time altered a shared intrinsic for the whole realm, so
// every unrelated library on the page saw `fn.constructor` throw — a common
// idiom in type guards, polyfills and serializers. Access to the Function
// constructor from inside a template is blocked where it belongs instead: on
// the sandbox proxies themselves (see FUNCTION_CONSTRUCTORS below).

/**
 * The dynamic-code constructors. Reaching any of these from a template would
 * allow arbitrary code execution, so the sandbox refuses to hand them out or
 * invoke them regardless of the route taken to obtain them (property access,
 * property descriptors, prototype walks).
 * @type {Set<Function>}
 */
const FUNCTION_CONSTRUCTORS = new Set(
  [
    Function,
    Object.getPrototypeOf(function* () {}).constructor,
    Object.getPrototypeOf(async () => {}).constructor,
    Object.getPrototypeOf(async function* () {}).constructor,
  ].filter((ctor) => typeof ctor === 'function'),
);

/**
 * Throws when a value is one of the dynamic-code constructors.
 * @param {any} value - The value about to be returned or invoked.
 */
function assertNotFunctionConstructor(value) {
  if (typeof value === 'function' && FUNCTION_CONSTRUCTORS.has(value)) {
    throw new AvenxError(
      AvenxErrorCodes.SANDBOX_VIOLATION,
      'Access to the Function constructor is blocked for security reasons.',
    );
  }
}

/**
 * Built-in prototypes shared by every object in the realm.
 *
 * `validateSource` only rejects the literal identifiers `constructor`,
 * `__proto__` and `prototype`, which a computed access such as
 * `Object.getPrototypeOf({})` or `Object['proto' + 'type']` walks straight
 * past. Handing one of these objects to a template would let it mutate state
 * shared with the host application, so the sandbox refuses to surface them at
 * all rather than trying to enumerate every mutating API.
 * @type {Set<object>}
 */
const PROTECTED_PROTOTYPES = new Set(
  [
    Object.prototype,
    Array.prototype,
    Function.prototype,
    String.prototype,
    Number.prototype,
    Boolean.prototype,
    Date.prototype,
    RegExp.prototype,
    Error.prototype,
    Map.prototype,
    Set.prototype,
    WeakMap.prototype,
    WeakSet.prototype,
    Promise.prototype,
  ].filter((proto) => proto !== null && proto !== undefined),
);

/**
 * Throws when a value is a shared built-in prototype.
 * @param {any} value - The value about to be returned, passed or invoked.
 */
function assertNotProtectedPrototype(value) {
  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
    return;
  }
  if (PROTECTED_PROTOTYPES.has(value)) {
    throw new AvenxError(
      AvenxErrorCodes.SANDBOX_VIOLATION,
      'Access to built-in prototypes is blocked for security reasons.',
    );
  }
}

/**
 * Unwraps a value if it's a sandbox Proxy, returning its raw target object.
 * @param {any} val - The value to unwrap.
 * @returns {any}
 */
function unwrap(val) {
  if (val && typeof val === 'object' && val[RAW_TARGET]) {
    return val[RAW_TARGET];
  }
  return val;
}

/**
 * Wraps an object or function recursively in a Proxy that blocks prototype pollution
 * and un-proxies arguments/context when called.
 * @param {any} val - The value to wrap.
 * @returns {any}
 */
function wrapValue(val) {
  if (val === null || val === undefined) {
    return val;
  }

  if (typeof val !== 'object' && typeof val !== 'function') {
    return val;
  }

  if (proxyCache.has(val)) {
    return proxyCache.get(val);
  }

  const traps = {
    /**
     * Intercepts property retrieval.
     * @param {object} target - The target object.
     * @param {string|symbol} key - The property name.
     * @param {object} receiver - The Proxy or inherits from it.
     * @returns {any}
     */
    get(target, key, receiver) {
      if (key === RAW_TARGET) {
        return target;
      }

      if (key === '__proto__') {
        throw new AvenxError(
          AvenxErrorCodes.SANDBOX_VIOLATION,
          'Access to property "__proto__" is blocked for security reasons.',
        );
      }
      if (key === 'constructor' && typeof target === 'function') {
        throw new AvenxError(
          AvenxErrorCodes.SANDBOX_VIOLATION,
          'Access to property "constructor" on functions is blocked for security reasons.',
        );
      }

      let res;
      try {
        // The proxy is the receiver so reactive state keeps tracking property
        // reads through it.
        res = Reflect.get(target, key, receiver);
      } catch (err) {
        // Accessors backed by an internal slot (Set.prototype.size,
        // Map.prototype.size) reject a receiver that does not carry the slot.
        if (err instanceof TypeError) {
          res = Reflect.get(target, key, target);
        } else {
          throw err;
        }
      }

      // Never hand out a dynamic-code constructor or a shared built-in
      // prototype, however they were reached.
      assertNotFunctionConstructor(res);
      assertNotProtectedPrototype(res);

      const desc = Reflect.getOwnPropertyDescriptor(target, key);
      if (desc && !desc.configurable && !desc.writable) {
        return res;
      }

      if (typeof res === 'function') {
        res = res.bind(target);
      }

      return wrapValue(res);
    },

    /**
     * Intercepts property assignment.
     * @param {object} target - The target object.
     * @param {string|symbol} key - The property name.
     * @param {any} value - The new value.
     * @param {object} receiver - The object originally targeted.
     * @returns {boolean}
     */
    set(target, key, value, receiver) {
      if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
        throw new AvenxError(
          AvenxErrorCodes.SANDBOX_VIOLATION,
          `Writing to property "${String(key)}" is blocked for security reasons.`,
        );
      }
      return Reflect.set(target, key, value, receiver);
    },
  };

  if (typeof val === 'function') {
    /**
     * Intercepts function execution.
     * @param {object} target - The target function.
     * @param {any} thisArg - The context.
     * @param {any[]} argumentsList - The arguments passed.
     * @returns {any}
     */
    traps.apply = function (target, thisArg, argumentsList) {
      assertNotFunctionConstructor(target);
      const rawThis = unwrap(thisArg);
      const rawArgs = argumentsList.map(unwrap);

      // A reflection call such as Object.assign or Object.defineProperty must
      // not be handed a shared prototype as its target.
      assertNotProtectedPrototype(rawThis);
      rawArgs.forEach(assertNotProtectedPrototype);

      const result = Reflect.apply(target, rawThis, rawArgs);
      assertNotFunctionConstructor(result);
      assertNotProtectedPrototype(result);
      return wrapValue(result);
    };

    /**
     * Intercepts `new` on a wrapped function.
     *
     * The raw target is used as newTarget. Passing the proxy would make
     * Reflect.construct read `newTarget.prototype` back through the `get`
     * trap, where the built-in prototype guard would reject the constructor's
     * own prototype and break ordinary `new Date(...)` / `new Map(...)` calls.
     * @param {object} target - The target function.
     * @param {any[]} argumentsList - The arguments passed.
     * @returns {any}
     */
    traps.construct = function (target, argumentsList) {
      assertNotFunctionConstructor(target);
      const rawArgs = argumentsList.map(unwrap);
      rawArgs.forEach(assertNotProtectedPrototype);
      return wrapValue(Reflect.construct(target, rawArgs, target));
    };
  }

  const wrapped = new Proxy(val, traps);
  proxyCache.set(val, wrapped);
  return wrapped;
}

/**
 * Handles creation of secure sandbox contexts.
 */
export class AvenxSandbox {
  /**
   * Statically validates an expression or statement string to ensure it does not contain
   * forbidden property names.
   * @param {string} source - The source code to check.
   */
  static validateSource(source) {
    const FORBIDDEN_WORDS = /\b(constructor|__proto__|prototype)\b/;
    if (typeof source === 'string' && FORBIDDEN_WORDS.test(source)) {
      throw new AvenxError(
        AvenxErrorCodes.SANDBOX_VIOLATION,
        'Access to "constructor", "__proto__", or "prototype" is blocked for security reasons.',
      );
    }
  }
  /**
   * Creates a sandboxed Proxy context representing the combined scope and thisArg.
   * @param {object} scope - The scope variables.
   * @param {object} thisArg - The active 'this' context.
   * @param {boolean} [excludeParams] - If true, excludes special event params from the has trap.
   * @returns {Proxy} The sandboxed Proxy object.
   */
  static createProxy(scope, thisArg, excludeParams = false) {
    const target = {};
    const activeThis = thisArg || scope || {};

    return new Proxy(target, {
      /**
       * Intercepts `has` check, claiming to have all properties to capture lookups in `with`.
       * @param {object} t - The target object.
       * @param {string|symbol} key - The property checked.
       * @returns {boolean}
       */
      has(t, key) {
        if (key === Symbol.unscopables) {
          return false;
        }
        if (excludeParams && (key === 'state' || key === 'methods' || key === 'event' || key === 'args')) {
          return false;
        }
        return true;
      },

      /**
       * Intercepts property retrieval.
       * @param {object} t - The target object.
       * @param {string|symbol} key - The property name.
       * @returns {any}
       */
      get(t, key) {
        if (key === Symbol.unscopables) {
          return undefined;
        }

        if (key === RAW_TARGET) {
          return activeThis;
        }

        if (key === '__proto__') {
          throw new AvenxError(
            AvenxErrorCodes.SANDBOX_VIOLATION,
            'Access to property "__proto__" is blocked for security reasons.',
          );
        }
        if (key === 'constructor' && typeof activeThis === 'function') {
          throw new AvenxError(
            AvenxErrorCodes.SANDBOX_VIOLATION,
            'Access to property "constructor" on functions is blocked for security reasons.',
          );
        }

        if (scope && key in scope) {
          return wrapValue(scope[key]);
        }

        if (thisArg && key in thisArg) {
          return wrapValue(thisArg[key]);
        }

        // Resolved through the tracer rather than read off globalThis so a
        // recording session can log the non-deterministic values template code
        // observes, and a replaying session can hand the same ones back. With
        // no recording in progress this is a Map.size check and the same
        // property read as before. Substituting here rather than patching the
        // page's real globals keeps the swap invisible to unrelated scripts
        // and impossible to outlive the recording.
        if (ALLOWED_GLOBALS.has(key)) {
          return wrapValue(resolveSandboxGlobal(key));
        }

        if (isRestrictedGlobal(key)) {
          throw new AvenxError(
            AvenxErrorCodes.SANDBOX_VIOLATION,
            `[Avenx Sandbox Violation] Access to global object "${String(key)}" is restricted inside templates. Decouple browser APIs into component methods.`,
          );
        }

        return undefined;
      },

      /**
       * Intercepts property assignment.
       * @param {object} t - The target object.
       * @param {string|symbol} key - The property name.
       * @param {any} value - The new value.
       * @returns {boolean}
       */
      set(t, key, value) {
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
          throw new AvenxError(
            AvenxErrorCodes.SANDBOX_VIOLATION,
            `Writing to property "${String(key)}" is blocked for security reasons.`,
          );
        }

        if (isRestrictedGlobal(key) && (!scope || !(key in scope)) && (!thisArg || !(key in thisArg))) {
          throw new AvenxError(
            AvenxErrorCodes.SANDBOX_VIOLATION,
            `[Avenx Sandbox Violation] Access to global object "${String(key)}" is restricted inside templates. Decouple browser APIs into component methods.`,
          );
        }

        if (thisArg && key in thisArg) {
          thisArg[key] = value;
          return true;
        }

        if (scope && key in scope) {
          scope[key] = value;
          return true;
        }

        if (scope) {
          scope[key] = value;
          return true;
        }

        return false;
      },

      /**
       * Intercepts `getPrototypeOf` check.
       * @returns {object}
       */
      getPrototypeOf() {
        return Reflect.getPrototypeOf(activeThis);
      },

      /**
       * Intercepts `getOwnPropertyDescriptor` check.
       * @param {object} t - The target.
       * @param {string|symbol} key - The property.
       * @returns {object}
       */
      getOwnPropertyDescriptor(t, key) {
        const desc = Reflect.getOwnPropertyDescriptor(activeThis, key);
        if (desc) return desc;
        return { configurable: true, enumerable: true, writable: true };
      },
    });
  }
}