@sebastianwessel/quickjs

repository·main·Indexed 21 days ago

https://github.com/sebastianwessel/quickjs

A TypeScript package that provides a secure WebAssembly-based sandbox for executing JavaScript and TypeScript code using the QuickJS engine. It features an isolated environment for untrusted code, support for virtual file systems, custom Node modules, a fetch client with security whitelisting and rate limiting, and a built-in test runner with chai-based expect support.

Tokens
41.1K
Snippets
130
Records
178
Agent score
74%

What's inside @sebastianwessel/quickjs

  1. Overview of Node.js module compatibility

    main

    The library provides basic support for common Node.js modules to enable running Node-centric code within the WebAssembly QuickJS sandbox. Note that the goal is not 100% compatibility; many modules (like http, crypto, child_process, and net) are not supported. Always verify if the specific module and methods you require are available before deployment.

    Supported Modules:

    • assert
    • buffer
    • events
    • fs (via memfs)
    • module
    • path
    • process
    • punycode
    • querystring
    • string_decoder
    • timers / timers/promises
    • url
    • util
  2. QuickJS Overview and Features

    main

    QuickJS is a TypeScript package that enables the secure execution of JavaScript and TypeScript code within a WebAssembly-based sandbox using the QuickJS engine. It is designed for isolating and running untrusted code safely.

    Key Capabilities:

    • Security: Isolated environment for untrusted code.
    • Node.js Support: Provides basic standard Node.js module support.
    • File System: Supports mounting a virtual file system.
    • Custom Modules: Ability to mount custom Node modules.
    • Networking: Includes a fetch client for making HTTP(S) calls.
    • Testing: Includes a built-in test runner with chai-based expect support.
    • Integration: Designed for easy integration into existing TypeScript projects.
  3. What is a Path Normalizer in QuickJS?

    main

    In the QuickJS sandboxed environment, a Path Normalizer is a mechanism used to resolve module imports. Because the sandbox lacks direct file system access and modules can originate from diverse sources (local virtual files, remote URLs, or Node.js built-ins), QuickJS does not automatically resolve paths like Node.js does.

    The Path Normalizer is responsible for mapping a requestedName (the path string found in an import statement) to its actual source location. This ensures that dependencies are correctly loaded regardless of their origin.

  4. Run asynchronous tests and hooks

    main

    TestRunner supports asynchronous operations. You can use async/await or return a Promise within your it test blocks or your hook functions (beforeAll, afterEach, etc.).

    describe('Asynchronous Test Suite', () => {
      beforeAll(async () => {
        // Async beforeAll hook
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log('Running async beforeAll hook');
      });
    
      it('should pass this async test', async () => {
        const result = await new Promise(resolve => setTimeout(() => resolve(true), 1000));
        expect(result).to.be.true;
      });
    });
  5. Customize Module Loading

    main

    You can provide custom logic for resolving and loading modules. The available options depend on whether you are using a synchronous or asynchronous sandbox.

    ### Synchronous Module Options (`SandboxOptions`)
    - `getModuleLoader`: `(fs: IFs, options: RuntimeOptions) => JSModuleLoader` 
    - `modulePathNormalizer`: `JSModuleNormalizer` 
    
    ### Asynchronous Module Options (`SandboxAsyncOptions`)
    - `getModuleLoader`: `(fs: IFs, options: RuntimeOptions) => JSModuleLoaderAsync` 
    - `modulePathNormalizer`: `JSModuleNormalizerAsync`
  6. Understand asynchronous behavior and pending jobs

    main

    The QuickJS WebAssembly runtime does not have its own internal event loop. Instead, the host system is responsible for driving the loop for any provided promises.

    How it works:

    1. This library automatically starts a host-side interval that calls executePendingJobs in QuickJS to process tasks.
    2. When the guest code reaches a promise provided by the host, the QuickJS runtime pauses execution if the promise is unsettled.
    3. Once the promise settles, the host must call executePendingJobs to instruct QuickJS to resume execution.
  7. How the QuickJS virtual file system works

    main

    Every QuickJS sandbox includes a virtual file system based on memfs. This file system is used to host node_modules and custom files required by the scripts running in the sandbox.

    By default, any code passed to the evalCode function is treated as being located at src/index.js. Consequently, any relative file paths used within your script will be resolved relative to the src/ directory.

  8. Exchange non-primitive data types (Classes and Functions)

    main

    While possible, exchanging non-primitives like Classes and Functions is complex because data is copied, not shared.

    • Classes: A class must exist in both the host and the guest. Because instances are copied, the host and guest will have separate instances; you must manually keep their states in sync.
    • Functions: When the host provides a function to the guest, the function is executed within the host's context. The result is then passed back to the guest. Conversely, the host can trigger guest logic by calling the evalCode method.
  9. Reuse a sandboxed context in QuickJS v2

    main

    In QuickJS v2, you can execute multiple pieces of code within the same sandboxed context by using the runSandboxed function. This is more efficient than creating a new instance for every execution. You pass an async callback to runSandboxed that receives an object containing evalCode, allowing you to perform sequential operations within the same environment.

    import { type SandboxOptions, loadQuickJs } from '@sebastianwessel/quickjs'
    import variant from '@jitl/quickjs-ng-wasmfile-release-sync'
    
    const options: SandboxOptions = {
      // [...]
    };
    
    const { runSandboxed } = await loadQuickJs(variant);
    
    const finalResult = await runSandboxed(async ({ evalCode }) => {
      const firstResult = await evalCode("// [...code 1]");
      console.log("Step 1:", firstResult);
    
      // Run second call
      return evalCode("// [...code 2]");
    }, options);
    
    console.log(finalResult);
  10. Implement state management (memory) for sandboxed code

    main

    QuickJS functions are stateless. To allow user-generated code to maintain state (e.g., for debouncing or rate limiting), you must manage the state in the host system and expose it to the sandbox via the env property in SandboxOptions.

    By providing getter and setter functions in env, the guest code can read and update host-managed variables.

    let memory: Date = new Date(0); // Host-side state
    
    const options: SandboxOptions = {
      allowFetch: false,
      allowFs: true,
      transformTypescript: true,
      env: {
        setLastAlert: (input: Date) => {
          memory = input;
        },
        getLastAlert: () => memory,
      },
    };
    
    // Inside the sandbox, the user can now call:
    // env.getLastAlert()
    // env.setLastAlert(new Date())
  11. Enable networking and file system capabilities

    main

    By default, the sandbox is restricted. You can enable specific capabilities using these options:

    • allowFetch: Set to true to allow code to make HTTP(S) calls via the global fetch API. You can also provide a custom fetchAdapter to control how fetching is handled.
    • allowFs: Set to true to enable file capabilities, making the node:fs package available.
    • mountFs: Use this to mount a virtual file system (compatible with memfs).
    • nodeModules: Mount custom node_modules within a virtual file system.