diff --git a/dashboard/frontend/package.json b/dashboard/frontend/package.json index 842e49d3a..1ba35b468 100644 --- a/dashboard/frontend/package.json +++ b/dashboard/frontend/package.json @@ -46,6 +46,7 @@ "jose": "^6.1.3", "minimatch": "^10.1.1", "multiformats": "^13.4.2", + "quickjs-emscripten": "^0.31.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.69.0", diff --git a/dashboard/frontend/src/helpers.ts b/dashboard/frontend/src/helpers.ts index 2b3da76e1..88cf869dc 100644 --- a/dashboard/frontend/src/helpers.ts +++ b/dashboard/frontend/src/helpers.ts @@ -26,3 +26,23 @@ export const DEFAULT_ENDPOINT = BuildURI.from("fireproof://cloud.fireproof.direc export const SYNC_DB_NAME = "fp_sync"; export const DASHAPI_URL = import.meta.env.VITE_DASHAPI_URL ?? "/api"; + +/** + * Default timeout for query execution in milliseconds. + * Configurable via VITE_QUERY_TIMEOUT environment variable. + * Set to 0 to disable timeouts. + * @default 30000 (30 seconds) + */ +/** Default timeout when no override is provided. */ +const DEFAULT_QUERY_TIMEOUT_MS = 30000; +/** Raw VITE_QUERY_TIMEOUT value, if provided. */ +const QUERY_TIMEOUT_ENV = import.meta.env.VITE_QUERY_TIMEOUT as string | undefined; +/** Parsed timeout override, NaN if invalid. */ +const PARSED_QUERY_TIMEOUT = QUERY_TIMEOUT_ENV !== undefined ? parseInt(QUERY_TIMEOUT_ENV, 10) : Number.NaN; + +export const QUERY_TIMEOUT_MS = + QUERY_TIMEOUT_ENV === undefined || QUERY_TIMEOUT_ENV === "" + ? DEFAULT_QUERY_TIMEOUT_MS + : Number.isFinite(PARSED_QUERY_TIMEOUT) && PARSED_QUERY_TIMEOUT >= 0 + ? PARSED_QUERY_TIMEOUT + : DEFAULT_QUERY_TIMEOUT_MS; diff --git a/dashboard/frontend/src/pages/databases/query.tsx b/dashboard/frontend/src/pages/databases/query.tsx index 3a9271d6c..19a096601 100644 --- a/dashboard/frontend/src/pages/databases/query.tsx +++ b/dashboard/frontend/src/pages/databases/query.tsx @@ -1,13 +1,28 @@ -import React, { useState } from "react"; +/** + * Query editor page for Fireproof databases. + * Allows users to write and execute map functions against database documents. + * + * Security: User code executes in a WASM sandbox (quickjs-emscripten) + * to prevent code injection attacks. + */ +import React, { useState, useEffect, useMemo } from "react"; import { Link, useParams } from "react-router-dom"; -// import { MapFn } from "@fireproof/core"; import { useFireproof } from "use-fireproof"; import { EditableCodeHighlight } from "../../components/CodeHighlight.jsx"; import { DynamicTable, TableRow } from "../../components/DynamicTable.jsx"; import { headersForDocs } from "../../components/dynamicTableHelpers.js"; +import { + initSandbox, + validateCode, + executeMapFn, + SandboxDocument, + MapResult, +} from "../../services/sandbox-service.js"; -// type AnyMapFn = MapFn; - +/** + * Main query editor component. + * Provides a code editor for map functions and displays query results. + */ export function DatabasesQuery() { const { name } = useParams(); if (!name) throw new Error("No database name provided"); @@ -17,26 +32,74 @@ export function DatabasesQuery() { const [editorCode, setEditorCode] = useState(emptyMap); const [editorCodeFnString, setEditorCodeFnString] = useState(() => editorCode); const [userCodeError, setUserCodeError] = useState(null); + const [sandboxReady, setSandboxReady] = useState(null); + + // Initialize sandbox on mount + useEffect(() => { + initSandbox().then(setSandboxReady); + }, []); + /** + * Handle code editor changes. + * @param code - Updated code from the editor + */ function editorChanged({ code }: { code: string }) { setEditorCode(code); } + /** + * Validate and run the query. + * Uses sandbox validation instead of eval() for security. + */ async function runTempQuery() { - try { - // Try to evaluate the function to check for errors - eval(`(${editorCode})`); - setEditorCodeFnString(editorCode); - setUserCodeError(null); - } catch (error) { - setUserCodeError((error as Error).message); + // Check if sandbox is available + if (!sandboxReady) { + setUserCodeError("Query sandbox is not available. Please refresh the page."); + return; + } + + // Validate code using sandbox + const validation = validateCode(editorCode); + if (!validation.valid) { + setUserCodeError(validation.error || "Invalid code"); + return; } + + setEditorCodeFnString(editorCode); + setUserCodeError(null); } + /** + * Save the current query (not implemented). + */ function saveTempQuery() { console.log("save not implemented"); } + // Show loading state while sandbox initializes + if (sandboxReady === null) { + return ( +
+
Initializing query sandbox...
+
+ ); + } + + // Show error if sandbox failed to load + if (sandboxReady === false) { + return ( +
+
+

