a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: sandboxed expr evaluator

* planned examples

+535 -153
+19 -19
ROADMAP.md
··· 79 79 - ✓ Form serialization and submission 80 80 - ✓ Request/response headers customization 81 81 82 - ## To-Do 83 - 84 82 ### Markup Based Reactivity 85 83 86 84 **Goal:** Allow Volt apps to declare state, bindings, and behavior entirely in HTML markup ··· 92 90 - ✓ Control-flow directives (`data-volt-for`, `data-volt-if`, `data-volt-else`) with lifecycle-safe teardown. 93 91 - ✓ Declarative event system (`data-volt-on:*`) with helper surface for list mutations and plugin hooks. 94 92 - ✓ SSR compatibility helpers 95 - - Sandboxed expression evaluator 93 + - ✓ Sandboxed expression evaluator 94 + 95 + ## To-Do 96 96 97 97 ### Streaming & Patch Engine 98 98 ··· 176 176 **Outcome:** Volt.js enables declarative animations and view transitions alongside reactivity. 177 177 **Deliverables:** 178 178 - `data-volt-transition` directive with enter/leave transitions 179 - - Transition modifiers (duration, delay, opacity, scale, etc.) 180 - - View Transitions API integration (when available) 181 - - CSS-based transition helpers 179 + - Transition modifiers (duration, delay, opacity, scale, etc.) 180 + - View Transitions API integration (when available) 181 + - CSS-based transition helpers 182 182 - `data-volt-animate` plugin for keyframe animations 183 - - Timing utilities and easing functions 183 + - Timing utilities and easing functions 184 184 - Integration with `data-volt-if` and `data-volt-show` for automatic transitions 185 185 186 186 ### Background Requests & Reactive Polling ··· 188 188 **Goal:** Enable declarative background data fetching and periodic updates within the Volt.js runtime. 189 189 **Outcome:** Volt.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions. 190 190 **Deliverables:** 191 - - `data-volt-fetch` attribute for declarative background requests 192 - - Configurable polling intervals, delays, and signal-based triggers 193 191 - `data-volt-visible` for fetching when an element enters the viewport (`IntersectionObserver`) 192 + - `data-volt-fetch` attribute for declarative background requests 193 + - Configurable polling intervals, delays, and signal-based triggers 194 + - Automatic cancellation of requests when elements are unmounted 195 + - Conditional execution tied to reactive signals 196 + - Integration hooks for loading and pending states 194 197 - Background task scheduler with priority management 195 - - Automatic cancellation of requests when elements are unmounted 196 - - Conditional execution tied to reactive signals 197 - - Integration hooks for loading and pending states 198 198 199 199 ### Navigation & History Management 200 200 ··· 202 202 **Outcome:** Volt.js provides enhanced navigation behavior with minimal overhead and full accessibility support. 203 203 **Deliverables:** 204 204 - `data-volt-navigate` for intercepting link and form actions 205 - - Integration with the History API (`pushState`, `replaceState`, `popState`) 206 - - Reactive synchronization of route and signal state 207 - - Smooth page and fragment transitions coordinated with Volt’s signal system 208 - - Native back/forward button support 209 - - Scroll position persistence and restoration 210 - - Optional preloading of linked resources on hover or idle 205 + - Integration with the History API (`pushState`, `replaceState`, `popState`) 206 + - Reactive synchronization of route and signal state 207 + - Smooth page and fragment transitions coordinated with Volt’s signal system 208 + - Native back/forward button support 209 + - Scroll position persistence and restoration 210 + - Preloading of linked resources on hover or idle 211 211 - `data-volt-url` for declarative history updates 212 - - Optional View Transition API integration for animated route changes 212 + - View Transition API integration for animated route changes 213 213 214 214 ### Inspector & Developer Tools 215 215
+1 -1
cli/README.md
··· 1 - # Volt CLI 1 + # Volt Developer CLI 2 2 3 3 Development tools for the Volt.js project. Uses: 4 4
+2 -2
cli/package.json
··· 1 1 { 2 - "name": "@volt/cli", 2 + "name": "@volt/dev", 3 3 "version": "0.1.0", 4 - "description": "CLI tools for Volt.js development", 4 + "description": "Local development CLI for Volt.js", 5 5 "type": "module", 6 6 "license": "MIT", 7 7 "bin": { "volt": "./dist/index.js" },
+6 -7
docs/.vitepress/config.ts
··· 1 1 import { defineConfig } from "vitepress"; 2 2 import { u } from "./utils"; 3 3 4 - // https://vitepress.dev/reference/site-config 4 + /** 5 + * @see https://vitepress.dev/reference/site-config 6 + */ 5 7 export default defineConfig({ 6 8 title: "Volt.js", 7 9 description: "A reactive, hypermedia framework.", 8 10 appearance: "dark", 9 11 themeConfig: { 10 - nav: [ 11 - { text: "Home", link: "/" }, 12 - { text: "Overview", link: "/overview" }, 13 - { text: "CSS", link: "/css/volt-css" }, 14 - { text: "API", link: "/api" }, 15 - ], 12 + nav: [{ text: "Home", link: "/" }, { text: "Overview", link: "/overview" }, { text: "CSS", link: "/css/volt-css" }], 16 13 sidebar: [ 17 14 { text: "Getting Started", items: [{ text: "Overview", link: "/overview" }] }, 15 + { text: "Concepts", items: [{ text: "Lifecycle", link: "/lifecycle" }] }, 16 + { text: "Expressions", items: [{ text: "Expressions", link: "/expressions" }] }, 18 17 { 19 18 text: "CSS", 20 19 collapsed: false,
+57
docs/expressions.md
··· 1 + # Expression Evaluation 2 + 3 + Volt.js evaluates JavaScript-like expressions in HTML templates using a sandboxed recursive descent parser. 4 + The evaluator is CSP-compliant and does not use `eval()` or `new Function()`. 5 + 6 + ## Supported Syntax 7 + 8 + The expression language supports a subset of JavaScript: 9 + 10 + - Standard literals (numbers, strings, booleans, null, undefined) 11 + - Arithmetic operators (`+`, `-`, `*`, `/`, `%`) 12 + - Comparison operators (`===`, `!==`, `<`, `>`, `<=`, `>=`) 13 + - Logical operators (`&&`, `||`, `!`) 14 + - Ternary operator (`? :`). 15 + 16 + Property access works via dot notation (`user.name`) or bracket notation (`items[0]`). 17 + Method calls are supported on any object, including chaining (`text.trim().toUpperCase()`). 18 + Arrow functions work with single-expression bodies for use in array methods like `filter`, `map`, and `reduce`. 19 + 20 + Array and object literals can be created inline, with spread operator support (`...`) for both arrays and objects. 21 + Signals are automatically unwrapped when referenced in expressions. 22 + 23 + ## Security Model 24 + 25 + The evaluator implements a balanced sandbox that blocks dangerous operations while attempting to preserve flexibility for most use cases. 26 + 27 + ### Blocked Access 28 + 29 + Three property names are unconditionally blocked to prevent prototype pollution: `__proto__`, `constructor`, and `prototype`. 30 + These restrictions apply to all access patterns including dot notation, bracket notation, and object literal keys. 31 + 32 + The following global names are blocked even if present in scope: 33 + `Function`, `eval`, `globalThis`, `window`, `global`, `process`, `require`, `import`, `module`, `exports`. 34 + 35 + ### Allowed Operations 36 + 37 + Standard constructors and utilities remain accessible: `Array`, `Object`, `String`, `Number`, `Boolean`, `Date`, `Math`, `JSON`, `RegExp`, `Map`, `Set`, `Promise`. 38 + 39 + All built-in methods on native types (strings, arrays, objects, etc.) are permitted. Signal methods (`get`, `set`, `subscribe`) are explicitly allowed even though `constructor` is otherwise blocked. 40 + 41 + ### Error Handling 42 + 43 + Expressions containing unsafe operations or syntax errors are caught, logged to the console, and return `undefined` rather than throwing. This prevents malicious or malformed expressions from breaking the application. 44 + 45 + ## Guidelines 46 + 47 + ### Performance 48 + 49 + Expressions are parsed on every evaluation. For optimal performance, keep expressions simple and use computed signals for complex calculations. 50 + The evaluator automatically tracks signal dependencies so only affected bindings re-evaluate when signals change. 51 + 52 + ### Best Practices 53 + 54 + - Use computed signals for logic that appears in multiple bindings or involves expensive operations. 55 + - Never use untrusted user input directly in expressions without validation. 56 + - Prefer simple, readable expressions in templates over complex nested operations. 57 + - Structure your scope data with consistent shapes (or consistent types) to avoid runtime errors.
+19 -105
docs/overview.md
··· 8 8 9 9 It combines HTML-driven behavior via `data-volt-*` attributes with signal-based reactivity. 10 10 11 - ## Architecture 12 - 13 - The framework consists of three layers: 14 - 15 - ### Reactivity Layer 16 - 17 - Signals are the foundation of Volt's reactive system: 18 - 19 - ```js 20 - import { signal, computed, effect } from 'volt'; 21 - 22 - const count = signal(0); 23 - const doubled = computed(() => count.get() * 2, [count]); 24 - 25 - effect(() => console.log('Count:', count.get()), [count]); 26 - ``` 27 - 28 - - **`signal()`** creates mutable reactive state 29 - - **`computed()`** derives values from signals 30 - - **`effect()`** runs side effects when dependencies change 31 - 32 - No reactivity scheduler. Signals notify subscribers directly on change. 33 - 34 - ### Binding System 35 - 36 - Bindings connect signals to DOM via `data-volt-*` attributes: 37 - 38 - ```html 39 - <div id="app"> 40 - <p data-volt-text="count">0</p> 41 - <button data-volt-on-click="increment">+</button> 42 - <div data-volt-if="isPositive">Positive</div> 43 - </div> 44 - ``` 45 - 46 - ```js 47 - mount(document.querySelector('#app'), { 48 - count, 49 - isPositive, 50 - increment: () => count.set(count.get() + 1) 51 - }); 52 - ``` 53 - 54 - Core bindings: 55 - 56 - - `data-volt-text` - Update text content 57 - - `data-volt-html` - Update HTML content 58 - - `data-volt-class` - Toggle CSS classes 59 - - `data-volt-on-*` - Attach event handlers 60 - - `data-volt-if` - Conditional rendering 61 - - `data-volt-for` - List rendering 62 - 63 - ### Plugin System 64 - 65 - Extend functionality via custom `data-volt-*` bindings: 66 - 67 - ```js 68 - import { registerPlugin } from 'volt'; 69 - 70 - registerPlugin('tooltip', (context, value) => { 71 - // Custom binding logic 72 - }); 73 - ``` 74 - 75 - See [Plugin Spec](./plugin-spec.md) for details. 76 - 77 - ## Key Concepts 78 - 79 - ### Signals Update DOM Directly 80 - 81 - Bindings subscribe to signals and update the real DOM when values change. 82 - 83 - ### HTML Drives Behavior 84 - 85 - Declare UI structure and interactivity in HTML. JavaScript provides state and handlers. 86 - 87 - ### Explicit Dependencies 88 - 89 - Computed signals and effects declare dependencies explicitly: 90 - 91 - ```js 92 - computed(() => a.get() + b.get(), [a, b]) // Both deps listed 93 - ``` 94 - 95 - ### Cleanup Management 96 - 97 - `mount()` returns a cleanup function. All bindings register their cleanup to prevent memory leaks: 98 - 99 - ```js 100 - const cleanup = mount(element, scope); 101 - // Later: 102 - cleanup(); // Unsubscribes all bindings 103 - ``` 11 + ## Features 104 12 105 - ## Examples 13 + ### Reactivity 106 14 107 - ### Counter 15 + - Signal-based state management with `signal`, `computed`, and `effect` primitives 16 + - Predicatable direct DOM updates without virtual DOM diffing 108 17 109 - Simple counter demonstrating basic reactivity: 18 + ### Hypermedia Integration 110 19 111 - - Location: `examples/counter/` 112 - - Shows: signals, computed values, conditional rendering, class bindings 20 + - Declarative HTTP requests with `data-volt-get`, `data-volt-post`, `data-volt-put`, `data-volt-patch`, and `data-volt-delete` 21 + - Multiple DOM swap strategies for server-rendered HTML fragments/partials 22 + - "Smart" retry with exponential backoff for failed requests 23 + - Automatic form serialization 113 24 114 - ### TodoMVC 25 + ### Plugins 115 26 116 - Complete todo app with filtering and editing: 27 + - Built-In 28 + - State persistence across page loads using `localStorage`, `sessionStorage`, or `IndexedDB` 29 + - Scroll management including position restore, scroll-to, scroll-spy, and smooth scrolling 30 + - URL synchronization for query parameters and hash-based routing 31 + - Extensibility 32 + - Custom plugin system via `registerPlugin` for domain-specific bindings 33 + - Global lifecycle hooks for mount, unmount, and binding creation 34 + - Automatic cleanup management 117 35 118 - - Location: `examples/todomvc/` 119 - - Shows: list rendering, event handling, computed filters, state mapping 120 - - Uses: Volt CSS (classless framework) for styling 121 - 122 - ## Design Constraints 36 + ### Design Constraints 123 37 124 38 - Core runtime under 15 KB gzipped 125 39 - Zero dependencies
+61
examples/TODO.md
··· 1 + # Example TODO 2 + 3 + Planned examples to demonstrate Volt.js features & show Volt.css 4 + 5 + All examples use declarative mode by default (data-volt-state, charge()). This ensures users can build functional apps without writing JavaScript. 6 + 7 + ## Example Set 8 + 9 + ### Counter 10 + 11 + Basic reactivity demonstration showing core primitives. 12 + 13 + - **Features**: data-volt-state, data-volt-computed, data-volt-text, data-volt-on-click, data-volt-class 14 + - **Shows**: Inline state declaration, computed values, event handlers that modify signals, conditional classes 15 + - **Structure**: Single index.html file with inline state 16 + 17 + ### Form Validation 18 + 19 + Real-world form handling with reactive validation. 20 + 21 + - **Features**: data-volt-state, data-volt-model, data-volt-if/else, data-volt-computed for validation, data-volt-bind:disabled 22 + - **Shows**: Two-way form binding, computed validation rules, conditional error messages, reactive button states 23 + - **Structure**: Single index.html with validation logic in computed expressions 24 + 25 + ### Persistent Settings 26 + 27 + Settings panel that survives page refresh. 28 + 29 + - **Features**: data-volt-state, data-volt-model, persist plugin with localStorage 30 + - **Shows**: Plugin usage, state persistence across page loads, settings form 31 + - **Structure**: Single index.html demonstrating data-volt-persist 32 + 33 + ### HTTP Todo List 34 + 35 + Full-featured todo app with server persistence and hypermedia. 36 + 37 + - **Features**: data-volt-get/patch/post/delete, data-volt-swap, data-volt-indicator, data-volt-retry, data-volt-for 38 + - **Shows**: Hypermedia approach, server communication, DOM swapping, loading states, error handling, smart retry, list rendering 39 + - **Structure**: index.html + minimal bootstrap script to fetch initial todos 40 + 41 + Made with a Go server with filesystem-based JSON persistence 42 + 43 + **Implementation**: 44 + 45 + - main.go with model, view & controller files 46 + - Filesystem-based storage (todos.json) 47 + - REST endpoints: 48 + - GET /todos - List all todos (returns HTML fragment) 49 + - POST /todos - Create new todo (returns HTML fragment) 50 + - PATCH /todos/:id - Update todo (partial - complete, edit text) 51 + - DELETE /todos/:id - Delete todo (returns empty or success fragment) 52 + - Returns HTML fragments for Volt's DOM swapping 53 + - REST API for demonstration 54 + 55 + **Storage Format**: 56 + 57 + ```json 58 + [ 59 + {"id": "1", "text": "Example todo", "completed": false} 60 + ] 61 + ```
+94 -7
lib/src/core/evaluator.ts
··· 1 1 /** 2 2 * Safe expression evaluation with operators support 3 3 * 4 - * Implements a recursive descent parser for expressions without using eval() 4 + * Implements a recursive descent parser for expressions without using eval(). 5 + * Includes sandboxing to prevent prototype pollution and sandbox escape attacks. 5 6 */ 6 7 7 8 import type { Dep, Scope } from "$types/volt"; 8 9 10 + /** 11 + * Blocked properties to prevent prototype pollution and sandbox escape 12 + */ 13 + const DANGEROUS_PROPERTIES = new Set(["__proto__", "prototype", "constructor"]); 14 + 15 + const SAFE_GLOBALS = new Set([ 16 + "Array", 17 + "Object", 18 + "String", 19 + "Number", 20 + "Boolean", 21 + "Date", 22 + "Math", 23 + "JSON", 24 + "RegExp", 25 + "Map", 26 + "Set", 27 + "Promise", 28 + ]); 29 + 30 + const DANGEROUS_GLOBALS = new Set([ 31 + "Function", 32 + "eval", 33 + "globalThis", 34 + "window", 35 + "global", 36 + "process", 37 + "require", 38 + "import", 39 + "module", 40 + "exports", 41 + ]); 42 + 43 + /** 44 + * Validates that a property name is safe to access 45 + */ 46 + function isSafeProp(key: unknown): boolean { 47 + if (typeof key !== "string" && typeof key !== "number") { 48 + return true; 49 + } 50 + 51 + const keyStr = String(key); 52 + return !DANGEROUS_PROPERTIES.has(keyStr); 53 + } 54 + 55 + /** 56 + * Validates that accessing a property on an object is safe 57 + */ 58 + function isSafeAccess(object: unknown, key: unknown): boolean { 59 + if (!isSafeProp(key)) { 60 + return false; 61 + } 62 + 63 + if (typeof object === "function") { 64 + const keyStr = String(key); 65 + if (keyStr === "constructor" && object.name && !SAFE_GLOBALS.has(object.name)) { 66 + return false; 67 + } 68 + } 69 + 70 + return true; 71 + } 72 + 9 73 type TokenType = 10 74 | "NUMBER" 11 75 | "STRING" ··· 42 106 | "OR_OR" 43 107 | "EOF"; 44 108 45 - /** 46 - * Token representing a lexical unit 47 - */ 48 109 type Token = { type: TokenType; value: unknown; start: number; end: number }; 49 110 50 - /** 51 - * Tokenize an expression string into a stream of tokens 52 - */ 53 111 function tokenize(expr: string): Token[] { 54 112 const tokens: Token[] = []; 55 113 let pos = 0; ··· 458 516 this.consume("RPAREN", "Expected ')' after arguments"); 459 517 460 518 if (typeof object === "function") { 519 + const func = object as { name?: string }; 520 + if (func.name === "Function" || func.name === "eval") { 521 + throw new Error("Cannot call dangerous function"); 522 + } 461 523 object = (object as (...args: unknown[]) => unknown)(...args); 462 524 } else { 463 525 throw new TypeError("Attempting to call a non-function value"); ··· 491 553 private callMethod(object: unknown, methodName: string, args: unknown[]): unknown { 492 554 if (object === null || object === undefined) { 493 555 throw new Error(`Cannot call method '${methodName}' on ${object}`); 556 + } 557 + 558 + if (!isSafeAccess(object, methodName)) { 559 + throw new Error(`Unsafe method call: ${methodName}`); 494 560 } 495 561 496 562 const method = (object as Record<string, unknown>)[methodName]; ··· 577 643 if (this.match("DOT_DOT_DOT")) { 578 644 const spreadValue = this.parseExpr(); 579 645 if (typeof spreadValue === "object" && spreadValue !== null && !Array.isArray(spreadValue)) { 646 + for (const key of Object.keys(spreadValue)) { 647 + if (!isSafeProp(key)) { 648 + throw new Error(`Unsafe property in spread: ${key}`); 649 + } 650 + } 580 651 Object.assign(object, spreadValue); 581 652 } else { 582 653 throw new Error("Spread operator can only be used with objects in object literals"); ··· 592 663 throw new Error("Expected property key in object literal"); 593 664 } 594 665 666 + if (!isSafeProp(key)) { 667 + throw new Error(`Unsafe property key in object literal: ${key}`); 668 + } 669 + 595 670 this.consume("COLON", "Expected ':' after property key"); 596 671 const value = this.parseExpr(); 597 672 object[key] = value; ··· 727 802 return undefined; 728 803 } 729 804 805 + if (!isSafeAccess(object, key)) { 806 + throw new Error(`Unsafe property access: ${String(key)}`); 807 + } 808 + 730 809 if (isSignal(object) && (key === "get" || key === "set" || key === "subscribe")) { 731 810 return (object as Record<string, unknown>)[key as string]; 732 811 } ··· 745 824 } 746 825 747 826 private resolvePropPath(path: string): unknown { 827 + if (!isSafeProp(path)) { 828 + throw new Error(`Unsafe property access: ${path}`); 829 + } 830 + 831 + if (DANGEROUS_GLOBALS.has(path)) { 832 + throw new Error(`Access to dangerous global: ${path}`); 833 + } 834 + 748 835 if (!(path in this.scope)) { 749 836 return undefined; 750 837 }
+3 -1
lib/src/index.ts
··· 20 20 export { clearPlugins, getRegisteredPlugins, hasPlugin, registerPlugin, unregisterPlugin } from "$core/plugin"; 21 21 export { computed, effect, signal } from "$core/signal"; 22 22 export { deserializeScope, hydrate, isHydrated, isServerRendered, serializeScope } from "$core/ssr"; 23 - export { persistPlugin, registerStorageAdapter, scrollPlugin, urlPlugin } from "$plugins"; 23 + export { persistPlugin, registerStorageAdapter } from "$plugins/persist"; 24 + export { scrollPlugin } from "$plugins/scroll"; 25 + export { urlPlugin } from "$plugins/url"; 24 26 export type { 25 27 AsyncEffectFunction, 26 28 AsyncEffectOptions,
+3 -1
lib/src/main.ts
··· 1 - import { persistPlugin, scrollPlugin, urlPlugin } from "$plugins"; 1 + import { persistPlugin } from "$plugins/persist"; 2 + import { scrollPlugin } from "$plugins/scroll"; 3 + import { urlPlugin } from "$plugins/url"; 2 4 import { computed, effect, mount, registerPlugin, signal } from "$volt"; 3 5 4 6 registerPlugin("persist", persistPlugin);
-9
lib/src/plugins/index.ts
··· 1 - /** 2 - * Built-in plugins for Volt.js 3 - * 4 - * All plugins require explicit registration via registerPlugin() 5 - */ 6 - 7 - export { persistPlugin, registerStorageAdapter } from "./persist"; 8 - export { scrollPlugin } from "./scroll"; 9 - export { urlPlugin } from "./url";
+2 -1
lib/src/plugins/persist.ts
··· 106 106 }, 107 107 } satisfies StorageAdapter; 108 108 109 + let dbPromise: Optional<Promise<IDBDatabase>>; 110 + 109 111 /** 110 112 * Open or create the IndexedDB database 111 113 */ 112 - let dbPromise: Optional<Promise<IDBDatabase>>; 113 114 function openDB(): Promise<IDBDatabase> { 114 115 if (dbPromise) return dbPromise; 115 116
+268
lib/test/core/evaluator-security.test.ts
··· 1 + import { evaluate } from "$core/evaluator"; 2 + import { signal } from "$core/signal"; 3 + import { describe, expect, it } from "vitest"; 4 + 5 + describe("evaluator security", () => { 6 + describe("prototype pollution prevention", () => { 7 + it("blocks __proto__ property access", () => { 8 + const scope = { obj: {} }; 9 + expect(evaluate("obj.__proto__", scope)).toBe(undefined); 10 + }); 11 + 12 + it("blocks __proto__ assignment attempts", () => { 13 + const scope = { obj: {} }; 14 + const result = evaluate("obj['__proto__']", scope); 15 + expect(result).toBe(undefined); 16 + }); 17 + 18 + it("blocks prototype property access", () => { 19 + const scope = { arr: [] }; 20 + expect(evaluate("arr.prototype", scope)).toBe(undefined); 21 + }); 22 + 23 + it("blocks constructor property on objects", () => { 24 + const scope = { obj: {} }; 25 + expect(evaluate("obj.constructor", scope)).toBe(undefined); 26 + }); 27 + 28 + it("blocks constructor property on arrays", () => { 29 + const scope = { arr: [1, 2, 3] }; 30 + expect(evaluate("arr.constructor", scope)).toBe(undefined); 31 + }); 32 + 33 + it("blocks constructor property on strings", () => { 34 + const scope = { text: "hello" }; 35 + expect(evaluate("text.constructor", scope)).toBe(undefined); 36 + }); 37 + 38 + it("blocks nested constructor access", () => { 39 + const scope = { obj: { nested: {} } }; 40 + expect(evaluate("obj.nested.constructor", scope)).toBe(undefined); 41 + }); 42 + 43 + it("blocks bracket notation __proto__ access", () => { 44 + const scope = { obj: {}, key: "__proto__" }; 45 + expect(evaluate("obj[key]", scope)).toBe(undefined); 46 + }); 47 + 48 + it("blocks bracket notation constructor access", () => { 49 + const scope = { obj: {}, key: "constructor" }; 50 + expect(evaluate("obj[key]", scope)).toBe(undefined); 51 + }); 52 + 53 + it("blocks bracket notation prototype access", () => { 54 + const scope = { obj: {}, key: "prototype" }; 55 + expect(evaluate("obj[key]", scope)).toBe(undefined); 56 + }); 57 + }); 58 + 59 + describe("sandbox escape prevention", () => { 60 + it("blocks direct constructor access from scope", () => { 61 + const scope = { constructor: function() {} }; 62 + expect(evaluate("constructor", scope)).toBe(undefined); 63 + }); 64 + 65 + it("blocks direct __proto__ access from scope", () => { 66 + const scope = { __proto__: {} }; 67 + expect(evaluate("__proto__", scope)).toBe(undefined); 68 + }); 69 + 70 + it("blocks direct prototype access from scope", () => { 71 + const scope = { prototype: {} }; 72 + expect(evaluate("prototype", scope)).toBe(undefined); 73 + }); 74 + 75 + it("prevents Function constructor access via constructor.constructor", () => { 76 + const scope = { fn: () => {} }; 77 + expect(evaluate("fn.constructor", scope)).toBe(undefined); 78 + }); 79 + 80 + it("prevents calling dangerous global functions", () => { 81 + const scope = { Function: globalThis.Function, eval: globalThis.eval }; 82 + expect(evaluate("Function", scope)).toBe(undefined); 83 + expect(evaluate("eval", scope)).toBe(undefined); 84 + }); 85 + }); 86 + 87 + describe("method call security", () => { 88 + it("blocks constructor method calls", () => { 89 + const scope = { obj: {} }; 90 + expect(evaluate("obj.constructor()", scope)).toBe(undefined); 91 + }); 92 + 93 + it("blocks __proto__ method calls", () => { 94 + const scope = { obj: {} }; 95 + expect(evaluate("obj.__proto__()", scope)).toBe(undefined); 96 + }); 97 + 98 + it("allows safe method calls", () => { 99 + const scope = { text: "hello" }; 100 + expect(evaluate("text.toUpperCase()", scope)).toBe("HELLO"); 101 + expect(evaluate("text.substring(0, 3)", scope)).toBe("hel"); 102 + }); 103 + 104 + it("allows safe array method calls", () => { 105 + const scope = { items: [1, 2, 3] }; 106 + expect(evaluate("items.slice(1)", scope)).toEqual([2, 3]); 107 + expect(evaluate("items.map(x => x * 2)", scope)).toEqual([2, 4, 6]); 108 + }); 109 + }); 110 + 111 + describe("object literal security", () => { 112 + it("allows creating safe object literals", () => { 113 + expect(evaluate("{ name: 'test', value: 42 }", {})).toEqual({ name: "test", value: 42 }); 114 + }); 115 + 116 + it("blocks dangerous keys in object literals", () => { 117 + expect(evaluate("{ __proto__: { polluted: true } }", {})).toBe(undefined); 118 + }); 119 + 120 + it("blocks constructor key in object literals", () => { 121 + expect(evaluate("{ constructor: 'bad' }", {})).toBe(undefined); 122 + }); 123 + 124 + it("blocks prototype key in object literals", () => { 125 + expect(evaluate("{ prototype: 'bad' }", {})).toBe(undefined); 126 + }); 127 + }); 128 + 129 + describe("array literal security", () => { 130 + it("allows creating safe array literals", () => { 131 + expect(evaluate("[1, 2, 3]", {})).toEqual([1, 2, 3]); 132 + }); 133 + 134 + it("allows spreading safe arrays", () => { 135 + const scope = { items: [1, 2, 3] }; 136 + expect(evaluate("[0, ...items, 4]", scope)).toEqual([0, 1, 2, 3, 4]); 137 + }); 138 + }); 139 + 140 + describe("arrow function security", () => { 141 + it("allows safe arrow functions", () => { 142 + const scope = { items: [1, 2, 3] }; 143 + expect(evaluate("items.map(x => x * 2)", scope)).toEqual([2, 4, 6]); 144 + }); 145 + 146 + it("blocks dangerous property access in arrow functions", () => { 147 + const scope = { items: [{}] }; 148 + expect(evaluate("items.map(x => x.__proto__)", scope)).toBe(undefined); 149 + }); 150 + 151 + it("blocks constructor access in arrow functions", () => { 152 + const scope = { items: [{}] }; 153 + expect(evaluate("items.map(x => x.constructor)", scope)).toBe(undefined); 154 + }); 155 + }); 156 + 157 + describe("signal security", () => { 158 + it("allows accessing signal values safely", () => { 159 + const scope = { count: signal(5) }; 160 + expect(evaluate("count", scope)).toBe(5); 161 + }); 162 + 163 + it("allows calling signal methods", () => { 164 + const count = signal(5); 165 + const scope = { count }; 166 + expect(evaluate("count.get()", scope)).toBe(5); 167 + }); 168 + 169 + it("blocks dangerous property access on signals", () => { 170 + const count = signal(5); 171 + const scope = { count }; 172 + expect(evaluate("count.constructor", scope)).toBe(undefined); 173 + }); 174 + }); 175 + 176 + describe("nested security", () => { 177 + it("blocks nested __proto__ access chains", () => { 178 + const scope = { a: { b: { c: {} } } }; 179 + expect(evaluate("a.b.c.__proto__", scope)).toBe(undefined); 180 + }); 181 + 182 + it("blocks mixed access patterns", () => { 183 + const scope = { obj: { arr: [{}] } }; 184 + expect(evaluate("obj.arr[0].__proto__", scope)).toBe(undefined); 185 + expect(evaluate("obj.arr[0].constructor", scope)).toBe(undefined); 186 + }); 187 + 188 + it("prevents prototype pollution via spread (enumerable properties only)", () => { 189 + const scope = { malicious: { constructor: "bad", normalProp: "ok" } }; 190 + expect(evaluate("{ ...malicious }", scope)).toBe(undefined); 191 + }); 192 + }); 193 + 194 + describe("real-world attack scenarios", () => { 195 + it("prevents prototype pollution via object assignment", () => { 196 + const scope = { target: {}, key: "__proto__", value: { polluted: true } }; 197 + expect(evaluate("target[key]", scope)).toBe(undefined); 198 + }); 199 + 200 + it("prevents constructor.constructor access pattern", () => { 201 + const scope = { fn: () => {} }; 202 + expect(evaluate("fn.constructor.constructor", scope)).toBe(undefined); 203 + }); 204 + 205 + it("prevents accessing Function by name even if in scope", () => { 206 + const scope = { Function: globalThis.Function, eval: globalThis.eval }; 207 + 208 + expect(evaluate("Function", scope)).toBe(undefined); 209 + expect(evaluate("eval", scope)).toBe(undefined); 210 + }); 211 + 212 + it("prevents eval through constructor chain", () => { 213 + const scope = { arr: [] }; 214 + expect(evaluate("arr.constructor.constructor", scope)).toBe(undefined); 215 + }); 216 + }); 217 + 218 + describe("legitimate use cases still work", () => { 219 + it("allows normal property access", () => { 220 + const scope = { user: { name: "Alice", age: 30 } }; 221 + expect(evaluate("user.name", scope)).toBe("Alice"); 222 + expect(evaluate("user.age", scope)).toBe(30); 223 + }); 224 + 225 + it("allows array operations", () => { 226 + const scope = { items: [1, 2, 3, 4, 5] }; 227 + expect(evaluate("items.length", scope)).toBe(5); 228 + expect(evaluate("items[0]", scope)).toBe(1); 229 + expect(evaluate("items.slice(1, 3)", scope)).toEqual([2, 3]); 230 + }); 231 + 232 + it("allows string operations", () => { 233 + const scope = { text: "hello world" }; 234 + expect(evaluate("text.length", scope)).toBe(11); 235 + expect(evaluate("text.toUpperCase()", scope)).toBe("HELLO WORLD"); 236 + expect(evaluate("text.substring(0, 5)", scope)).toBe("hello"); 237 + }); 238 + 239 + it("allows object creation", () => { 240 + expect(evaluate("{ active: true, count: 5 }", {})).toEqual({ active: true, count: 5 }); 241 + }); 242 + 243 + it("allows array creation", () => { 244 + expect(evaluate("[1, 2, 3]", {})).toEqual([1, 2, 3]); 245 + }); 246 + 247 + it("allows function composition", () => { 248 + const scope = { items: [1, 2, 3, 4, 5] }; 249 + expect(evaluate("items.filter(x => x > 2).map(x => x * 2)", scope)).toEqual([6, 8, 10]); 250 + }); 251 + 252 + it("allows complex expressions", () => { 253 + const scope = { count: 5, limit: 10, items: [1, 2, 3] }; 254 + expect(evaluate("count < limit && items.length > 0", scope)).toBe(true); 255 + }); 256 + 257 + it("allows ternary with safe operations", () => { 258 + const scope = { count: 5 }; 259 + expect(evaluate("count > 0 ? 'positive' : 'zero'", scope)).toBe("positive"); 260 + }); 261 + 262 + it("allows signals in complex expressions", () => { 263 + const scope = { count: signal(5), limit: 10 }; 264 + expect(evaluate("count * 2 > limit", scope)).toBe(false); 265 + expect(evaluate("count * 3 > limit", scope)).toBe(true); 266 + }); 267 + }); 268 + });