/ Docs

Security & Sandbox

Run untrusted code safely with cross-origin isolation, Web Worker execution, and sandboxed iframes.

Security Modes

almostnode provides three execution modes with different security levels. Use createRuntime() to select a mode:

Mode Option Security Use Case
Cross-origin sandbox sandbox: 'https://...' Highest Production, untrusted code
Same-origin Worker dangerouslyAllowSameOrigin: true, useWorker: true Medium Demos with trusted code
Same-origin main thread dangerouslyAllowSameOrigin: true Lowest Trusted code only
Secure by default

createRuntime() throws an error if neither sandbox nor dangerouslyAllowSameOrigin is provided. You must explicitly choose a security level.

Cross-Origin Sandbox (Recommended)

The most secure option. Code runs in a cross-origin iframe, fully isolated from your main application:

import { createRuntime, VirtualFS } from 'almostnode';

const vfs = new VirtualFS();

const runtime = await createRuntime(vfs, {
  sandbox: 'https://your-sandbox.vercel.app',
});

// Safe to run untrusted code
const result = await runtime.execute(untrustedCode);

Same-Origin with Worker

Runs code in a Web Worker on the same origin. Offloads execution from the main thread but shares the origin with your app:

const runtime = await createRuntime(vfs, {
  dangerouslyAllowSameOrigin: true,
  useWorker: true,
});

Same-Origin Main Thread

Runs code directly on the main thread. Simplest setup but provides no isolation — only use with fully trusted code:

const runtime = await createRuntime(vfs, {
  dangerouslyAllowSameOrigin: true,
});
Note

The createContainer() helper always uses same-origin main thread execution. For secure sandboxing, use createRuntime() directly.

REPL & eval() Security

The runtime.createREPL() method uses eval() internally to return expression values. While the eval runs in an isolated generator closure (not the global scope), it shares the same security boundary as execute():

Execution Context eval() Safety Risk
Cross-origin sandbox iframe Safe Code cannot access host page cookies, localStorage, or DOM
Same-origin Worker Moderate Code can access IndexedDB and make network requests, but not the DOM
Same-origin main thread Dangerous for untrusted input Code can access cookies, localStorage, DOM, and all page APIs
Warning

If you let users type arbitrary code into a REPL on the main thread or in a same-origin iframe, that code can access everything your page can: cookies, auth tokens, localStorage, the DOM, and network APIs. For any user-facing REPL or code editor, always use a cross-origin sandboxed iframe.

Sandbox Setup

The cross-origin sandbox must be deployed at a different origin (different domain, subdomain, or port) from your main application.

Quick Setup (Vercel)

Use generateSandboxFiles() to create all required files, then deploy to Vercel:

import { generateSandboxFiles } from 'almostnode';
import fs from 'fs';

const files = generateSandboxFiles();

fs.mkdirSync('sandbox', { recursive: true });
for (const [filename, content] of Object.entries(files)) {
  fs.writeFileSync(`sandbox/${filename}`, content);
}

// Deploy: cd sandbox && vercel --prod

Generated Files

FilePurpose
index.htmlSandbox page that loads almostnode and registers the service worker
vercel.jsonCORS headers for cross-origin iframe embedding
__sw__.jsService worker for intercepting dev server requests

Manual Setup (Any Platform)

If you're not using Vercel, you need two things: a sandbox HTML page and the correct CORS headers.

1. Sandbox HTML Page

Create an index.html that loads almostnode and handles postMessage communication with your main app:

<script type="module">
  import { VirtualFS, Runtime } from 'https://unpkg.com/almostnode/dist/index.js';

  let vfs = null;
  let runtime = null;

  window.addEventListener('message', async (event) => {
    const { type, id, code, vfsSnapshot, options } = event.data;

    switch (type) {
      case 'init':
        vfs = VirtualFS.fromSnapshot(vfsSnapshot);
        runtime = new Runtime(vfs, options);
        break;
      case 'execute':
        const result = runtime.execute(code);
        parent.postMessage({ type: 'result', id, result }, '*');
        break;
    }
  });

  parent.postMessage({ type: 'ready' }, '*');
</script>

2. CORS Headers

The sandbox server must include these headers to allow cross-origin iframe embedding:

Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin

