
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
almostnode
Advanced tools
Node.js in your browser. Just like that.
A lightweight, browser-native Node.js runtime environment. Run Node.js code, install npm packages, and develop with Vite or Next.js - all without a server.
Built by the creators of Macaly.com — a tool that lets anyone build websites and web apps, even without coding experience. Think Claude Code for non-developers.
Warning: This project is experimental and may contain bugs. Use with caution in production environments.
fs, path, http, events, and more)bin entries (vitest, eslint, tsc, etc.) work automaticallyNote: almostnode runs in the browser and emulates Node.js 20 APIs. The Node.js requirement is only for development tooling (Vite, Vitest, TypeScript).
npm install almostnode
import { createContainer } from 'almostnode';
// Create a Node.js container in the browser
const container = createContainer();
// Execute JavaScript code directly
const result = container.execute(`
const path = require('path');
const fs = require('fs');
// Use Node.js APIs in the browser!
fs.writeFileSync('/hello.txt', 'Hello from the browser!');
module.exports = fs.readFileSync('/hello.txt', 'utf8');
`);
console.log(result.exports); // "Hello from the browser!"
⚠️ Security Warning: The example above runs code on the main thread with full access to your page. Do not use
createContainer()orcontainer.execute()with untrusted code. For untrusted code, usecreateRuntime()with a cross-origin sandbox - see Sandbox Setup.
import { createRuntime, VirtualFS } from 'almostnode';
const vfs = new VirtualFS();
// Create a secure runtime with cross-origin isolation
const runtime = await createRuntime(vfs, {
sandbox: 'https://your-sandbox.vercel.app', // Deploy with generateSandboxFiles()
});
// Now it's safe to run untrusted code
const result = await runtime.execute(untrustedCode);
See Sandbox Setup for deployment instructions.
import { createContainer } from 'almostnode';
const container = createContainer();
const { vfs } = container;
// Pre-populate the virtual filesystem
vfs.writeFileSync('/src/index.js', `
const data = require('./data.json');
console.log('Users:', data.users.length);
module.exports = data;
`);
vfs.writeFileSync('/src/data.json', JSON.stringify({
users: [{ name: 'Alice' }, { name: 'Bob' }]
}));
// Run from the virtual filesystem
const result = container.runFile('/src/index.js');
import { createContainer } from 'almostnode';
const container = createContainer();
// Install a package
await container.npm.install('lodash');
// Use it in your code
container.execute(`
const _ = require('lodash');
console.log(_.capitalize('hello world'));
`);
// Output: Hello world
import { createContainer } from 'almostnode';
const container = createContainer();
// Write a package.json with scripts
container.vfs.writeFileSync('/package.json', JSON.stringify({
name: 'my-app',
scripts: {
build: 'echo Building...',
test: 'vitest run'
}
}));
// Run shell commands directly
const result = await container.run('npm run build');
console.log(result.stdout); // "Building..."
await container.run('npm test');
await container.run('echo hello && echo world');
await container.run('ls /');
Supported npm commands: npm run <script>, npm start, npm test, npm install, npm ls.
Pre/post lifecycle scripts (prebuild, postbuild, etc.) run automatically.
Any npm package with a bin field works automatically after install — no configuration needed.
// Install a package that includes a CLI tool
await container.npm.install('vitest');
// Run it directly — bin stubs are created in /node_modules/.bin/
const result = await container.run('vitest run');
console.log(result.stdout); // Test results
This works because npm install reads each package's bin field and creates executable scripts in /node_modules/.bin/. The shell's PATH includes /node_modules/.bin, so tools like vitest, eslint, tsc, etc. resolve automatically.
For commands that run continuously (like watch mode), use streaming callbacks and abort signals:
const controller = new AbortController();
await container.run('vitest --watch', {
onStdout: (data) => console.log(data),
onStderr: (data) => console.error(data),
signal: controller.signal,
});
// Send input to the running process
container.sendInput('a'); // Press 'a' to re-run all tests
// Stop the command
controller.abort();
import { VirtualFS, NextDevServer, getServerBridge } from 'almostnode';
const vfs = new VirtualFS();
// Create a Next.js page
vfs.mkdirSync('/pages', { recursive: true });
vfs.writeFileSync('/pages/index.jsx', `
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
`);
// Start the dev server
const server = new NextDevServer(vfs, { port: 3000 });
const bridge = getServerBridge();
await bridge.initServiceWorker();
bridge.registerServer(server, 3000);
// Access at: /__virtual__/3000/
almostnode uses a Service Worker to intercept HTTP requests and route them to virtual dev servers (e.g., ViteDevServer, NextDevServer).
Note: 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 withruntime.execute(), you don't need the service worker.
| Use Case | Setup Required |
|---|---|
| Cross-origin sandbox (recommended for untrusted code) | generateSandboxFiles() - includes everything |
| Same-origin with Vite | almostnodePlugin from almostnode/vite |
| Same-origin with Next.js | getServiceWorkerContent from almostnode/next |
| Same-origin with other frameworks | Manual copy to public directory |
When using createRuntime() with a cross-origin sandbox URL, the service worker must be deployed with the sandbox, not your main app.
The generateSandboxFiles() helper generates all required files:
import { generateSandboxFiles } from 'almostnode';
import fs from 'fs';
const files = generateSandboxFiles();
// Creates: index.html, vercel.json, __sw__.js
fs.mkdirSync('sandbox', { recursive: true });
for (const [filename, content] of Object.entries(files)) {
fs.writeFileSync(`sandbox/${filename}`, content);
}
// Deploy to a different origin:
// cd sandbox && vercel --prod
Generated files:
| File | Purpose |
|---|---|
index.html | Sandbox page that loads almostnode and registers the service worker |
vercel.json | CORS headers for cross-origin iframe embedding |
__sw__.js | Service worker for intercepting dev server requests |
See Sandbox Setup for full deployment instructions.
For trusted code using dangerouslyAllowSameOrigin: true:
// vite.config.ts
import { defineConfig } from 'vite';
import { almostnodePlugin } from 'almostnode/vite';
export default defineConfig({
plugins: [almostnodePlugin()]
});
The plugin serves /__sw__.js automatically during development.
Custom path:
// vite.config.ts
almostnodePlugin({ swPath: '/custom/__sw__.js' })
// Then in your app:
await bridge.initServiceWorker({ swUrl: '/custom/__sw__.js' });
For trusted code using dangerouslyAllowSameOrigin: true:
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());
}
Initialize with the correct path:
// App Router (file-based route)
await bridge.initServiceWorker({ swUrl: '/__sw__.js' });
// Pages Router (API route)
await bridge.initServiceWorker({ swUrl: '/api/__sw__' });
Available exports from almostnode/next:
| Export | Description |
|---|---|
getServiceWorkerContent() | Returns the service worker file content as a string |
getServiceWorkerPath() | Returns the absolute path to the service worker file |
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');
| Feature | almostnode | WebContainers |
|---|---|---|
| Bundle Size | ~250KB gzipped | ~2MB |
| Startup Time | Instant | 2-5 seconds |
| Execution Model | Main thread or Web Worker (configurable) | Web Worker isolates |
| Shell | just-bash (POSIX subset) | Full Linux kernel |
| Native Modules | Stubs only | Full support |
| Networking | Virtual ports | Real TCP/IP |
| Use Case | Lightweight playgrounds, demos | Full development environments |
import { createContainer } from 'almostnode';
function createPlayground() {
const container = createContainer();
return {
run: (code: string) => {
try {
const result = container.execute(code);
return { success: true, result: result.exports };
} catch (error) {
return { success: false, error: error.message };
}
},
reset: () => container.runtime.clearCache(),
};
}
// Usage
const playground = createPlayground();
const output = playground.run(`
const crypto = require('crypto');
module.exports = crypto.randomUUID();
`);
console.log(output); // { success: true, result: "550e8400-e29b-..." }
createContainer(options?)Creates a new container with all components initialized.
interface ContainerOptions {
cwd?: string; // Working directory (default: '/')
env?: Record<string, string>; // Environment variables
onConsole?: (method: string, args: any[]) => void; // Console hook
}
const container = createContainer({
cwd: '/app',
env: { NODE_ENV: 'development' },
onConsole: (method, args) => console.log(`[${method}]`, ...args),
});
Returns:
container.vfs - VirtualFS instancecontainer.runtime - Runtime instancecontainer.npm - PackageManager instancecontainer.serverBridge - ServerBridge instancecontainer.run(command, options?) - Run a shell command (returns Promise<RunResult>)container.sendInput(data) - Send stdin data to the currently running processcontainer.execute(code) - Execute JavaScript codecontainer.runFile(filename) - Run a file from VirtualFScontainer.run(command, options?)interface RunResult {
stdout: string;
stderr: string;
exitCode: number;
}
interface RunOptions {
onStdout?: (data: string) => void; // Stream stdout in real-time
onStderr?: (data: string) => void; // Stream stderr in real-time
signal?: AbortSignal; // Cancel the command
}
container.sendInput(data)Sends data to the stdin of the currently running process. Emits both data and keypress events for compatibility with readline-based tools (e.g., vitest watch mode).
Node.js-compatible filesystem API.
// Synchronous operations
vfs.writeFileSync(path, content);
vfs.readFileSync(path, encoding?);
vfs.mkdirSync(path, { recursive: true });
vfs.readdirSync(path);
vfs.statSync(path);
vfs.unlinkSync(path);
vfs.rmdirSync(path);
vfs.existsSync(path);
vfs.renameSync(oldPath, newPath);
// Async operations
await vfs.readFile(path, encoding?);
await vfs.stat(path);
// File watching
vfs.watch(path, { recursive: true }, (event, filename) => {
console.log(`${event}: ${filename}`);
});
Execute JavaScript/TypeScript code.
// Execute code string
runtime.execute('console.log("Hello")');
// Run a file from VirtualFS
runtime.runFile('/path/to/file.js');
// Require a module
const module = runtime.require('/path/to/module.js');
For advanced use cases, use createRuntime to create a runtime with security options:
import { createRuntime, VirtualFS } from 'almostnode';
const vfs = new VirtualFS();
// RECOMMENDED: Cross-origin sandbox (fully isolated)
const secureRuntime = await createRuntime(vfs, {
sandbox: 'https://your-sandbox.vercel.app',
});
// For demos/trusted code: Same-origin with explicit opt-in
const demoRuntime = await createRuntime(vfs, {
dangerouslyAllowSameOrigin: true,
useWorker: true, // Optional: run in Web Worker
cwd: '/project',
env: { NODE_ENV: 'development' },
});
// Both modes use the same async API
const result = await secureRuntime.execute('module.exports = 1 + 1;');
console.log(result.exports); // 2
| Mode | Option | Security Level | 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 |
Security by default: createRuntime() throws an error if neither sandbox nor dangerouslyAllowSameOrigin is provided.
For running untrusted code securely, deploy a cross-origin sandbox. The key requirement is that the sandbox must be served from a different origin (different domain, subdomain, or port).
import { generateSandboxFiles } from 'almostnode';
import fs from 'fs';
const files = generateSandboxFiles();
// Generates: index.html, vercel.json, __sw__.js
fs.mkdirSync('sandbox', { recursive: true });
for (const [filename, content] of Object.entries(files)) {
fs.writeFileSync(`sandbox/${filename}`, content);
}
// Deploy: cd sandbox && vercel --prod
The generated files include:
index.html - Sandbox page with service worker registrationvercel.json - CORS headers for cross-origin iframe embedding__sw__.js - Service worker for dev server URL accessThe sandbox requires two things:
Create an index.html that loads almostnode and handles postMessage:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<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, filename, vfsSnapshot, options, path, content } = event.data;
try {
switch (type) {
case 'init':
vfs = VirtualFS.fromSnapshot(vfsSnapshot);
runtime = new Runtime(vfs, {
cwd: options?.cwd,
env: options?.env,
onConsole: (method, args) => {
parent.postMessage({ type: 'console', consoleMethod: method, consoleArgs: args }, '*');
},
});
break;
case 'execute':
const result = runtime.execute(code, filename);
parent.postMessage({ type: 'result', id, result }, '*');
break;
case 'runFile':
const runResult = runtime.runFile(filename);
parent.postMessage({ type: 'result', id, result: runResult }, '*');
break;
case 'syncFile':
if (content === null) { try { vfs.unlinkSync(path); } catch {} }
else { vfs.writeFileSync(path, content); }
break;
case 'clearCache':
runtime?.clearCache();
break;
}
} catch (error) {
if (id) parent.postMessage({ type: 'error', id, error: error.message }, '*');
}
});
parent.postMessage({ type: 'ready' }, '*');
</script>
</body>
</html>
The sandbox server must include these headers:
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
Example configurations:
server {
listen 3002;
root /path/to/sandbox;
location / {
add_header Access-Control-Allow-Origin *;
add_header Cross-Origin-Resource-Policy cross-origin;
}
}
Header set Access-Control-Allow-Origin "*"
Header set Cross-Origin-Resource-Policy "cross-origin"
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'));
app.listen(3002);
from http.server import HTTPServer, SimpleHTTPRequestHandler
class CORSHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
super().end_headers()
HTTPServer(('', 3002), CORSHandler).serve_forever()
const runtime = await createRuntime(vfs, {
sandbox: 'https://sandbox.yourdomain.com', // Must be different origin!
});
// Code runs in isolated cross-origin iframe
const result = await runtime.execute(untrustedCode);
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.
| Threat | Status |
|---|---|
| Cookies | Blocked (different origin) |
| localStorage | Blocked (different origin) |
| IndexedDB | Blocked (different origin) |
| DOM access | Blocked (cross-origin iframe) |
Note: Network requests from the sandbox are still possible. Add CSP headers for additional protection.
Install npm packages.
// Install a package
await npm.install('react');
await npm.install('lodash@4.17.21');
// Install multiple packages
await npm.install(['react', 'react-dom']);
967 compatibility tests verify our Node.js API coverage.
| Module | Tests | Coverage | Notes |
|---|---|---|---|
path | 219 | High | POSIX paths (no Windows) |
buffer | 95 | High | All common operations |
fs | 76 | High | Sync + promises API |
url | 67 | High | WHATWG URL + legacy parser |
util | 77 | High | format, inspect, promisify |
process | 60 | High | env, cwd, hrtime, EventEmitter |
events | 50 | High | Full EventEmitter API |
os | 58 | High | Platform info (simulated) |
crypto | 57 | High | Hash, HMAC, random, sign/verify |
querystring | 52 | High | parse, stringify, escape |
stream | 44 | Medium | Readable, Writable, Transform |
zlib | 39 | High | gzip, deflate, brotli |
tty | 40 | High | ReadStream, WriteStream |
perf_hooks | 33 | High | Performance API |
These modules export empty objects or no-op functions:
net, tls, dns, dgramcluster, worker_threadsvm, v8, inspectorasync_hooksimport { VirtualFS, ViteDevServer, getServerBridge } from 'almostnode';
const vfs = new VirtualFS();
// Create a React app
vfs.writeFileSync('/index.html', `
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
`);
vfs.mkdirSync('/src', { recursive: true });
vfs.writeFileSync('/src/main.jsx', `
import React from 'react';
import ReactDOM from 'react-dom/client';
function App() {
return <h1>Hello Vite!</h1>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
`);
// Start Vite dev server
const server = new ViteDevServer(vfs, { port: 5173 });
Supports both Pages Router and App Router:
/pages
/index.jsx → /
/about.jsx → /about
/users/[id].jsx → /users/:id
/api/hello.js → /api/hello
/app
/layout.jsx → Root layout
/page.jsx → /
/about/page.jsx → /about
/users/[id]/page.jsx → /users/:id
almostnode includes built-in Hot Module Replacement support for instant updates during development. When you edit files, changes appear immediately in the preview without a full page reload.
HMR is automatically enabled when using NextDevServer or ViteDevServer. The system uses:
vfs.watch()// HMR works automatically - just edit files and save
vfs.writeFileSync('/app/page.tsx', updatedContent);
// The preview iframe will automatically refresh with the new content
For security, the preview iframe should be sandboxed. HMR uses postMessage for communication, which works correctly with sandboxed iframes:
// Create sandboxed iframe for security
const iframe = document.createElement('iframe');
iframe.src = '/__virtual__/3000/';
// Sandbox restricts the iframe's capabilities - add only what you need
iframe.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups';
container.appendChild(iframe);
// Register the iframe as HMR target after it loads
iframe.onload = () => {
if (iframe.contentWindow) {
devServer.setHMRTarget(iframe.contentWindow);
}
};
Recommended sandbox permissions:
allow-scripts - Required for JavaScript executionallow-same-origin - Allows the iframe to access cookies, localStorage, and IndexedDB (only add if your app needs these; omit for better isolation)allow-forms - If your app uses formsallow-popups - If your app opens new windows/tabsNote: The service worker intercepts
/__virtual__/requests at the origin level, not the iframe level. Theallow-same-originattribute does NOT affect service worker functionality. For maximum security isolation, consider using cross-origin sandbox mode (see below) which doesn't useallow-same-origin.
If you need to manually trigger HMR updates (e.g., after programmatic file changes):
function triggerHMR(path: string, iframe: HTMLIFrameElement): void {
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);
| File Type | HMR Behavior |
|---|---|
.jsx, .tsx | React Refresh (preserves state) |
.js, .ts | Full module reload |
.css | Style injection (no reload) |
.json | Full page reload |
Start the dev server with npm run dev and open any demo at http://localhost:5173:
| Demo | Path | Description |
|---|---|---|
| Next.js | /examples/next-demo.html | Pages & App Router, CSS modules, route groups, API routes, HMR |
| Vite | /examples/vite-demo.html | Vite dev server with React and HMR |
| Vitest | /examples/vitest-demo.html | Real vitest execution with xterm.js terminal and watch mode |
| Express | /examples/express-demo.html | Express.js HTTP server running in the browser |
| Convex | /examples/demo-convex-app.html | Real-time todo app with Convex cloud deployment |
| Vercel AI SDK | /examples/demo-vercel-ai-sdk.html | Streaming AI chatbot with Next.js and OpenAI |
| Bash | /examples/bash-demo.html | Interactive POSIX shell emulator |
git clone https://github.com/macaly/almostnode.git
cd almostnode
npm install
# Unit tests
npm test
# E2E tests (requires Playwright)
npm run test:e2e
npm run dev
See the Demos section for all available examples.
Contributions are welcome! Please:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)MIT License - see LICENSE for details.
Built by the creators of Macaly.com
FAQs
Node.js in your browser. Just like that.
The npm package almostnode receives a total of 648 weekly downloads. As such, almostnode popularity was classified as not popular.
We found that almostnode demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.