Query Editor Unavailable

+

+ The query sandbox failed to initialize. This feature requires WebAssembly support. Please try refreshing the + page or use a different browser. +

+
+
+ ); + } + return (
@@ -84,12 +147,83 @@ export function DatabasesQuery() { ); } -function QueryDynamicTable({ mapFn, name }: { mapFn: string; name: string }) { +/** + * Props for the QueryDynamicTable component. + */ +interface QueryDynamicTableProps { + /** Map function code as a string */ + mapFn: string; + /** Database name to query */ + name: string; +} + +/** + * Component that executes the map function and displays results. + * Uses the sandbox for safe code execution. + * + * @param props - Component props + */ +function QueryDynamicTable({ mapFn, name }: QueryDynamicTableProps) { const { useLiveQuery } = useFireproof(name); - const allDocs = useLiveQuery(eval(`(${mapFn})`)); - const docs = allDocs.docs.filter((doc) => doc); - console.log(docs); - const headers = headersForDocs(docs); + const [queryResults, setQueryResults] = useState([]); + const [queryError, setQueryError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Fetch all documents from the database + const allDocsResult = useLiveQuery("_id"); + const allDocs = useMemo(() => allDocsResult.docs.filter((doc) => doc) as SandboxDocument[], [allDocsResult.docs]); + + // Execute map function in sandbox when docs or mapFn change + useEffect(() => { + let cancelled = false; + + if (allDocs.length === 0) { + setQueryResults([]); + setIsLoading(false); + return () => { + cancelled = true; + }; + } + + setIsLoading(true); + setQueryError(null); + + executeMapFn(mapFn, allDocs) + .then((results: MapResult[]) => { + if (cancelled) return; + // Extract documents from results + const docs = results + .map((r: MapResult) => r.value) + .filter((v: unknown): v is SandboxDocument => v !== null && typeof v === "object"); + setQueryResults(docs); + setIsLoading(false); + }) + .catch((error: Error) => { + if (cancelled) return; + setQueryError(error.message); + setQueryResults([]); + setIsLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [mapFn, allDocs]); + + if (isLoading) { + return
Executing query...
; + } + + if (queryError) { + return ( +
+

Query Error:

+

{queryError}

+
+ ); + } + + const headers = headersForDocs(queryResults); - return ; + return ; } diff --git a/dashboard/frontend/src/services/sandbox-service.ts b/dashboard/frontend/src/services/sandbox-service.ts new file mode 100644 index 000000000..a6df3fa30 --- /dev/null +++ b/dashboard/frontend/src/services/sandbox-service.ts @@ -0,0 +1,299 @@ +/** + * Sandbox service for safe query execution. + * Uses quickjs-emscripten WASM to isolate user code from the host environment. + * + * Security model: + * - User code executes in isolated WASM context + * - No access to DOM, network, or filesystem + * - Only read-only document array exposed + * - Execution timeout enforced + * - Dangerous patterns blocked before execution + */ +import { getQuickJS, QuickJSContext, QuickJSHandle } from "quickjs-emscripten"; +import { QUERY_TIMEOUT_MS } from "../helpers.js"; + +/** + * Result of code validation. + */ +export interface ValidationResult { + /** Whether the code passed validation. */ + valid: boolean; + /** Error message if validation failed. */ + error?: string; +} + +/** + * Document type for sandbox execution. + * Represents a Fireproof document with ID. + */ +export interface SandboxDocument { + _id: string; + [key: string]: unknown; +} + +/** + * Result of map function execution in sandbox. + */ +export interface MapResult { + /** Key emitted by the map function. */ + key: unknown; + /** Value emitted by the map function. */ + value: unknown; +} + +/** + * Patterns that are blocked before code reaches the sandbox. + * These patterns indicate dangerous operations that should never execute. + * This is best-effort validation; the sandbox is the primary security boundary. + */ +const BLOCKED_PATTERNS: { pattern: RegExp; description: string }[] = [ + { pattern: /\bfetch\s*\(/, description: "fetch() calls are not allowed" }, + { pattern: /\bXMLHttpRequest\b/, description: "XMLHttpRequest is not allowed" }, + { pattern: /\bimport\s*\(/, description: "Dynamic imports are not allowed" }, + { pattern: /\brequire\s*\(/, description: "require() calls are not allowed" }, + { pattern: /\beval\s*\(/, description: "eval() is not allowed" }, + { pattern: /\bFunction\s*\(/, description: "Function constructor is not allowed" }, + { pattern: /\.constructor\s*\(/, description: "Constructor access is not allowed" }, + { pattern: /\bwindow\b/, description: "window object access is not allowed" }, + { pattern: /\bdocument\b/, description: "document object access is not allowed" }, + { pattern: /\blocalStorage\b/, description: "localStorage access is not allowed" }, + { pattern: /\bsessionStorage\b/, description: "sessionStorage access is not allowed" }, + { pattern: /\bglobalThis\b/, description: "globalThis access is not allowed" }, +]; + +/** Singleton QuickJS instance. */ +let quickJSInstance: Awaited> | null = null; + +/** Flag indicating if sandbox is available. */ +let sandboxAvailable: boolean | null = null; + +/** + * Initialize the QuickJS WASM sandbox. + * Must be called before using other sandbox functions. + * Safe to call multiple times - will return cached result. + * + * @returns Promise resolving to true if sandbox is available, false otherwise + */ +export async function initSandbox(): Promise { + if (sandboxAvailable !== null) { + return sandboxAvailable; + } + + try { + quickJSInstance = await getQuickJS(); + sandboxAvailable = true; + return true; + } catch (error) { + console.error("Failed to initialize QuickJS sandbox:", error); + sandboxAvailable = false; + return false; + } +} + +/** + * Check if the sandbox is available. + * Returns null if initSandbox() hasn't been called yet. + * + * @returns true if available, false if unavailable, null if not yet initialized + */ +export function isSandboxAvailable(): boolean | null { + return sandboxAvailable; +} + +/** + * Validate code for dangerous patterns before execution. + * Performs static analysis to block obviously malicious code. + * + * @param code - The JavaScript code to validate + * @returns Validation result with error message if invalid + */ +export function validateCode(code: string): ValidationResult { + // Check for blocked patterns + for (const { pattern, description } of BLOCKED_PATTERNS) { + if (pattern.test(code)) { + return { valid: false, error: description }; + } + } + + // Try to parse as a function expression + try { + // Use Function constructor just for syntax validation + // This is safe because we're not executing the result + new Function(`return (${code})`); + return { valid: true }; + } catch (error) { + return { + valid: false, + error: `Syntax error: ${(error as Error).message}`, + }; + } +} + +/** + * Execute a map function in the sandbox with the provided documents. + * The map function receives (doc, emit) and should call emit(key, value). + * + * @param code - The map function code as a string + * @param docs - Array of documents to process + * @param timeoutMs - Execution timeout in milliseconds (default from config) + * @returns Promise resolving to array of emitted results + * @throws Error if sandbox unavailable, code invalid, or execution fails + */ +export async function executeMapFn( + code: string, + docs: SandboxDocument[], + timeoutMs: number = QUERY_TIMEOUT_MS +): Promise { + // Ensure sandbox is initialized + if (sandboxAvailable === null) { + await initSandbox(); + } + + if (!sandboxAvailable || !quickJSInstance) { + throw new Error("Query sandbox is unavailable. Please refresh the page."); + } + + // Validate code first + const validation = validateCode(code); + if (!validation.valid) { + throw new Error(validation.error); + } + + // Create a new context for this execution + const vm = quickJSInstance.newContext(); + + try { + // Collect emitted results + const results: MapResult[] = []; + + // Create emit function that collects results + const emitHandle = vm.newFunction("emit", (keyHandle, valueHandle) => { + const key = vm.dump(keyHandle); + const value = vm.dump(valueHandle); + results.push({ key, value }); + }); + + // Set up the execution environment + vm.setProp(vm.global, "emit", emitHandle); + emitHandle.dispose(); + + // Serialize documents to JSON for the sandbox + const docsJson = JSON.stringify(docs); + + // Create the execution code + const executionCode = ` + (function() { + const mapFn = (${code}); + const docs = ${docsJson}; + const emit = globalThis.emit; + + for (const doc of docs) { + mapFn(doc, emit); + } + + return true; + })() + `; + + // Execute with timeout + const result = await executeWithTimeout(vm, executionCode, timeoutMs); + + if (result.error) { + const errorMessage = vm.dump(result.error); + result.error.dispose(); + throw new Error(`Query execution error: ${JSON.stringify(errorMessage)}`); + } + + result.value?.dispose(); + + return results; + } finally { + vm.dispose(); + } +} + +/** + * Execute code in the QuickJS context with a timeout. + * + * @param vm - QuickJS context + * @param code - Code to execute + * @param timeoutMs - Timeout in milliseconds + * @returns Execution result + */ +async function executeWithTimeout( + vm: QuickJSContext, + code: string, + timeoutMs: number +): Promise<{ value?: QuickJSHandle; error?: QuickJSHandle }> { + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { + return vm.evalCode(code); + } + + const deadline = Date.now() + timeoutMs; + let timedOut = false; + const runtime = vm.runtime; + + runtime.setInterruptHandler(() => { + if (Date.now() > deadline) { + timedOut = true; + return true; + } + return false; + }); + + try { + const result = vm.evalCode(code); + if (timedOut) { + result.error?.dispose(); + result.value?.dispose(); + throw new Error(`Query execution timed out after ${timeoutMs}ms`); + } + return result; + } catch (error) { + if (timedOut) { + throw new Error(`Query execution timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + runtime.removeInterruptHandler(); + } +} + +/** + * Filter documents using a map function in the sandbox. + * Convenience wrapper that returns documents that emit any value. + * + * @param code - The map function code as a string + * @param docs - Array of documents to filter + * @param timeoutMs - Execution timeout in milliseconds + * @returns Promise resolving to filtered documents with their emitted values + */ +export async function filterDocsWithMapFn( + code: string, + docs: SandboxDocument[], + timeoutMs: number = QUERY_TIMEOUT_MS +): Promise<{ doc: SandboxDocument; key: unknown; value: unknown }[]> { + const results = await executeMapFn(code, docs, timeoutMs); + + // Create a map from doc _id to emit results + const docResultMap = new Map(); + for (const result of results) { + // The value should be the document or contain _id + const doc = result.value as SandboxDocument; + if (doc && typeof doc === "object" && "_id" in doc) { + docResultMap.set(doc._id, { key: result.key, value: result.value }); + } + } + + // Return documents that were emitted + return docs + .filter((doc) => docResultMap.has(doc._id)) + .map((doc) => { + const result = docResultMap.get(doc._id); + return { + doc, + key: result?.key, + value: result?.value, + }; + }); +} diff --git a/dashboard/frontend/tests/sandbox.test.ts b/dashboard/frontend/tests/sandbox.test.ts new file mode 100644 index 000000000..5c7cc9540 --- /dev/null +++ b/dashboard/frontend/tests/sandbox.test.ts @@ -0,0 +1,204 @@ +/** + * Security test suite for the query sandbox. + * Tests that malicious patterns are blocked and the sandbox provides proper isolation. + */ +import { describe, it, expect } from "vitest"; +import { + initSandbox, + isSandboxAvailable, + validateCode, + executeMapFn, + SandboxDocument, +} from "../src/services/sandbox-service.js"; + +/** Whether the WASM sandbox is available in the test environment. */ +const sandboxReady = await initSandbox(); +/** Test helper that skips when the sandbox isn't available. */ +const runIfSandbox = sandboxReady ? it : it.skip; + +describe("Sandbox Security", () => { + describe("validateCode - Blocked Patterns", () => { + /** Verifies that fetch() calls are blocked. */ + it("blocks fetch() calls", () => { + const result = validateCode('(doc, emit) => { fetch("http://evil.com"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("fetch"); + }); + + /** Verifies that XMLHttpRequest is blocked. */ + it("blocks XMLHttpRequest", () => { + const result = validateCode("(doc, emit) => { new XMLHttpRequest(); emit(doc._id, doc); }"); + expect(result.valid).toBe(false); + expect(result.error).toContain("XMLHttpRequest"); + }); + + /** Verifies that dynamic imports are blocked. */ + it("blocks dynamic imports", () => { + const result = validateCode('(doc, emit) => { import("evil-module"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("import"); + }); + + /** Verifies that require() calls are blocked. */ + it("blocks require() calls", () => { + const result = validateCode('(doc, emit) => { require("fs"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("require"); + }); + + /** Verifies that eval() in user code is blocked. */ + it("blocks eval() in user code", () => { + const result = validateCode('(doc, emit) => { eval("malicious"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("eval"); + }); + + /** Verifies that Function constructor is blocked. */ + it("blocks Function constructor", () => { + const result = validateCode('(doc, emit) => { new Function("return this")(); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("Function"); + }); + + /** Verifies that constructor chain access is blocked. */ + it("blocks constructor chain escapes", () => { + const result = validateCode('(doc, emit) => { doc.constructor("malicious")(); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("Constructor"); + }); + + /** Verifies that window object access is blocked. */ + it("blocks window object access", () => { + const result = validateCode("(doc, emit) => { window.location = 'http://evil.com'; emit(doc._id, doc); }"); + expect(result.valid).toBe(false); + expect(result.error).toContain("window"); + }); + + /** Verifies that document object access is blocked. */ + it("blocks document object access", () => { + const result = validateCode("(doc, emit) => { document.cookie; emit(doc._id, doc); }"); + expect(result.valid).toBe(false); + expect(result.error).toContain("document"); + }); + + /** Verifies that localStorage access is blocked. */ + it("blocks localStorage access", () => { + const result = validateCode('(doc, emit) => { localStorage.getItem("key"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("localStorage"); + }); + + /** Verifies that sessionStorage access is blocked. */ + it("blocks sessionStorage access", () => { + const result = validateCode('(doc, emit) => { sessionStorage.getItem("key"); emit(doc._id, doc); }'); + expect(result.valid).toBe(false); + expect(result.error).toContain("sessionStorage"); + }); + + /** Verifies that globalThis access is blocked. */ + it("blocks globalThis access", () => { + const result = validateCode("(doc, emit) => { globalThis.fetch; emit(doc._id, doc); }"); + expect(result.valid).toBe(false); + expect(result.error).toContain("globalThis"); + }); + }); + + describe("validateCode - Valid Patterns", () => { + /** Verifies that valid map functions pass validation. */ + it("allows valid map functions", () => { + const result = validateCode("(doc, emit) => { emit(doc._id, doc); }"); + expect(result.valid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + /** Verifies that functions with conditionals pass validation. */ + it("allows functions with conditionals", () => { + const result = validateCode('(doc, emit) => { if (doc.type === "user") emit(doc._id, doc); }'); + expect(result.valid).toBe(true); + }); + + /** Verifies that functions with loops pass validation. */ + it("allows functions with loops", () => { + const result = validateCode("(doc, emit) => { for (const key of Object.keys(doc)) emit(key, doc); }"); + expect(result.valid).toBe(true); + }); + + /** Verifies that functions with array methods pass validation. */ + it("allows functions with array methods", () => { + const result = validateCode("(doc, emit) => { doc.tags?.forEach(tag => emit(tag, doc)); }"); + expect(result.valid).toBe(true); + }); + }); + + describe("validateCode - Syntax Errors", () => { + /** Verifies that syntax errors are caught. */ + it("catches syntax errors", () => { + const result = validateCode("(doc, emit) => { emit(doc._id, doc)"); + expect(result.valid).toBe(false); + expect(result.error).toContain("Syntax error"); + }); + + /** Verifies that invalid JavaScript is caught. */ + it("catches invalid JavaScript", () => { + const result = validateCode("not valid javascript at all"); + expect(result.valid).toBe(false); + }); + }); + + describe("executeMapFn - Sandbox Execution", () => { + const testDocs: SandboxDocument[] = [ + { _id: "doc1", name: "Alice", type: "user" }, + { _id: "doc2", name: "Bob", type: "user" }, + { _id: "doc3", name: "Post 1", type: "post" }, + ]; + + /** Verifies that valid map functions execute correctly. */ + runIfSandbox("executes valid map functions", async () => { + const results = await executeMapFn("(doc, emit) => { emit(doc._id, doc); }", testDocs); + expect(results).toHaveLength(3); + expect(results[0].key).toBe("doc1"); + }); + + /** Verifies that map functions can filter documents. */ + runIfSandbox("filters documents based on map function", async () => { + const results = await executeMapFn( + '(doc, emit) => { if (doc.type === "user") emit(doc._id, doc); }', + testDocs + ); + expect(results).toHaveLength(2); + }); + + /** Verifies that blocked patterns are rejected during execution. */ + runIfSandbox("rejects blocked patterns during execution", async () => { + await expect(executeMapFn('(doc, emit) => { fetch("http://evil.com"); }', testDocs)).rejects.toThrow( + "fetch() calls are not allowed" + ); + }); + + /** Verifies that syntax errors are caught during execution. */ + runIfSandbox("rejects syntax errors during execution", async () => { + await expect(executeMapFn("(doc, emit) => { emit(doc._id, doc)", testDocs)).rejects.toThrow("Syntax error"); + }); + }); + + describe("executeMapFn - Timeout Protection", () => { + /** Verifies that infinite loops are terminated by timeout. */ + runIfSandbox("enforces execution timeout", async () => { + const testDocs: SandboxDocument[] = [{ _id: "doc1", name: "Test" }]; + + // This should timeout - use a very short timeout for testing + await expect( + executeMapFn("(doc, emit) => { while(true) {} }", testDocs, 100) // 100ms timeout + ).rejects.toThrow(/timed out/i); + }, 5000); // Test timeout of 5 seconds + }); + + describe("Sandbox Availability", () => { + /** Verifies that sandbox availability can be checked. */ + it("reports sandbox availability", () => { + const available = isSandboxAvailable(); + // Should be either true, false, or null (if not initialized) + expect([true, false, null]).toContain(available); + }); + }); +}); diff --git a/dashboard/frontend/vite.config.ts b/dashboard/frontend/vite.config.ts index f5bdba56a..c97fc0cd5 100644 --- a/dashboard/frontend/vite.config.ts +++ b/dashboard/frontend/vite.config.ts @@ -45,11 +45,9 @@ export default defineConfig({ server: { port: 7370, hmr: false, - proxy: { - "/*": { - rewrite: () => "/index.html", - }, - }, + }, + optimizeDeps: { + exclude: ["quickjs-emscripten"], }, resolve: process.env.USE_SOURCE ? { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f342fb4a..14dfbb674 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1343,6 +1343,9 @@ importers: multiformats: specifier: ^13.4.2 version: 13.4.2 + quickjs-emscripten: + specifier: ^0.31.0 + version: 0.31.0 react: specifier: ^19.2.3 version: 19.2.3 @@ -3069,6 +3072,21 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jitl/quickjs-ffi-types@0.31.0': + resolution: {integrity: sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw==} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.31.0': + resolution: {integrity: sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w==} + + '@jitl/quickjs-wasmfile-debug-sync@0.31.0': + resolution: {integrity: sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw==} + + '@jitl/quickjs-wasmfile-release-asyncify@0.31.0': + resolution: {integrity: sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg==} + + '@jitl/quickjs-wasmfile-release-sync@0.31.0': + resolution: {integrity: sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -6612,6 +6630,13 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} + quickjs-emscripten-core@0.31.0: + resolution: {integrity: sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ==} + + quickjs-emscripten@0.31.0: + resolution: {integrity: sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ==} + engines: {node: '>=16.0.0'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -9030,6 +9055,24 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@jitl/quickjs-ffi-types@0.31.0': {} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.31.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.31.0 + + '@jitl/quickjs-wasmfile-debug-sync@0.31.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.31.0 + + '@jitl/quickjs-wasmfile-release-asyncify@0.31.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.31.0 + + '@jitl/quickjs-wasmfile-release-sync@0.31.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.31.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10175,20 +10218,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser-playwright@4.0.14(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14)': - dependencies: - '@vitest/browser': 4.0.14(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14) - '@vitest/mocker': 4.0.14(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - playwright: 1.57.0 - tinyrainbow: 3.0.3 - vitest: 4.0.14(@types/node@25.0.3)(@vitest/browser-playwright@4.0.14)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - optional: true - '@vitest/browser-playwright@4.0.14(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14)': dependencies: '@vitest/browser': 4.0.14(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14) @@ -10202,24 +10231,6 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.0.14(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14)': - dependencies: - '@vitest/mocker': 4.0.14(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/utils': 4.0.14 - magic-string: 0.30.21 - pixelmatch: 7.1.0 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.0.3 - vitest: 4.0.14(@types/node@25.0.3)(@vitest/browser-playwright@4.0.14)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - optional: true - '@vitest/browser@4.0.14(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14)': dependencies: '@vitest/mocker': 4.0.14(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -13088,6 +13099,18 @@ snapshots: quick-lru@4.0.1: {} + quickjs-emscripten-core@0.31.0: + dependencies: + '@jitl/quickjs-ffi-types': 0.31.0 + + quickjs-emscripten@0.31.0: + dependencies: + '@jitl/quickjs-wasmfile-debug-asyncify': 0.31.0 + '@jitl/quickjs-wasmfile-debug-sync': 0.31.0 + '@jitl/quickjs-wasmfile-release-asyncify': 0.31.0 + '@jitl/quickjs-wasmfile-release-sync': 0.31.0 + quickjs-emscripten-core: 0.31.0 + range-parser@1.2.1: {} react-devtools-core@6.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10): @@ -14092,7 +14115,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.3 - '@vitest/browser-playwright': 4.0.14(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.2.6(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14) + '@vitest/browser-playwright': 4.0.14(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.14) transitivePeerDependencies: - jiti - less