Example server configurations:

Nginx:

server {
    listen 3002;
    root /path/to/sandbox;
    location / {
        add_header Access-Control-Allow-Origin *;
        add_header Cross-Origin-Resource-Policy cross-origin;
    }
}

Express.js:

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
  next();
});
app.use(express.static('sandbox'));

What the Sandbox Protects

ThreatStatus
CookiesBlocked (different origin)
localStorageBlocked (different origin)
IndexedDBBlocked (different origin)
DOM accessBlocked (cross-origin iframe)
Note

Network requests from the sandbox are still possible. Add Content Security Policy headers for additional protection.

Local Development

For local testing, run the sandbox on a different port:

# Terminal 1: Main app on port 5173
npm run dev

# Terminal 2: Sandbox on port 3002
npm run sandbox

Then use sandbox: 'http://localhost:3002/sandbox/' in your app.

Service Worker Setup

almostnode uses a service worker to intercept HTTP requests and route them to virtual dev servers. The service worker is required when using ServerBridge with dev servers.

When is it needed?

The service worker is only needed if you're using dev servers with URL access (e.g., /__virtual__/3000/). If you're only executing code with runtime.execute(), you don't need it.

Use CaseSetup
Cross-origin sandboxgenerateSandboxFiles() — includes everything
Same-origin with VitealmostnodePlugin from almostnode/vite
Same-origin with Next.jsgetServiceWorkerContent from almostnode/next
Other frameworksManual copy to public directory

Vite Plugin

For same-origin setups with Vite, use the built-in plugin:

// vite.config.ts
import { defineConfig } from 'vite';
import { almostnodePlugin } from 'almostnode/vite';

export default defineConfig({
  plugins: [almostnodePlugin()]
});

The plugin automatically serves /__sw__.js during development. You can customize the path:

almostnodePlugin({ swPath: '/custom/__sw__.js' })

// Then in your app:
await bridge.initServiceWorker({ swUrl: '/custom/__sw__.js' });

Next.js Route Handler

For same-origin setups with Next.js:

App Router

// app/__sw__.js/route.ts
import { getServiceWorkerContent } from 'almostnode/next';

export async function GET() {
  return new Response(getServiceWorkerContent(), {
    headers: {
      'Content-Type': 'application/javascript',
      'Cache-Control': 'no-cache',
    },
  });
}

Pages Router

// pages/api/__sw__.ts
import { getServiceWorkerContent } from 'almostnode/next';
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.setHeader('Content-Type', 'application/javascript');
  res.setHeader('Cache-Control', 'no-cache');
  res.send(getServiceWorkerContent());
}

Manual Setup

For other frameworks, copy the service worker to your public directory:

cp node_modules/almostnode/dist/__sw__.js ./public/

Or programmatically:

import { getServiceWorkerPath } from 'almostnode/next';
import fs from 'fs';

fs.copyFileSync(getServiceWorkerPath(), './public/__sw__.js');

HMR with Sandboxed Iframes

Hot module replacement works with sandboxed iframes using postMessage for communication between your main page and the preview iframe.

Iframe Setup

// Create sandboxed iframe for security
const iframe = document.createElement('iframe');
iframe.src = '/__virtual__/3000/';
iframe.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups';
container.appendChild(iframe);

// Register as HMR target after load
iframe.onload = () => {
  if (iframe.contentWindow) {
    devServer.setHMRTarget(iframe.contentWindow);
  }
};

Sandbox Permissions

Manual HMR Triggering

If you need to manually trigger HMR updates after programmatic file changes:

function triggerHMR(path: string, iframe: HTMLIFrameElement) {
  if (iframe.contentWindow) {
    iframe.contentWindow.postMessage({
      type: 'update',
      path,
      timestamp: Date.now(),
      channel: 'next-hmr', // Use 'vite-hmr' for Vite
    }, '*');
  }
}

// After writing a file
vfs.writeFileSync('/app/page.tsx', newContent);
triggerHMR('/app/page.tsx', iframe);

Supported File Types

File TypeHMR Behavior
.jsx, .tsxReact Refresh (preserves state)
.js, .tsFull module reload
.cssStyle injection (no reload)
.jsonFull page reload