diff --git a/docs-internal/todo.md b/docs-internal/todo.md index 7ce34222c..46150280f 100644 --- a/docs-internal/todo.md +++ b/docs-internal/todo.md @@ -1,162 +1,130 @@ -# Sandboxed Node TODOs +# Secure Exec TODOs -## High level +This file tracks the active implementation backlog only. +Resolved work should stay in OpenSpec archives and `docs-internal/friction.md` instead of remaining here as unchecked debt. -- Review the node driver -- Plan desired minimal driver that we'll use in rivet -- Add codemode example -- Add just-bash example -- Test with - - Pi - - Express - - Hono - - ??? +Priority order is: +1. Security and host-protection gaps +2. Compatibility bugs and missing platform behavior +3. Maintainability and performance follow-ups +4. Examples, validation breadth, and product-shaping work -## Todo +## Priority 0: Security and Host Protection -- [x] Remove all `@hono/node-server` bridge integration and load it only from sandboxed `node_modules`. - - Remove bridge module and exports (`packages/secure-exec/src/bridge/hono-node-server.ts`, `packages/secure-exec/src/bridge/index.ts`). - - Remove `@hono/node-server` special-cases in runtime resolution/execution (`packages/secure-exec/src/index.ts`, `packages/secure-exec/src/shared/require-setup.ts`). - - Remove `honoServe`/`honoClose` from adapter/types if no longer needed (`packages/secure-exec/src/types.ts`, `packages/secure-exec/src/shared/permissions.ts`, `packages/secure-exec/src/node/driver.ts`). +- [ ] Finish end-to-end payload guards for remaining browser/bridge paths. + - Node isolate execution now enforces JSON/base64 payload limits, but browser worker paths and remaining bridge `JSON.parse(...)` callsites still need equivalent bounds. + - Files: `packages/secure-exec/src/browser/worker.ts`, `packages/secure-exec/src/bridge/*.ts` -- [x] Implement Node built-in HTTP server bridging (`http.createServer`) without third-party module bridges. - - Add server listen/close/address request-dispatch bridge hooks in runtime setup (`packages/secure-exec/src/index.ts`). - - Implement server-side compatibility in network bridge (`packages/secure-exec/src/bridge/network.ts`). - - Add Node driver implementation backed by `node:http` (`packages/secure-exec/src/node/driver.ts`, `packages/secure-exec/src/types.ts`, `packages/secure-exec/src/shared/permissions.ts`). +- [ ] Add global host resource budgets. + - Bound output bytes, bridge-call rate, timer count, and child-process count so hostile workloads cannot amplify host CPU or memory usage. + - Files: `packages/secure-exec/src/node/execution-driver.ts`, `packages/secure-exec/src/bridge/process.ts`, `packages/secure-exec/src/shared/permissions.ts` -- [x] Expose host-side request path to sandbox servers via `sandbox.network.fetch(...)`. - - Provide a NodeRuntime-level network facade and document concurrent run/fetch pattern (`packages/secure-exec/src/index.ts`, `README.md`, `examples/hono/README.md`). - - Validate end-to-end from loader to runner (`examples/hono/loader/src/index.ts`, `examples/hono/runner/src/index.ts`). - -- [x] Fix `run()` ESM semantics to match docs (return module exports/default instead of evaluation result). - - Fix: `runESM` now returns copied entry-module namespace exports (default + named) after evaluation. - - `packages/secure-exec/src/index.ts`, `packages/secure-exec/tests/index.test.ts` - -- [x] Fix dynamic import execution semantics so imports are not eagerly evaluated before user code. - - Fix: precompile step now resolves/compiles only; instantiate/evaluate occur on first `import()` reach. - - `packages/secure-exec/src/index.ts`, `packages/secure-exec/tests/index.test.ts` - -- [x] Remove brittle require-path hacks/monkeypatches and replace with minimal, explicit compatibility behavior. - - Current hacks include `chalk`, `supports-color`, `tty`, `constants`, `v8`, and `util/url/path` patching. - - `packages/secure-exec/src/shared/require-setup.ts` - -- [x] Decide and enforce sandbox permission default model (allow-by-default vs deny-by-default); tighten if strict mode is desired. - - Fixed by flipping permission checks and env filtering to deny-by-default, and by exporting explicit `allowAll*` helpers for opt-in access. - - `packages/secure-exec/src/shared/permissions.ts` - -- [x] Make console capture robust for circular objects (avoid `JSON.stringify` throw paths in logging). - - `packages/secure-exec/src/index.ts` - -- [x] Reconcile `docs/node-compatability.mdx` with current runtime behavior. - - Fix: completed in `codify-stdlib-support-policy` (remove stale third-party bridge notes, align `http`/`https`/`http2` sections, add support tiers). - -- [x] Close or explicitly codify missing `fs` APIs listed in compatibility docs. - - Fix: completed in `codify-stdlib-support-policy` (`access`/`realpath` documented as implemented; missing APIs now use deterministic unsupported errors). - -- [x] Decide `child_process.fork()` support level. - - Fix: completed in `codify-stdlib-support-policy` (`fork` marked unsupported with deterministic `child_process.fork is not supported in sandbox` error). - -- [x] Tighten crypto support policy and implementation. - - Fix: completed in `codify-stdlib-support-policy` (explicit insecurity warning for `getRandomValues`, deterministic `crypto.subtle.*` unsupported errors, tiered policy documented). - -- [x] Track unimplemented core modules from compatibility docs as explicit product decisions. - - Fix: completed in `codify-stdlib-support-policy` (Tier 4 Deferred vs Tier 5 Unsupported classifications with rationale and runtime policy). +- [ ] Cap and hard-fail child-process output buffering in sync APIs. + - `spawnSync`/`execSync` paths still need deterministic output caps rather than unbounded accumulation. + - Files: `packages/secure-exec/src/node/execution-driver.ts`, `packages/secure-exec/src/bridge/child-process.ts` -## Security & Hardening +- [ ] Ensure child-process sessions are always cleaned up on timeout, dispose, and error paths. + - Session-map leaks will keep host resources alive after sandbox failure paths. + - Files: `packages/secure-exec/src/node/execution-driver.ts` -- [x] Filter Python `exec(..., { env })` overrides through `permissions.env`. - - Fix: `PyodideRuntimeDriver.exec()` now applies the shared `filterEnv(...)` gate before env overrides reach the worker, and runtime-driver tests cover both denied-by-default and explicitly-allowed cases. - - `packages/secure-exec/src/python/driver.ts`, `packages/secure-exec/tests/runtime-driver/python.test.ts` +- [ ] Add request and response body limits for driver HTTP paths, including decompression. + - The Node driver currently buffers request/response bodies and decompresses gzip/deflate without explicit caps. + - Files: `packages/secure-exec/src/node/driver.ts` -- [x] Bridge `crypto.getRandomValues` / `randomUUID` to host `node:crypto` instead of `Math.random()`. - - Fix: runtime now wires host `node:crypto` references from `packages/secure-exec/src/index.ts` into the isolate and uses them in `packages/secure-exec/src/bridge/process.ts`. - - Fail-closed contract: bridge throws deterministic `crypto.getRandomValues is not supported in sandbox` / `crypto.randomUUID is not supported in sandbox` errors when host entropy hooks are unavailable. +- [ ] Fix HTTP server lifecycle leaks when executions time out or are disposed. + - Sandbox-owned servers need deterministic teardown on all execution shutdown paths. + - Files: `packages/secure-exec/src/execution.ts`, `packages/secure-exec/src/node/execution-driver.ts`, `packages/secure-exec/src/node/driver.ts` -- [ ] Add transfer size limits on base64 file I/O across the isolate boundary. - - `packages/secure-exec/src/index.ts` (~line 1138, `readFileBinaryRef` / `writeFileBinaryRef`) - - No cap currently; a large file read can OOM the host process. +- [ ] Verify timer and event-rate controls under hostile workloads. + - Add explicit stress coverage for `setInterval`, `setImmediate`, and high-frequency event emission so abuse resistance is tested instead of assumed. + - Files: `tests/test-suite/node/`, `tests/runtime-driver/` -- [ ] Validate size before host-side `JSON.parse` calls. - - `packages/secure-exec/src/index.ts` (10 unvalidated `JSON.parse` calls) - - Crafted large payloads from sandbox can OOM the host process. +- [ ] Document extension attack vectors and hardening guidance. + - Consolidate memory amplification, CPU amplification, timer/event amplification, and extension host-hook abuse paths in the internal threat model. + - Files: `docs-internal/attack-vectors.md` -- [x] Make bridge globals non-writable on `globalThis`. - - Fix: active-handle lifecycle globals now install via `Object.defineProperty` with `writable: false` and `configurable: false`, preventing sandbox overwrite of `_registerHandle` / `_unregisterHandle` / `_waitForActiveHandles`. - - `packages/secure-exec/src/bridge/active-handles.ts`, `packages/secure-exec/tests/index.test.ts` +## Priority 1: Compatibility and API Coverage -- [ ] Remove default host-side console buffering; drop logs by default and expose optional streaming hook. - - `packages/secure-exec/src/index.ts`, `packages/secure-exec/src/execution.ts` +- [ ] Fix `v8.serialize` and `v8.deserialize` to use V8 structured serialization semantics. + - The current JSON-based behavior is observably wrong for `Map`, `Set`, `RegExp`, circular references, and other structured-clone cases. + - Files: `packages/secure-exec/isolate-runtime/src/inject/bridge-initial-globals.ts` -- [ ] Add global host resource budgets (output bytes, bridge-call rate, timer count, child-process count). - - `packages/secure-exec/src/index.ts`, `packages/secure-exec/src/bridge/process.ts`, `packages/secure-exec/src/shared/permissions.ts` +- [ ] Add missing `fs` APIs needed for broader Node parity. + - Missing APIs: `cp`, `cpSync`, `glob`, `globSync`, `opendir`, `mkdtemp`, `mkdtempSync`, `statfs`, `statfsSync`, `readv`, `readvSync`, `fdatasync`, `fdatasyncSync`, `fsync`, `fsyncSync`. + - Files: `packages/secure-exec/src/bridge/fs.ts` -- [ ] Cap and hard-fail child-process output buffering in sync APIs. - - `packages/secure-exec/src/index.ts` (`spawnSyncRef` / `execSyncRef`) +- [ ] Implement deferred `fs` APIs or explicitly keep them out of scope with stronger compatibility guidance. + - Deferred APIs: `chmod`, `chown`, `link`, `symlink`, `readlink`, `truncate`, `utimes`, `watch`, `watchFile`. + - Files: `packages/secure-exec/src/bridge/fs.ts`, `docs/node-compatability.mdx` -- [ ] Ensure child-process sessions are always cleaned up on timeout/dispose/error paths. - - `packages/secure-exec/src/index.ts` (`sessions` map in child-process bridge) +- [ ] Add missing lower-level `http` and `https` APIs. + - Remaining gaps include `Agent` pooling/keep-alive controls, upgrade handling, trailer headers, and socket-level events. + - Files: `packages/secure-exec/src/bridge/network.ts`, `packages/secure-exec/src/node/driver.ts` -- [ ] Add request/response body limits for driver HTTP paths (including decompression). - - `packages/secure-exec/src/node/driver.ts` (`httpServerListen`, `fetch`, `httpRequest`) - -- [ ] Fix HTTP server lifecycle leaks when executions time out or are disposed. - - `packages/secure-exec/src/execution.ts`, `packages/secure-exec/src/index.ts`, `packages/secure-exec/src/node/driver.ts` +- [ ] Add a dedicated lazy dynamic-import regression test. + - Top-level-await plus `import()` ordering still has a tracked edge case and needs explicit coverage. + - Files: `packages/secure-exec/tests/` -## API Coverage Gaps +- [ ] Document and verify package-manager support for `node_modules` loading behavior. + - Add compatibility fixtures that exercise npm, pnpm, yarn, and bun layouts without sandbox-aware fixture code. + - Files: `packages/secure-exec/tests/projects/`, `docs/node-compatability.mdx` -- [ ] Add missing `fs` APIs: `cp`, `cpSync`, `glob`, `globSync`, `opendir`, `mkdtemp`, `mkdtempSync`, `statfs`, `statfsSync`, `readv`, `readvSync`, `fdatasync`, `fdatasyncSync`, `fsync`, `fsyncSync`. - - `packages/secure-exec/src/bridge/fs.ts` - - Goal: full `node:fs` coverage for core file operations. +## Priority 2: Maintainability and Performance -- [ ] Implement deferred `fs` APIs: `chmod`, `chown`, `link`, `symlink`, `readlink`, `truncate`, `utimes`, `watch`, `watchFile`. - - `packages/secure-exec/src/bridge/fs.ts` - - Currently throw deterministic unsupported errors. +- [ ] Remove remaining `@ts-nocheck` bypasses in bridge internals. + - Current bypasses remain in `bridge/polyfills.ts`, `bridge/os.ts`, `bridge/child-process.ts`, `bridge/process.ts`, and `bridge/network.ts`. + - Files: `packages/secure-exec/src/bridge/*.ts` -- [ ] Add missing `http`/`https` APIs: connection pooling (`Agent`), keep-alive tuning, WebSocket upgrade, trailer headers, socket-level events. - - `packages/secure-exec/src/bridge/network.ts` - - Current implementation is fetch-based and fully buffered with no socket-level control. +- [ ] Split `NodeExecutionDriver` into focused modules. + - The old `index.ts` monolith has moved; the main concentration of complexity is now `packages/secure-exec/src/node/execution-driver.ts`. + - Suggested extraction targets: isolate bootstrap, module resolution, ESM compilation, bridge setup, and execution lifecycle. -- [ ] Fix `v8.serialize`/`v8.deserialize` to use V8 structured serialization instead of `JSON.stringify`/`JSON.parse`. - - `packages/secure-exec/isolate-runtime/src/inject/bridge-initial-globals.ts` (~line 51) - - Bug: current implementation silently produces JSON instead of V8 binary format. Code depending on structured clone semantics (`Map`, `Set`, `RegExp`, circular refs) will get wrong results. +- [ ] Make ESM module reverse lookup O(1). + - Large import graphs still risk quadratic resolver work. + - Files: `packages/secure-exec/src/node/execution-driver.ts` -## Performance & Correctness +- [ ] Add resolver memoization for positive and negative lookups. + - Avoid repeated miss probes across `require()` and `import()` paths. + - Files: `packages/secure-exec/src/package-bundler.ts`, `packages/secure-exec/src/shared/require-setup.ts`, `packages/secure-exec/src/node/execution-driver.ts` -- [ ] Add `stat` and `exists` methods to `VirtualFileSystem` interface. - - `packages/secure-exec/src/types.ts`, `packages/secure-exec/src/fs-helpers.ts` - - Current `stat()` and `exists()` read entire file contents; O(file size) when should be O(1). Also a DoS vector via large files. +- [ ] Cap and cache `package.json` parsing in resolver paths. + - Prevent repeated large-file reads and large JSON parse overhead in package resolution. + - Files: `packages/secure-exec/src/package-bundler.ts` -- [ ] Split `index.ts` into focused modules. - - `packages/secure-exec/src/index.ts` (1,956 lines and growing) - - Extract: `isolate.ts`, `module-resolver.ts`, `esm-compiler.ts`, `bridge-setup.ts`, `execution.ts`. +- [ ] Reduce module-access lookup overhead. + - Add prefix indexing and canonicalization memoization in module-access checks. + - Files: `packages/secure-exec/src/node/module-access.ts` -- [ ] Replace magic O_* flag numbers with named constants. - - `packages/secure-exec/src/bridge/fs.ts` (~line 690) - - Hardcoded integers (577, 578, 1089, etc.) are Linux-specific and undocumented. +- [ ] Replace whole-file fd sync emulation with offset-based host read/write primitives. + - The current approach does more work than necessary and increases large-file pressure. + - Files: `packages/secure-exec/src/bridge/fs.ts` -- [ ] Fix `readDirWithTypes` N+1 I/O pattern. - - `packages/secure-exec/src/fs-helpers.ts` (~line 112) - - Calls `readDir` per entry to check if directory; add `readDirWithTypes` or `stat` to `VirtualFileSystem`. +- [ ] Replace magic `O_*` flag numbers with named constants. + - Hardcoded flag values are difficult to audit and easy to misuse. + - Files: `packages/secure-exec/src/bridge/fs.ts` -- [ ] Make `rename` atomic or document limitation. - - `packages/secure-exec/src/fs-helpers.ts` (lines 90-92) - - Currently read + write + delete; crash between steps can duplicate or lose data. +- [ ] Convert IO handling into a shared abstraction reusable across runtimes. + - Shared request/response/stream/error contracts should reduce Node/browser/runtime drift. + - Files: `packages/secure-exec/src/`, `tests/test-suite/` -- [ ] Make ESM module reverse-lookup O(1) to avoid O(n^2) resolver work on large import graphs. - - `packages/secure-exec/src/index.ts` (ESM resolver / module cache lookup) +## Priority 3: Examples, Validation Breadth, and Product Direction -- [ ] Add resolver memoization (positive + negative) to avoid repeated miss probes across `require()`/`import()`. - - `packages/secure-exec/src/package-bundler.ts`, `packages/secure-exec/src/shared/require-setup.ts`, `packages/secure-exec/src/index.ts` +- [ ] Review the Node driver against the intended long-term runtime contract. + - Use the current architecture docs and glossary terms to decide what stays driver-owned versus runtime-owned. + - Files: `docs-internal/arch/overview.md`, `docs-internal/glossary.md`, `packages/secure-exec/src/node/` -- [ ] Document and verify package manager support for `node_modules` loading behavior. - - Cover expected resolver behavior and known caveats for npm, pnpm, yarn, and bun installs. - - Add/maintain compatibility fixtures that exercise transitive dependency loading across supported package manager layouts. +- [ ] Define the minimal driver surface needed for Rivet integration. + - This should turn the high-level “minimal driver” idea into a concrete API checklist. + - Files: `docs-internal/arch/overview.md`, `packages/secure-exec/src/types.ts` -- [ ] Cap and cache `package.json` parsing in resolver paths. - - `packages/secure-exec/src/package-bundler.ts` +- [ ] Add a codemode example. + - Provide a focused example that demonstrates secure-exec usage in a realistic tool flow. + - Files: `examples/` -- [ ] Reduce module-access lookup overhead (prefix index + canonicalization memoization). - - `packages/secure-exec/src/node/module-access.ts` +- [ ] Add a just-bash example. + - Show the smallest useful command-execution integration without broader framework scaffolding. + - Files: `examples/` -- [ ] Replace whole-file fd sync emulation with offset-based host read/write primitives. - - `packages/secure-exec/src/bridge/fs.ts` +- [ ] Expand framework and environment validation. + - Add or maintain black-box coverage for projects using Pi, Express, Hono, and other representative package stacks. + - Files: `packages/secure-exec/tests/projects/` diff --git a/docs/api-reference.mdx b/docs/api-reference.mdx index ee5f7c454..a4048c3dc 100644 --- a/docs/api-reference.mdx +++ b/docs/api-reference.mdx @@ -1,6 +1,7 @@ --- title: API Reference description: Complete reference for all secure-exec exports. +icon: "rectangle-code" --- ## Runtimes @@ -390,33 +391,33 @@ type StdioHook = (event: { channel: "stdout" | "stderr"; message: string }) => v | Field | Type | Default | |---|---|---| -| `platform` | `string` | — | -| `arch` | `string` | — | -| `version` | `string` | — | +| `platform` | `string` | | +| `arch` | `string` | | +| `version` | `string` | | | `cwd` | `string` | `"/root"` | -| `env` | `Record` | — | -| `argv` | `string[]` | — | -| `execPath` | `string` | — | -| `pid` | `number` | — | -| `ppid` | `number` | — | -| `uid` | `number` | — | -| `gid` | `number` | — | -| `stdin` | `string` | — | -| `timingMitigation` | `"off" \| "freeze"` | — | -| `frozenTimeMs` | `number` | — | +| `env` | `Record` | | +| `argv` | `string[]` | | +| `execPath` | `string` | | +| `pid` | `number` | | +| `ppid` | `number` | | +| `uid` | `number` | | +| `gid` | `number` | | +| `stdin` | `string` | | +| `timingMitigation` | `"off" \| "freeze"` | | +| `frozenTimeMs` | `number` | | ### `OSConfig` | Field | Type | Default | |---|---|---| -| `platform` | `string` | — | -| `arch` | `string` | — | -| `type` | `string` | — | -| `release` | `string` | — | -| `version` | `string` | — | +| `platform` | `string` | | +| `arch` | `string` | | +| `type` | `string` | | +| `release` | `string` | | +| `version` | `string` | | | `homedir` | `string` | `"/root"` | | `tmpdir` | `string` | `"/tmp"` | -| `hostname` | `string` | — | +| `hostname` | `string` | | ### `SystemDriver` diff --git a/docs/architecture.mdx b/docs/architecture.mdx new file mode 100644 index 000000000..561f288d2 --- /dev/null +++ b/docs/architecture.mdx @@ -0,0 +1,182 @@ +--- +title: Architecture +description: How secure-exec components fit together across runtimes and environments. +icon: "sitemap" +--- + +## Overview + +Every secure-exec sandbox has three layers: a **runtime** (public API), a **bridge** (isolation boundary), and a **system driver** (host capabilities). + +```mermaid +flowchart TB + subgraph Host["Host Process"] + RT["Runtime
(NodeRuntime / PythonRuntime)"] + SD["System Driver
(filesystem, network, processes, permissions)"] + end + subgraph Isolate["Sandbox"] + UC["User Code"] + BR["Bridge"] + end + RT --> SD + RT --> Isolate + UC -->|"capability request"| BR + BR -->|"permission-gated call"| SD +``` + +User code runs inside the sandbox and can only reach host capabilities through the bridge. The bridge enforces payload size limits on every transfer. The system driver wraps each capability in a permission check before executing it on the host. + +## Components + +### Runtime + +The public API. `NodeRuntime` and `PythonRuntime` are thin facades that accept a system driver and a runtime driver factory, then delegate all execution to the runtime driver. + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), +}); + +await runtime.exec("console.log('hello')"); +await runtime.run("export default 42"); +runtime.dispose(); +``` + +### System Driver + +A config object that bundles host capabilities. Deny-by-default. + +| Capability | What it provides | +|---|---| +| `filesystem` | Read/write/stat/mkdir operations | +| `network` | fetch, DNS, HTTP | +| `commandExecutor` | Child process spawning | +| `permissions` | Per-capability allow/deny checks | + +Each capability is wrapped in a permission layer before the bridge can call it. Missing capabilities get deny-all stubs. + +### Bridge + +The narrow interface between the sandbox and the host. All privileged operations pass through the bridge. It serializes requests, enforces payload size limits, and routes calls to the appropriate system driver capability. + +### Runtime Driver + +Manages the actual execution environment. This is where the runtime-specific isolation mechanism lives. + +## Node Runtime + +On Node, the sandbox is a V8 isolate managed by `isolated-vm`. + +```mermaid +flowchart TB + subgraph Host["Host (Node.js process)"] + NR["NodeRuntime"] + NED["NodeExecutionDriver"] + SD["System Driver"] + MAFS["ModuleAccessFileSystem"] + end + subgraph ISO["V8 Isolate"] + UC["User Code (CJS / ESM)"] + BR["Bridge (ivm.Reference callbacks)"] + MOD["Module Cache"] + end + NR --> NED + NED --> ISO + UC --> BR + BR -->|"fs / net / process / crypto"| SD + SD --> MAFS +``` + +**Inside the isolate:** +- User code runs as CJS or ESM (auto-detected from `package.json` `type` field) +- Bridge globals are injected as `ivm.Reference` callbacks for fs, network, child_process, crypto, and timers +- Compiled modules are cached per isolate +- `Date.now()` and `performance.now()` return frozen values by default (timing mitigation) +- `SharedArrayBuffer` is unavailable in freeze mode + +**Outside the isolate (host):** +- `NodeExecutionDriver` creates contexts, compiles modules, and manages the isolate lifecycle +- `ModuleAccessFileSystem` overlays host `node_modules` at `/app/node_modules` (read-only, blocks `.node` native addons, prevents symlink escapes) +- System driver applies permission checks before every host operation +- Bridge enforces payload size limits on all transfers (`ERR_SANDBOX_PAYLOAD_TOO_LARGE`) + +**Resource controls:** +- `memoryLimit`: V8 isolate memory cap (default 128 MB) +- `cpuTimeLimitMs`: CPU time budget (exit code 124 on exceeded) +- `timingMitigation`: `"freeze"` (default) or `"off"` + +## Browser Runtime + +In the browser, the sandbox is a Web Worker. + +```mermaid +flowchart TB + subgraph Host["Host (browser main thread)"] + NR["NodeRuntime"] + BRD["BrowserRuntimeDriver"] + SD["System Driver"] + end + subgraph WK["Web Worker"] + UC["User Code (CJS / ESM)"] + BR["Bridge (postMessage RPC)"] + end + NR --> BRD + BRD -->|"postMessage"| WK + UC --> BR + BR -->|"postMessage"| SD +``` + +**Inside the worker:** +- User code runs as transformed CJS/ESM +- Bridge globals are initialized from the worker init payload +- Filesystem and network use permission-aware adapters +- DNS operations return deterministic `ENOSYS` errors + +**Outside the worker (host):** +- `BrowserRuntimeDriver` spawns workers, dispatches requests by ID, and correlates responses +- `createBrowserDriver()` configures OPFS or in-memory filesystem and fetch-based networking +- Node-only runtime options (like `memoryLimit`) are validated and rejected at creation time + +## Python Runtime + +The Python sandbox runs Pyodide in a Node Worker thread. + +```mermaid +flowchart TB + subgraph Host["Host (Node.js process)"] + PR["PythonRuntime"] + PRD["PyodideRuntimeDriver"] + SD["System Driver"] + end + subgraph WK["Worker Thread"] + PY["Pyodide (CPython via Emscripten)"] + UC["User Code (Python)"] + BR["Bridge (worker RPC)"] + end + PR --> PRD + PRD -->|"worker messages"| WK + UC --> BR + BR -->|"worker RPC"| SD +``` + +**Inside the worker:** +- Pyodide loads once and keeps interpreter state warm across runs +- Python code executes with access to bridged filesystem and network +- stdio streams to the host via message events + +**Outside the worker (host):** +- `PyodideRuntimeDriver` manages worker lifecycle and request correlation +- Filesystem and network access goes through the same `SystemDriver` permission layer +- On execution timeout, the worker state restarts for deterministic recovery +- No `memoryLimit` or `timingMitigation` (Pyodide runs in a Worker, not a V8 isolate) + +## Permission Flow + +Every capability request follows the same path regardless of runtime: + +``` +User Code -> Bridge -> Permission Check -> System Driver -> Host OS +``` + +If the permission check denies the request, the bridge returns an error before the system driver is called. If no adapter is configured for a capability, a deny-all stub handles it. diff --git a/docs/cloudflare-workers-comparison.mdx b/docs/cloudflare-workers-comparison.mdx index e564edf37..247263cf5 100644 --- a/docs/cloudflare-workers-comparison.mdx +++ b/docs/cloudflare-workers-comparison.mdx @@ -1,6 +1,7 @@ --- title: Cloudflare Workers Comparison description: Node.js API compatibility comparison between secure-exec and Cloudflare Workers (standard, Workers for Platforms, dynamic dispatch). +icon: "scale-balanced" --- *Last updated: 2026-03-10* @@ -23,12 +24,12 @@ All three CF deployment models share the same `nodejs_compat` API surface. WfP a | Icon | Meaning | | --- | --- | -| 🟢 | Supported — native or full implementation. | -| 🔵 | Planned — not yet implemented; on the roadmap. | -| 🟡 | Partial — functional with behavioral gaps or wrapper limitations. | -| ⚪ | TBD — under consideration; not yet committed. | -| 🔴 | Stub — requireable but most APIs throw on call. | -| ⛔ | Unsupported — not available; `require()` throws immediately. | +| 🟢 | Supported: native or full implementation. | +| 🔵 | Planned: not yet implemented; on the roadmap. | +| 🟡 | Partial: functional with behavioral gaps or wrapper limitations. | +| ⚪ | TBD: under consideration; not yet committed. | +| 🔴 | Stub: requireable but most APIs throw on call. | +| ⛔ | Unsupported: not available; `require()` throws immediately. | --- @@ -57,8 +58,8 @@ All three CF deployment models share the same `nodejs_compat` API surface. WfP a | **`worker_threads`** | ⛔ Stubs that throw on API call. | 🔴 Non-functional stub. | Neither platform supports worker threads. | | **`cluster`** | ⛔ `require()` throws. | 🔴 Non-functional stub. | Neither platform supports clustering. | | **`timers`** | 🟢 `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `setImmediate`, `clearImmediate`. | 🟢 Same surface; returns `Timeout` objects. | Equivalent support. | -| **`vm`** | 🔴 Browser polyfill via `Function()`/`eval()`. No real context isolation; shares global scope. | 🔴 Non-functional stub. | Neither offers real `vm` sandboxing. secure-exec polyfill silently runs code in shared scope — not safe for isolation. | -| **`v8`** | 🔴 Mock heap stats; `serialize`/`deserialize` use JSON instead of V8 binary format (bug). | 🔴 Non-functional stub. | Neither exposes real V8 internals. secure-exec `v8.serialize` silently produces JSON — needs fix to use V8 structured serialization. | +| **`vm`** | 🔴 Browser polyfill via `Function()`/`eval()`. No real context isolation; shares global scope. | 🔴 Non-functional stub. | Neither offers real `vm` sandboxing. secure-exec polyfill silently runs code in shared scope, not safe for isolation. | +| **`v8`** | 🔴 Mock heap stats; `serialize`/`deserialize` use JSON instead of V8 binary format (bug). | 🔴 Non-functional stub. | Neither exposes real V8 internals. secure-exec `v8.serialize` silently produces JSON, needs fix to use V8 structured serialization. | ### Crypto and Security @@ -117,7 +118,7 @@ All three CF deployment models share the same `nodejs_compat` API surface. WfP a | **Permission model** | Deny-by-default for `fs`, `network`, `childProcess`, `env`. Fine-grained per-domain policies. | No granular permission model. WfP adds `request.cf` restriction and cache isolation. | | **Memory limits** | Configurable `memoryLimit` (MB). | 128 MB per Worker (platform-managed). | | **CPU time limits** | Configurable `cpuTimeLimitMs` with exit code 124. | 10ms (free) / 30s (paid) CPU time; WfP operators can set custom limits. | -| **Timing mitigation** | `freeze` mode (deterministic clocks) or `off` (real-time). | I/O-gated coarsening — `Date.now()` and `performance.now()` only advance after I/O to mitigate Spectre-class side channels. | +| **Timing mitigation** | `freeze` mode (deterministic clocks) or `off` (real-time). | I/O-gated coarsening: `Date.now()` and `performance.now()` only advance after I/O to mitigate Spectre-class side channels. | | **Module loading** | CJS + ESM with package.json `type` field semantics; `node_modules` overlay. | ES modules primary; CJS via `nodejs_compat`; no `node_modules` overlay (bundled at deploy). | | **Subprocess execution** | Bound to the system driver; subprocess behavior determined by driver implementation. | Not available. | | **Filesystem** | System-driver-determined: host filesystem (permission-gated) or virtual filesystem, depending on driver implementation. Read-only `/app/node_modules` overlay. | Ephemeral VFS only; Durable Objects for persistence. | diff --git a/docs/docs.json b/docs/docs.json index 2d41e2d7b..483093269 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,17 +1,30 @@ { "$schema": "https://mintlify.com/docs.json", "theme": "mint", - "name": "secure-exec", + "appearance": { + "default": "dark", + "strict": true + }, + "background": { + "color": { + "dark": "#09090b" + } + }, + "name": "Secure Exec", + "logo": { + "light": "/logo.svg", + "dark": "/logo.svg" + }, "colors": { - "primary": "#0EA5A4", - "light": "#5EEAD4", - "dark": "#0F766E" + "primary": "#CC0000", + "light": "#FF3333", + "dark": "#CC0000" }, "navigation": { "groups": [ { "group": "Getting Started", - "pages": ["quickstart", "api-reference"] + "pages": ["quickstart", "sdk-overview"] }, { "group": "Runtimes", @@ -30,16 +43,28 @@ ] }, { - "group": "Security", - "pages": ["security-model"] - }, - { - "group": "Compatibility", - "pages": ["node-compatability"] + "group": "Features", + "pages": [ + "features/permissions", + "features/filesystem", + "features/networking", + "features/module-loading", + "features/resource-limits", + "features/typescript", + "features/child-processes", + "features/output-capture" + ] }, { - "group": "Comparisons", - "pages": ["cloudflare-workers-comparison"] + "group": "Reference", + "pages": [ + "api-reference", + "architecture", + "security-model", + "nodejs-compatibility", + "python-compatibility", + "cloudflare-workers-comparison" + ] } ] } diff --git a/docs/features/child-processes.mdx b/docs/features/child-processes.mdx new file mode 100644 index 000000000..c092d883a --- /dev/null +++ b/docs/features/child-processes.mdx @@ -0,0 +1,84 @@ +--- +title: Child Processes +description: Spawn child processes from sandboxed code. +icon: "terminal" +--- + +Sandboxed code can spawn child processes through the `CommandExecutor` interface, gated by the `childProcess` permission. + +## Setup + +```ts +import { + NodeRuntime, + createNodeDriver, + createNodeRuntimeDriverFactory, + allowAllChildProcess, +} from "secure-exec"; + +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver({ + permissions: { childProcess: allowAllChildProcess }, + }), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), +}); +``` + +## Usage from sandboxed code + +```ts +await runtime.exec(` + const { execSync } = require('child_process'); + const result = execSync('echo hello').toString(); + console.log(result); // "hello" +`); +``` + +## Permission gating + +Restrict which commands sandboxed code can spawn: + +```ts +const driver = createNodeDriver({ + permissions: { + childProcess: (req) => { + const allowed = ["node", "python3", "echo"]; + return allowed.includes(req.command); + }, + }, +}); +``` + +## Custom CommandExecutor + +Provide your own executor for full control over process spawning: + +```ts +const driver = createNodeDriver({ + commandExecutor: { + spawn(command, args, options) { + // Custom spawn logic + // Returns a SpawnedProcess + }, + }, + permissions: { childProcess: true }, +}); +``` + +## Process configuration + +Configure the process environment visible to sandboxed code: + +```ts +const driver = createNodeDriver({ + processConfig: { + cwd: "/app", + env: { NODE_ENV: "production" }, + argv: ["node", "script.js"], + platform: "linux", + arch: "x64", + }, +}); +``` + +Child processes are only available with the Node system driver. The browser driver does not support process spawning. diff --git a/docs/features/filesystem.mdx b/docs/features/filesystem.mdx new file mode 100644 index 000000000..a238fc6fa --- /dev/null +++ b/docs/features/filesystem.mdx @@ -0,0 +1,74 @@ +--- +title: Filesystem +description: Filesystem backends for sandboxed code. +icon: "folder" +--- + +secure-exec supports three filesystem backends. The system driver controls which backend sandboxed code uses. + +## In-memory + +Fully in-memory filesystem backed by Maps. Works in both Node and browser. + +```ts +import { createInMemoryFileSystem } from "secure-exec"; + +const fs = createInMemoryFileSystem(); +await fs.writeFile("/hello.txt", new TextEncoder().encode("hello")); +``` + +Use with a driver: + +```ts +import { createNodeDriver, createInMemoryFileSystem, allowAllFs } from "secure-exec"; + +const driver = createNodeDriver({ + filesystem: createInMemoryFileSystem(), + permissions: { fs: allowAllFs }, +}); +``` + +## OPFS (browser) + +Persistent filesystem using the Origin Private File System API. This is the default for `createBrowserDriver()`. + +```ts +import { createBrowserDriver } from "secure-exec/browser"; + +// OPFS (default) +const driver = await createBrowserDriver({ filesystem: "opfs" }); + +// In-memory fallback +const memDriver = await createBrowserDriver({ filesystem: "memory" }); +``` + +OPFS does not support atomic rename operations. + +## Node filesystem + +Thin wrapper around Node.js `fs/promises`. Provides real host filesystem access (gated by permissions). + +```ts +import { NodeFileSystem } from "secure-exec"; + +const fs = new NodeFileSystem(); +``` + +## VirtualFileSystem interface + +All backends implement this interface: + +| Method | Returns | Description | +|---|---|---| +| `readFile(path)` | `Promise` | Read file as bytes | +| `readTextFile(path)` | `Promise` | Read file as text | +| `readDir(path)` | `Promise` | List directory entries | +| `readDirWithTypes(path)` | `Promise` | List entries with type info | +| `writeFile(path, content)` | `Promise` | Write file | +| `createDir(path)` | `Promise` | Create directory | +| `mkdir(path)` | `Promise` | Create directory (alias) | +| `exists(path)` | `Promise` | Check if path exists | +| `stat(path)` | `Promise` | Get file metadata | +| `removeFile(path)` | `Promise` | Delete a file | +| `removeDir(path)` | `Promise` | Delete a directory | +| `rename(old, new)` | `Promise` | Rename a file or directory | diff --git a/docs/features/module-loading.mdx b/docs/features/module-loading.mdx new file mode 100644 index 000000000..89baafab5 --- /dev/null +++ b/docs/features/module-loading.mdx @@ -0,0 +1,51 @@ +--- +title: Module Loading +description: How sandboxed code resolves and loads modules. +icon: "cubes" +--- + +Sandboxed code can `require()` and `import` modules through secure-exec's module resolution system. + +## node_modules overlay + +Node runtime executions expose a read-only dependency overlay at `/app/node_modules`, sourced from `/node_modules` on the host (default `cwd` is `process.cwd()`). + +```ts +// Inside the sandbox, this resolves from the host's node_modules +const lodash = require("lodash"); +``` + +Key constraints: + +- Overlay paths are **read-only** +- Reads are constrained to canonical paths under `/node_modules` (no symlink escapes) +- Native addons (`.node` files) are rejected +- Access outside overlay paths remains permission-gated + +## Configuring moduleAccess + +Override the host directory used for the overlay: + +```ts +import { createNodeDriver } from "secure-exec"; + +const driver = createNodeDriver({ + moduleAccess: { + cwd: "/path/to/project", + }, +}); +``` + +## Module support tiers + +Built-in modules fall into five support tiers: + +| Tier | Behavior | Examples | +|---|---|---| +| **Bridge** | Full implementation in secure-exec bridge | `fs`, `process`, `os`, `child_process`, `http`, `dns` | +| **Polyfill** | Browser-compatible polyfill | `path`, `buffer`, `url`, `events`, `stream`, `util`, `assert` | +| **Stub** | Minimal compatibility surface | `http2`, `crypto`, `v8` | +| **Deferred** | `require()` succeeds, APIs throw unsupported errors | `net`, `tls`, `readline`, `perf_hooks`, `worker_threads` | +| **Unsupported** | `require()` throws immediately | `dgram`, `cluster`, `wasi`, `inspector` | + +See the [Node Compatibility](/node-compatability) page for the complete module matrix. diff --git a/docs/features/networking.mdx b/docs/features/networking.mdx new file mode 100644 index 000000000..7196216fb --- /dev/null +++ b/docs/features/networking.mdx @@ -0,0 +1,82 @@ +--- +title: Networking +description: Network access for sandboxed code. +icon: "globe" +--- + +Network access is deny-by-default. Enable it by setting `useDefaultNetwork: true` on the system driver and granting the `network` permission. + +## Quick setup + + + + ```ts + import { createNodeDriver, allowAllNetwork } from "secure-exec"; + + const driver = createNodeDriver({ + useDefaultNetwork: true, + permissions: { network: allowAllNetwork }, + }); + ``` + + The Node adapter supports `fetch`, DNS lookups, and low-level HTTP requests. + + + ```ts + import { createBrowserDriver } from "secure-exec/browser"; + + const driver = await createBrowserDriver({ + useDefaultNetwork: true, + }); + ``` + + The browser adapter supports `fetch` only. DNS and low-level HTTP are not available. + + + +## Network adapters + +You can provide a custom adapter instead of using `useDefaultNetwork`: + +```ts +import { createNodeDriver, createDefaultNetworkAdapter } from "secure-exec"; + +const driver = createNodeDriver({ + networkAdapter: createDefaultNetworkAdapter(), + permissions: { network: true }, +}); +``` + +| Factory | Environment | Capabilities | +|---|---|---| +| `createDefaultNetworkAdapter()` | Node | fetch, DNS, HTTP | +| `createBrowserNetworkAdapter()` | Browser | fetch only | + +## NetworkAdapter interface + +| Method | Returns | Description | +|---|---|---| +| `fetch(url, options?)` | `Promise` | HTTP fetch | +| `dnsLookup(hostname)` | `Promise` | DNS resolution | +| `httpRequest(url, options?)` | `Promise` | Low-level HTTP request | +| `httpServerListen?(options)` | `Promise<{ address }>` | Start a loopback HTTP server | +| `httpServerClose?(serverId)` | `Promise` | Close a loopback HTTP server | + +## Permission gating + +Use a function to filter requests: + +```ts +const driver = createNodeDriver({ + useDefaultNetwork: true, + permissions: { + network: (req) => { + // Block internal services + if (req.hostname.endsWith(".internal")) return false; + // Block metadata endpoints + if (req.hostname === "169.254.169.254") return false; + return true; + }, + }, +}); +``` diff --git a/docs/features/output-capture.mdx b/docs/features/output-capture.mdx new file mode 100644 index 000000000..f04441792 --- /dev/null +++ b/docs/features/output-capture.mdx @@ -0,0 +1,79 @@ +--- +title: Output Capture +description: Capture console output from sandboxed code. +icon: "message-lines" +--- + +Console output from sandboxed code is **not buffered** into result fields. `exec()` and `run()` do not return `stdout` or `stderr`. Use the `onStdio` hook to capture output. + +## Basic capture + +```ts +const logs: string[] = []; + +await runtime.exec("console.log('hello'); console.error('oops')", { + onStdio: (event) => logs.push(`[${event.channel}] ${event.message}`), +}); + +// logs: ["[stdout] hello", "[stderr] oops"] +``` + +## Default hook + +Set a runtime-level hook that applies to all executions: + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + onStdio: (event) => console.log(event.message), +}); +``` + +Per-execution hooks override the default. + +## StdioHook type + +```ts +type StdioHook = (event: { + channel: "stdout" | "stderr"; + message: string; +}) => void; +``` + +## Patterns + +**Collect to array:** + +```ts +const output: string[] = []; +await runtime.exec(code, { + onStdio: (e) => output.push(e.message), +}); +``` + +**Stream to logger:** + +```ts +await runtime.exec(code, { + onStdio: (e) => { + if (e.channel === "stderr") logger.warn(e.message); + else logger.info(e.message); + }, +}); +``` + +**Filter by channel:** + +```ts +const errors: string[] = []; +await runtime.exec(code, { + onStdio: (e) => { + if (e.channel === "stderr") errors.push(e.message); + }, +}); +``` + +## Why no buffering? + +Buffering console output by default would let untrusted code grow host memory without bound by logging at high volume. The explicit `onStdio` hook puts you in control of how output is stored and when to stop collecting. diff --git a/docs/features/permissions.mdx b/docs/features/permissions.mdx new file mode 100644 index 000000000..f48628aea --- /dev/null +++ b/docs/features/permissions.mdx @@ -0,0 +1,107 @@ +--- +title: Permissions +description: Control what sandboxed code can access on the host. +icon: "lock" +--- + +All host capabilities are **deny-by-default**. Sandboxed code cannot access the filesystem, network, child processes, or environment variables unless you explicitly allow it. + +## Permission helpers + +Quick presets for common configurations: + +```ts +import { + createNodeDriver, + allowAll, + allowAllFs, + allowAllNetwork, + allowAllChildProcess, + allowAllEnv, +} from "secure-exec"; + +// Allow everything +const driver = createNodeDriver({ permissions: allowAll }); + +// Allow only filesystem and network +const selective = createNodeDriver({ + permissions: { + fs: allowAllFs, + network: allowAllNetwork, + }, +}); +``` + +| Export | Allows | +|---|---| +| `allowAll` | All operations across all domains | +| `allowAllFs` | All filesystem reads and writes | +| `allowAllNetwork` | All network requests and DNS lookups | +| `allowAllChildProcess` | All child process spawning | +| `allowAllEnv` | All environment variable access | + +## Function-based checks + +Each permission field accepts a boolean or a function that inspects the request: + +```ts +const driver = createNodeDriver({ + permissions: { + // Allow reads, block writes + fs: (req) => req.operation === "read", + + // Block internal hosts + network: (req) => !req.hostname.endsWith(".internal"), + + // Only allow specific commands + childProcess: (req) => ["node", "python3"].includes(req.command), + + // Allow specific env vars + env: (req) => ["PATH", "HOME"].includes(req.name), + }, +}); +``` + +## Permissions type + +```ts +type Permissions = { + fs?: PermissionCheck; + network?: PermissionCheck; + childProcess?: PermissionCheck; + env?: PermissionCheck; +}; + +// Each field accepts a boolean or async function +type PermissionCheck = boolean | ((request: T) => boolean | Promise); +``` + +## Full example + +```ts +import { + NodeRuntime, + createNodeDriver, + createNodeRuntimeDriverFactory, + allowAllFs, +} from "secure-exec"; + +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver({ + useDefaultNetwork: true, + permissions: { + fs: allowAllFs, + network: (req) => req.hostname !== "localhost", + childProcess: false, + }, + }), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), +}); + +await runtime.exec(` + const fs = require('fs'); + fs.writeFileSync('/tmp/test.txt', 'hello'); // allowed + await fetch('https://example.com'); // allowed + await fetch('http://localhost:8080'); // blocked +`); +``` diff --git a/docs/features/resource-limits.mdx b/docs/features/resource-limits.mdx new file mode 100644 index 000000000..4d84c48bf --- /dev/null +++ b/docs/features/resource-limits.mdx @@ -0,0 +1,89 @@ +--- +title: Resource Limits +description: CPU time, memory, and payload size controls. +icon: "gauge" +--- + +Resource limits prevent sandboxed code from running forever or exhausting host memory. + +## CPU time limit + +Set a CPU time budget in milliseconds. When exceeded, the execution exits with code `124`. + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + cpuTimeLimitMs: 5000, // 5 seconds +}); + +const result = await runtime.exec("while (true) {}"); +console.log(result.code); // 124 +console.log(result.errorMessage); // "CPU time limit exceeded" +``` + +You can also override per execution: + +```ts +await runtime.exec("while (true) {}", { + cpuTimeLimitMs: 1000, // tighter limit for this call +}); +``` + +## Memory limit + +Cap isolate memory in MB. Default is `128`. + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + memoryLimit: 64, // 64 MB +}); +``` + +Memory limits apply to Node runtimes only. Python runtimes run in a thread and do not support isolate-level memory caps. + +## Payload limits + +The bridge enforces size limits on data crossing the isolate boundary. Oversized payloads are rejected with `ERR_SANDBOX_PAYLOAD_TOO_LARGE` instead of exhausting host memory. + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + payloadLimits: { + base64TransferBytes: 10 * 1024 * 1024, // 10 MB for file transfers + jsonPayloadBytes: 5 * 1024 * 1024, // 5 MB for JSON payloads + }, +}); +``` + +Hosts can tune these limits within bounded safe ranges but cannot disable enforcement. + +## Timing mitigation + +High-resolution timers are frozen by default to mitigate timing side-channel attacks. + +```ts +// Default: frozen timers +const secure = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + timingMitigation: "freeze", // default +}); + +// Opt out for Node-compatible advancing clocks +const compatible = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + timingMitigation: "off", +}); +``` + +In `"freeze"` mode: +- `Date.now()` and `performance.now()` return frozen values within an execution +- `process.hrtime()` and `process.uptime()` follow the hardened path +- `SharedArrayBuffer` is unavailable + +See the [Security Model](/security-model) for more on timing hardening and trust boundaries. diff --git a/docs/features/typescript.mdx b/docs/features/typescript.mdx new file mode 100644 index 000000000..14470a3fb --- /dev/null +++ b/docs/features/typescript.mdx @@ -0,0 +1,95 @@ +--- +title: TypeScript +description: Sandboxed type checking and compilation. +icon: "code" +--- + +The `@secure-exec/typescript` companion package runs the TypeScript compiler inside a sandbox for safe type checking and compilation of untrusted code. + +## Install + +```bash +pnpm add @secure-exec/typescript +``` + +## Setup + +```ts +import { createNodeDriver, createNodeRuntimeDriverFactory } from "secure-exec"; +import { createTypeScriptTools } from "@secure-exec/typescript"; + +const ts = createTypeScriptTools({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), +}); +``` + +**Options:** + +| Option | Type | Default | Description | +|---|---|---|---| +| `systemDriver` | `SystemDriver` | required | Compiler runtime capabilities and filesystem view | +| `runtimeDriverFactory` | `NodeRuntimeDriverFactory` | required | Creates the compiler sandbox | +| `memoryLimit` | `number` | `512` | Compiler isolate memory cap in MB | +| `cpuTimeLimitMs` | `number` | | Compiler CPU time budget in ms | +| `compilerSpecifier` | `string` | `"typescript"` | Module specifier for the TypeScript compiler | + +## Type-check a source string + +```ts +const result = await ts.typecheckSource({ + sourceText: "const x: number = 'hello';", +}); + +console.log(result.success); // false +console.log(result.diagnostics[0].message); +// "Type 'string' is not assignable to type 'number'." +``` + +## Type-check a project + +```ts +const result = await ts.typecheckProject({ + cwd: "/app", + configFilePath: "/app/tsconfig.json", +}); + +for (const d of result.diagnostics) { + console.log(`${d.filePath}:${d.line} ${d.message}`); +} +``` + +## Compile a source string + +```ts +const result = await ts.compileSource({ + sourceText: "const x: number = 42; export default x;", +}); + +console.log(result.outputText); +// "const x = 42; export default x;" +``` + +## Compile a project + +```ts +const result = await ts.compileProject({ + cwd: "/app", +}); + +console.log(result.emittedFiles); // ["/app/dist/index.js", ...] +console.log(result.success); // true +``` + +## Diagnostic shape + +```ts +type TypeScriptDiagnostic = { + code: number; + category: "error" | "warning" | "suggestion" | "message"; + message: string; + filePath?: string; + line?: number; + column?: number; +}; +``` diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 000000000..564c5d52e Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 000000000..cd27d6b8b --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/node-compatability.mdx b/docs/nodejs-compatibility.mdx similarity index 99% rename from docs/node-compatability.mdx rename to docs/nodejs-compatibility.mdx index e9f72b202..28280dfe7 100644 --- a/docs/node-compatability.mdx +++ b/docs/nodejs-compatibility.mdx @@ -1,6 +1,7 @@ --- -title: Node Compatibility +title: Node.js Compatibility description: Target Node.js version and standard-library compatibility matrix for secure-exec. +icon: "list-check" --- ## Target Node Version diff --git a/docs/python-compatibility.mdx b/docs/python-compatibility.mdx new file mode 100644 index 000000000..110b05c0e --- /dev/null +++ b/docs/python-compatibility.mdx @@ -0,0 +1,132 @@ +--- +title: Python Compatibility +description: Python standard-library and runtime compatibility matrix for secure-exec. +icon: "list-check" +--- + +## Runtime + +`PythonRuntime` uses **Pyodide 0.28.3** (CPython compiled to WebAssembly via Emscripten) running in a Node.js Worker thread. + +## Support Tiers + +| Tier | Label | Meaning | +| --- | --- | --- | +| 1 | Bridge | Implemented via worker-to-host RPC through the system driver. | +| 2 | Pyodide built-in | Available through Pyodide's bundled standard library. | +| 3 | Blocked | Intentionally disabled; deterministic error on use. | + +## Compatibility Matrix + +### Bridge capabilities (Tier 1) + +These features use the same permission-gated system driver as the Node runtime. + +| Capability | API | Notes | +| --- | --- | --- | +| File read | `secure_exec.read_text_file(path)` | Requires `permissions.fs`. Returns `EACCES` if denied, `ENOSYS` if no filesystem adapter. | +| File write | `open(path, "w")` | Requires `permissions.fs`. | +| Network | `secure_exec.fetch(url, options)` | Requires `permissions.network`. Returns `EACCES` if denied, `ENOSYS` if no network adapter. | +| Environment | `os.environ` | Filtered by `permissions.env`. Only allowed keys are visible. | +| Working directory | `os.getcwd()` / `os.chdir()` | Per-execution `cwd` override supported. | +| stdio | `print()` / `input()` | `print()` streams through `onStdio` hook. `input()` reads from `stdin` option. | + +### Standard library (Tier 2) + +These modules are available through Pyodide's bundled CPython standard library. This is not an exhaustive list; most pure-Python stdlib modules work. + +| Module | Status | +| --- | --- | +| `json` | Full support. | +| `os` | Partial. `os.environ`, `os.getcwd()`, `os.chdir()`, `os.makedirs()`, `os.path.*` work. Subprocess and signal APIs are unavailable (Emscripten limitation). | +| `sys` | Full support. `sys.modules` persists across warm executions. | +| `math` | Full support. | +| `re` | Full support. | +| `datetime` | Full support. | +| `collections` | Full support. | +| `itertools` | Full support. | +| `functools` | Full support. | +| `typing` | Full support. | +| `io` | Full support. | +| `hashlib` | Full support. | +| `base64` | Full support. | +| `struct` | Full support. | +| `dataclasses` | Full support. | +| `enum` | Full support. | +| `abc` | Full support. | + +### Blocked (Tier 3) + +| Feature | Error | +| --- | --- | +| `micropip` / package installation | `ERR_PYTHON_PACKAGE_INSTALL_UNSUPPORTED: Python package installation is not supported in this runtime` | +| `loadPackagesFromImports` | Same as above. | +| `loadPackage` | Same as above. | +| Subprocess spawning | `secure_exec.spawn` is not exposed. | + +Package installation keywords are detected before execution and fail deterministically. + +## Execution Model + +### Warm state + +The Python interpreter stays alive per `PythonRuntime` instance. Consecutive `exec()` and `run()` calls share module globals and state: + +```python +# First call +import sys +sys.modules[__name__].counter = 1 + +# Second call +import sys +print(sys.modules[__name__].counter) # 1 +``` + +To reset state, dispose and recreate the runtime. + +### Return values + +`run()` returns structured results with `value` and `globals`: + +```ts +const result = await runtime.run<{ value: number; globals: Record }>( + "x = 2 + 2", + { globals: ["x"] } +); +console.log(result.globals?.x); // 4 +``` + +### Serialization limits + +Values crossing the bridge are serialized with these caps: + +| Limit | Value | +| --- | --- | +| Object depth | 8 levels | +| Array elements | 1024 | +| Object properties | 1024 | +| Total payload | 4 MB | +| Circular references | Converted to `[Circular]` | + +## Differences from Node Runtime + +| | Node | Python | +| --- | --- | --- | +| Isolation | V8 isolate (`isolated-vm`) | Pyodide in Worker thread | +| `memoryLimit` | Configurable | Not available | +| `timingMitigation` | `"freeze"` / `"off"` | Not available | +| Return shape | `exports` (ESM default export) | `value` + `globals` | +| Module system | CJS / ESM | Python imports | +| Package installation | Host `node_modules` overlay | Blocked by design | +| Subprocess | `child_process` (permission-gated) | Not available | +| HTTP server | `http.createServer` (bridge) | Not available | +| State model | Fresh per execution | Warm (persistent across calls) | + +## Timeout Behavior + +Same contract as Node runtime: + +- `cpuTimeLimitMs` sets the CPU budget per execution +- On timeout: `code: 124`, `errorMessage: "CPU time limit exceeded"` +- Worker restarts after timeout for deterministic recovery +- Subsequent executions work normally after a timeout diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 1b0ec1ed6..c1060b27f 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -1,85 +1,97 @@ --- title: Quickstart description: Get secure-exec running in a few minutes. +icon: "rocket" --- -## Install - -```bash -pnpm add secure-exec -``` + + + ```bash + pnpm add secure-exec + ``` + + + + A runtime needs a **system driver** (host capabilities) and a **runtime driver** (sandbox execution). + + + + ```ts + import { + NodeRuntime, + createNodeDriver, + createNodeRuntimeDriverFactory, + } from "secure-exec"; + + const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + }); + ``` + + + ```ts + import { + NodeRuntime, + createBrowserDriver, + createBrowserRuntimeDriverFactory, + } from "secure-exec/browser"; + + const runtime = new NodeRuntime({ + systemDriver: await createBrowserDriver({ filesystem: "memory" }), + runtimeDriverFactory: createBrowserRuntimeDriverFactory(), + }); + ``` + + + + + + Use `exec()` to run code for side effects. Console output streams through the `onStdio` hook. -Optional TypeScript tooling: + ```ts + const logs: string[] = []; -```bash -pnpm add @secure-exec/typescript -``` + const result = await runtime.exec( + "const value = 'hello from secure-exec'; console.log(value)", + { + onStdio: (event) => logs.push(`[${event.channel}] ${event.message}`), + } + ); -## Create a runtime + console.log(logs.join("\n")); + // [stdout] hello from secure-exec + ``` + -A runtime needs two things: a **system driver** that provides host capabilities (filesystem, network, permissions) and a **runtime driver** that manages the sandboxed execution environment. + + Use `run()` to execute code and get an exported value back. - - ```ts - import { - NodeRuntime, - createNodeDriver, - createNodeRuntimeDriverFactory, - } from "secure-exec"; - - const runtime = new NodeRuntime({ - systemDriver: createNodeDriver(), - runtimeDriverFactory: createNodeRuntimeDriverFactory(), - }); + const result = await runtime.run<{ default: number }>( + "export default 2 + 2" + ); + console.log(result.exports?.default); // 4 ``` - - - ```ts - import { - NodeRuntime, - createBrowserDriver, - createBrowserRuntimeDriverFactory, - } from "secure-exec/browser"; + - const runtime = new NodeRuntime({ - systemDriver: await createBrowserDriver({ filesystem: "memory" }), - runtimeDriverFactory: createBrowserRuntimeDriverFactory(), - }); + + ```ts + runtime.dispose(); ``` - - - -## Execute code - -Use `exec()` to run code in the sandbox. Console output is streamed through the `onStdio` hook. - -```ts -const logs: string[] = []; - -const result = await runtime.exec("const value = 'hello from secure-exec'; console.log(value)", { - onStdio: (event) => logs.push(`[${event.channel}] ${event.message}`), -}); - -console.log(logs.join("\n")); -// [stdout] hello from secure-exec -``` - -## Return a value - -Use `run()` to execute code and get an exported value back. - -```ts -const result = await runtime.run<{ default: number }>("export default 2 + 2"); -console.log(result.exports?.default); // 4 -``` - -If you want sandboxed TypeScript type checking or compilation before execution, use the separate `@secure-exec/typescript` package and pass its emitted JavaScript into `secure-exec`. - -## Clean up - -Dispose the runtime when you're done to free resources. - -```ts -runtime.dispose(); -``` + + + +## Next steps + + + + Full tour of the API surface and core concepts. + + + Control filesystem, network, and process access. + + + Trust boundaries, timing hardening, and resource limits. + + diff --git a/docs/runtimes/node.mdx b/docs/runtimes/node.mdx index 607a5edd5..61502353c 100644 --- a/docs/runtimes/node.mdx +++ b/docs/runtimes/node.mdx @@ -1,6 +1,7 @@ --- title: Node Runtime description: Sandboxed JavaScript execution in a V8 isolate. +icon: "js" --- `NodeRuntime` runs JavaScript code in an isolated V8 isolate. The sandbox supports memory limits, CPU time budgets, and timing side-channel mitigation. diff --git a/docs/runtimes/overview.mdx b/docs/runtimes/overview.mdx index 408f3ae49..1b6b0e88f 100644 --- a/docs/runtimes/overview.mdx +++ b/docs/runtimes/overview.mdx @@ -1,14 +1,15 @@ --- title: Runtimes description: Sandboxed execution environments for running untrusted code. +icon: "microchip" --- A **runtime** is a sandboxed execution environment. You give it code, it runs that code in isolation, and returns the result. Runtimes enforce resource limits (CPU time, memory) and prevent untrusted code from accessing the host system directly. Every runtime exposes two methods for executing code: -- **`exec(code)`** — run code for its side effects (console output, file writes). Returns an exit code. -- **`run(code)`** — run code and get a value back (the default export in Node, or an evaluated expression in Python). +- **`exec(code)`**: run code for its side effects (console output, file writes). Returns an exit code. +- **`run(code)`**: run code and get a value back (the default export in Node, or an evaluated expression in Python). Console output is not buffered by default. Use the `onStdio` hook to capture it. diff --git a/docs/runtimes/python.mdx b/docs/runtimes/python.mdx index 8785782d6..8ef868a99 100644 --- a/docs/runtimes/python.mdx +++ b/docs/runtimes/python.mdx @@ -1,6 +1,7 @@ --- title: Python Runtime description: Sandboxed Python execution via Pyodide. +icon: "python" --- `PythonRuntime` runs Python code in a Pyodide environment inside a Worker thread. It supports CPU time budgets and bidirectional data exchange through globals. @@ -43,7 +44,7 @@ console.log(result.globals); // { x: 42 } ## Capturing output -Same pattern as Node — use the `onStdio` hook. +Same pattern as Node. Use the `onStdio` hook. ```ts const logs: string[] = []; @@ -54,7 +55,7 @@ await runtime.exec("print('hello')", { ## Differences from Node runtime -- No `memoryLimit` — Pyodide runs in a Worker thread, not a V8 isolate -- No `timingMitigation` — not applicable to the Pyodide environment +- No `memoryLimit`: Pyodide runs in a Worker thread, not a V8 isolate +- No `timingMitigation`: not applicable to the Pyodide environment - `run()` returns `value` and `globals` instead of `exports` - Globals exchange lets you pass data into and out of the Python environment diff --git a/docs/sdk-overview.mdx b/docs/sdk-overview.mdx new file mode 100644 index 000000000..95881ee47 --- /dev/null +++ b/docs/sdk-overview.mdx @@ -0,0 +1,219 @@ +--- +title: SDK Overview +description: Guided tour of the secure-exec API surface. +icon: "compass" +--- + +## Install + + + + ```bash + pnpm add secure-exec + ``` + + + ```bash + npm install secure-exec + ``` + + + ```bash + bun add secure-exec + ``` + + + +## Core concepts + +Every sandbox has two layers: + +- **System driver** provides host capabilities: filesystem, network, child processes, and permissions. +- **Runtime driver** manages the isolated execution environment (V8 isolate on Node, Web Worker in browser). + +All host capabilities are deny-by-default. You opt in to what sandboxed code can access. + +## Create a runtime + + + + ```ts + import { + NodeRuntime, + createNodeDriver, + createNodeRuntimeDriverFactory, + } from "secure-exec"; + + const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + }); + ``` + + + ```ts + import { + NodeRuntime, + createBrowserDriver, + createBrowserRuntimeDriverFactory, + } from "secure-exec/browser"; + + const runtime = new NodeRuntime({ + systemDriver: await createBrowserDriver({ filesystem: "memory" }), + runtimeDriverFactory: createBrowserRuntimeDriverFactory(), + }); + ``` + + + +## Execute code + +Two methods for running sandboxed code: + +```ts +// exec() runs code for side effects, returns an exit code +const execResult = await runtime.exec("console.log('hello')"); +console.log(execResult.code); // 0 + +// run() runs code and returns the default export +const runResult = await runtime.run<{ default: number }>( + "export default 2 + 2" +); +console.log(runResult.exports?.default); // 4 +``` + +## Capture output + +Console output is not buffered by default. Use the `onStdio` hook to capture it: + +```ts +const logs: string[] = []; + +await runtime.exec("console.log('hello'); console.error('oops')", { + onStdio: (event) => logs.push(`[${event.channel}] ${event.message}`), +}); + +// logs: ["[stdout] hello", "[stderr] oops"] +``` + +You can also set a default hook on the runtime: + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + onStdio: (event) => console.log(event.message), +}); +``` + +See [Output Capture](/features/output-capture) for more patterns. + +## Permissions + +All capabilities are blocked unless you opt in: + +```ts +import { createNodeDriver, allowAll, allowAllFs } from "secure-exec"; + +// Allow everything +const driver = createNodeDriver({ permissions: allowAll }); + +// Allow only filesystem +const fsOnly = createNodeDriver({ permissions: { fs: allowAllFs } }); + +// Custom check +const filtered = createNodeDriver({ + permissions: { + network: (req) => req.hostname !== "internal.corp", + }, +}); +``` + +See [Permissions](/features/permissions) for the full API. + +## Resource limits + +Prevent runaway execution with CPU and memory bounds: + +```ts +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + memoryLimit: 64, // MB + cpuTimeLimitMs: 5000, +}); +``` + +See [Resource Limits](/features/resource-limits) for payload limits and per-execution overrides. + +## Filesystem + +```ts +import { createNodeDriver, allowAllFs } from "secure-exec"; + +const driver = createNodeDriver({ + permissions: { fs: allowAllFs }, +}); +``` + +Three filesystem backends are available: Node.js host fs, in-memory, and OPFS (browser). See [Filesystem](/features/filesystem). + +## Networking + +```ts +import { createNodeDriver, allowAllNetwork } from "secure-exec"; + +const driver = createNodeDriver({ + useDefaultNetwork: true, + permissions: { network: allowAllNetwork }, +}); +``` + +See [Networking](/features/networking). + +## TypeScript + +Optional companion package for sandboxed type checking and compilation: + +```bash +pnpm add @secure-exec/typescript +``` + +```ts +import { createTypeScriptTools } from "@secure-exec/typescript"; + +const ts = createTypeScriptTools({ + systemDriver: createNodeDriver(), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), +}); + +const result = await ts.typecheckSource({ + sourceText: "const x: number = 'hello';", +}); +console.log(result.diagnostics); +``` + +See [TypeScript](/features/typescript). + +## Clean up + +```ts +runtime.dispose(); +``` + +## Next steps + + + + Node and Python runtime details. + + + Configure host capabilities per environment. + + + Trust boundaries, timing hardening, and isolation guarantees. + + + Complete type and method reference. + + diff --git a/docs/security-model.mdx b/docs/security-model.mdx index 364438d5d..e093f71de 100644 --- a/docs/security-model.mdx +++ b/docs/security-model.mdx @@ -1,18 +1,19 @@ --- title: Security Model description: How secure-exec isolates untrusted code, what it protects against, and what you're responsible for. +icon: "shield" --- -`secure-exec` runs untrusted code in isolate-based sandboxes — the same core pattern used by **modern browsers and Cloudflare Workers**. Host access is blocked by default and only available through explicitly configured capabilities. +`secure-exec` runs untrusted code in isolate-based sandboxes, the same core pattern used by **modern browsers and Cloudflare Workers**. Host access is blocked by default and only available through explicitly configured capabilities. ## Runtime Guarantees When you run untrusted code through `secure-exec`, the runtime enforces: -- **Isolate containment** — code runs inside an isolate with no direct access to host APIs. -- **Capability gating** — filesystem, network, process spawn, and environment access are all blocked unless you explicitly allow them. -- **Timing hardening** — high-resolution timers are frozen by default to mitigate timing side channels. -- **Resource limits** — CPU time and memory are bounded so user code can't run forever or exhaust the host. +- **Isolate containment**: code runs inside an isolate with no direct access to host APIs. +- **Capability gating**: filesystem, network, process spawn, and environment access are all blocked unless you explicitly allow them. +- **Timing hardening**: high-resolution timers are frozen by default to mitigate timing side channels. +- **Resource limits**: CPU time and memory are bounded so user code can't run forever or exhaust the host. ## Trust Boundaries @@ -65,7 +66,7 @@ In default `"freeze"` mode (`timingMitigation: "freeze"`): - `Date.now()` and `performance.now()` return frozen values within an execution. - `process.hrtime()`, `process.hrtime.bigint()`, and `process.uptime()` follow the hardened path. -- `SharedArrayBuffer` is unavailable — shared memory between threads can be used to [build high-resolution timers](https://v8.dev/blog/spectre) that [bypass frozen clocks](https://security.googleblog.com/2021/03/a-spectre-proof-of-concept-for-spectre.html). +- `SharedArrayBuffer` is unavailable. Shared memory between threads can be used to [build high-resolution timers](https://v8.dev/blog/spectre) that [bypass frozen clocks](https://security.googleblog.com/2021/03/a-spectre-proof-of-concept-for-spectre.html). Setting `timingMitigation: "off"` gives you normal advancing clocks but weaker side-channel protection. @@ -73,8 +74,8 @@ Setting `timingMitigation: "off"` gives you normal advancing clocks but weaker s Two controls prevent runaway execution: -- **`cpuTimeLimitMs`** — CPU time budget for the runtime. When exceeded, the process exits with code `124` and `errorMessage` includes `CPU time limit exceeded`. -- **`memoryLimit`** — Isolate memory cap in MB (default `128`). +- **`cpuTimeLimitMs`**: CPU time budget for the runtime. When exceeded, the process exits with code `124` and `errorMessage` includes `CPU time limit exceeded`. +- **`memoryLimit`**: Isolate memory cap in MB (default `128`). The bridge also enforces isolate-boundary payload limits so oversized base64 file transfers and oversized isolate-originated JSON payloads are rejected deterministically with `ERR_SANDBOX_PAYLOAD_TOO_LARGE` instead of exhausting host memory. Hosts can tune these limits within bounded safe ranges, but cannot disable enforcement. diff --git a/docs/system-drivers/browser.mdx b/docs/system-drivers/browser.mdx index d7d1d17ee..2036b893c 100644 --- a/docs/system-drivers/browser.mdx +++ b/docs/system-drivers/browser.mdx @@ -1,6 +1,7 @@ --- title: Browser System Driver description: Browser-compatible system driver with OPFS and fetch-based networking. +icon: "globe" --- The browser system driver provides sandboxed runtimes with filesystem and network capabilities that work in browser environments. It uses OPFS or an in-memory filesystem and fetch-based networking. @@ -57,7 +58,7 @@ const runtimeDriver = createBrowserRuntimeDriverFactory({ ## Differences from Node driver -- **Async creation** — `createBrowserDriver` returns a `Promise` -- **No child processes** — `CommandExecutor` is not available -- **No DNS** — only fetch-based networking, no `dnsLookup` -- **OPFS limitations** — atomic rename is not supported +- **Async creation**: `createBrowserDriver` returns a `Promise` +- **No child processes**: `CommandExecutor` is not available +- **No DNS**: only fetch-based networking, no `dnsLookup` +- **OPFS limitations**: atomic rename is not supported diff --git a/docs/system-drivers/node.mdx b/docs/system-drivers/node.mdx index a5241f897..660ea8f1f 100644 --- a/docs/system-drivers/node.mdx +++ b/docs/system-drivers/node.mdx @@ -1,9 +1,10 @@ --- title: Node System Driver description: Server-side system driver with full host capabilities. +icon: "node-js" --- -The Node system driver provides sandboxed runtimes with access to the host filesystem, networking, child processes, and environment — all behind a permission layer. +The Node system driver provides sandboxed runtimes with access to the host filesystem, networking, child processes, and environment. All capabilities sit behind a permission layer. ## Basic setup diff --git a/docs/system-drivers/overview.mdx b/docs/system-drivers/overview.mdx index 4437bb20f..b55e3eb2e 100644 --- a/docs/system-drivers/overview.mdx +++ b/docs/system-drivers/overview.mdx @@ -1,6 +1,7 @@ --- title: System Drivers description: Host capabilities provided to sandboxed runtimes. +icon: "server" --- A **system driver** defines what host capabilities a sandboxed runtime can access. It controls the filesystem, network, child process execution, and permission boundaries available to untrusted code. diff --git a/justfile b/justfile index c8e5ffc90..1aa782652 100644 --- a/justfile +++ b/justfile @@ -1,2 +1,11 @@ -docs: +dev-docs: cd docs && npx mintlify dev + +dev-website: + pnpm --filter @secure-exec/website dev + +build-website: + pnpm --filter @secure-exec/website build + +open-docs: + open https://secure-exec.rivet.gg diff --git a/packages/website/.astro/content-assets.mjs b/packages/website/.astro/content-assets.mjs new file mode 100644 index 000000000..2b8b8234b --- /dev/null +++ b/packages/website/.astro/content-assets.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/packages/website/.astro/content-modules.mjs b/packages/website/.astro/content-modules.mjs new file mode 100644 index 000000000..2b8b8234b --- /dev/null +++ b/packages/website/.astro/content-modules.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/packages/website/.astro/content.d.ts b/packages/website/.astro/content.d.ts new file mode 100644 index 000000000..c0082cc81 --- /dev/null +++ b/packages/website/.astro/content.d.ts @@ -0,0 +1,199 @@ +declare module 'astro:content' { + export interface RenderResult { + Content: import('astro/runtime/server/index.js').AstroComponentFactory; + headings: import('astro').MarkdownHeading[]; + remarkPluginFrontmatter: Record; + } + interface Render { + '.md': Promise; + } + + export interface RenderedContent { + html: string; + metadata?: { + imagePaths: Array; + [key: string]: unknown; + }; + } +} + +declare module 'astro:content' { + type Flatten = T extends { [K: string]: infer U } ? U : never; + + export type CollectionKey = keyof AnyEntryMap; + export type CollectionEntry = Flatten; + + export type ContentCollectionKey = keyof ContentEntryMap; + export type DataCollectionKey = keyof DataEntryMap; + + type AllValuesOf = T extends any ? T[keyof T] : never; + type ValidContentEntrySlug = AllValuesOf< + ContentEntryMap[C] + >['slug']; + + export type ReferenceDataEntry< + C extends CollectionKey, + E extends keyof DataEntryMap[C] = string, + > = { + collection: C; + id: E; + }; + export type ReferenceContentEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}) = string, + > = { + collection: C; + slug: E; + }; + export type ReferenceLiveEntry = { + collection: C; + id: string; + }; + + /** @deprecated Use `getEntry` instead. */ + export function getEntryBySlug< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + // Note that this has to accept a regular string too, for SSR + entrySlug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + + /** @deprecated Use `getEntry` instead. */ + export function getDataEntryById( + collection: C, + entryId: E, + ): Promise>; + + export function getCollection>( + collection: C, + filter?: (entry: CollectionEntry) => entry is E, + ): Promise; + export function getCollection( + collection: C, + filter?: (entry: CollectionEntry) => unknown, + ): Promise[]>; + + export function getLiveCollection( + collection: C, + filter?: LiveLoaderCollectionFilterType, + ): Promise< + import('astro').LiveDataCollectionResult, LiveLoaderErrorType> + >; + + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + entry: ReferenceContentEntry, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + entry: ReferenceDataEntry, + ): E extends keyof DataEntryMap[C] + ? Promise + : Promise | undefined>; + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + slug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + collection: C, + id: E, + ): E extends keyof DataEntryMap[C] + ? string extends keyof DataEntryMap[C] + ? Promise | undefined + : Promise + : Promise | undefined>; + export function getLiveEntry( + collection: C, + filter: string | LiveLoaderEntryFilterType, + ): Promise, LiveLoaderErrorType>>; + + /** Resolve an array of entry references from the same collection */ + export function getEntries( + entries: ReferenceContentEntry>[], + ): Promise[]>; + export function getEntries( + entries: ReferenceDataEntry[], + ): Promise[]>; + + export function render( + entry: AnyEntryMap[C][string], + ): Promise; + + export function reference( + collection: C, + ): import('astro/zod').ZodEffects< + import('astro/zod').ZodString, + C extends keyof ContentEntryMap + ? ReferenceContentEntry> + : ReferenceDataEntry + >; + // Allow generic `string` to avoid excessive type errors in the config + // if `dev` is not running to update as you edit. + // Invalid collection names will be caught at build time. + export function reference( + collection: C, + ): import('astro/zod').ZodEffects; + + type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; + type InferEntrySchema = import('astro/zod').infer< + ReturnTypeOrOriginal['schema']> + >; + + type ContentEntryMap = { + + }; + + type DataEntryMap = { + + }; + + type AnyEntryMap = ContentEntryMap & DataEntryMap; + + type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< + infer TData, + infer TEntryFilter, + infer TCollectionFilter, + infer TError + > + ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } + : { data: never; entryFilter: never; collectionFilter: never; error: never }; + type ExtractDataType = ExtractLoaderTypes['data']; + type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; + type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; + type ExtractErrorType = ExtractLoaderTypes['error']; + + type LiveLoaderDataType = + LiveContentConfig['collections'][C]['schema'] extends undefined + ? ExtractDataType + : import('astro/zod').infer< + Exclude + >; + type LiveLoaderEntryFilterType = + ExtractEntryFilterType; + type LiveLoaderCollectionFilterType = + ExtractCollectionFilterType; + type LiveLoaderErrorType = ExtractErrorType< + LiveContentConfig['collections'][C]['loader'] + >; + + export type ContentConfig = typeof import("../src/content.config.mjs"); + export type LiveContentConfig = never; +} diff --git a/packages/website/.astro/data-store.json b/packages/website/.astro/data-store.json new file mode 100644 index 000000000..8188dca82 --- /dev/null +++ b/packages/website/.astro/data-store.json @@ -0,0 +1 @@ +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.18.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://secure-exec.dev\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] \ No newline at end of file diff --git a/packages/website/.astro/settings.json b/packages/website/.astro/settings.json new file mode 100644 index 000000000..67720613a --- /dev/null +++ b/packages/website/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "_variables": { + "lastUpdateCheck": 1773641384389 + } +} \ No newline at end of file diff --git a/packages/website/.astro/types.d.ts b/packages/website/.astro/types.d.ts new file mode 100644 index 000000000..f964fe0cf --- /dev/null +++ b/packages/website/.astro/types.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/website/astro.config.mjs b/packages/website/astro.config.mjs new file mode 100644 index 000000000..50d1f7a34 --- /dev/null +++ b/packages/website/astro.config.mjs @@ -0,0 +1,10 @@ +import { defineConfig } from "astro/config"; +import react from "@astrojs/react"; +import tailwind from "@astrojs/tailwind"; +import sitemap from "@astrojs/sitemap"; + +export default defineConfig({ + site: "https://secure-exec.dev", + output: "static", + integrations: [react(), tailwind(), sitemap()], +}); diff --git a/packages/website/package.json b/packages/website/package.json new file mode 100644 index 000000000..223699b2b --- /dev/null +++ b/packages/website/package.json @@ -0,0 +1,30 @@ +{ + "name": "@secure-exec/website", + "private": true, + "version": "0.0.1", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/react": "^4.2.0", + "@astrojs/sitemap": "^3.2.0", + "@astrojs/tailwind": "^6.0.0", + "astro": "^5.1.0", + "framer-motion": "^12.0.0", + "lucide-react": "^0.469.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^3.4.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/website/public/favicon.svg b/packages/website/public/favicon.svg new file mode 100644 index 000000000..d7119e885 --- /dev/null +++ b/packages/website/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/website/public/hero-bg.jpg b/packages/website/public/hero-bg.jpg new file mode 100644 index 000000000..a0ae979bf Binary files /dev/null and b/packages/website/public/hero-bg.jpg differ diff --git a/packages/website/public/rivet-icon.svg b/packages/website/public/rivet-icon.svg new file mode 100644 index 000000000..287c42579 --- /dev/null +++ b/packages/website/public/rivet-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/website/public/rivet-logo-text-white.svg b/packages/website/public/rivet-logo-text-white.svg new file mode 100644 index 000000000..fcb2f05b8 --- /dev/null +++ b/packages/website/public/rivet-logo-text-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/website/public/robots.txt b/packages/website/public/robots.txt new file mode 100644 index 000000000..d2c5dd264 --- /dev/null +++ b/packages/website/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://sandbox-agent.dev/sitemap-index.xml diff --git a/packages/website/public/secure-exec-logo-long.svg b/packages/website/public/secure-exec-logo-long.svg new file mode 100644 index 000000000..cd27d6b8b --- /dev/null +++ b/packages/website/public/secure-exec-logo-long.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/public/secure-exec-logo.png b/packages/website/public/secure-exec-logo.png new file mode 100644 index 000000000..564c5d52e Binary files /dev/null and b/packages/website/public/secure-exec-logo.png differ diff --git a/packages/website/public/secure-exec-logo.svg b/packages/website/public/secure-exec-logo.svg new file mode 100644 index 000000000..d7119e885 --- /dev/null +++ b/packages/website/public/secure-exec-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/website/src/components/Benchmarks.tsx b/packages/website/src/components/Benchmarks.tsx new file mode 100644 index 000000000..b1fa80721 --- /dev/null +++ b/packages/website/src/components/Benchmarks.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { motion } from "framer-motion"; + +const metrics = [ + { + label: "Cold start", + secureExec: { value: "~10 ms", bar: 3 }, + sandbox: { value: "~300 ms", bar: 60, label: "Sandbox provider" }, + }, + { + label: "Memory per instance", + secureExec: { value: "~10 MB", bar: 8 }, + sandbox: { value: "~128 MB", bar: 100, label: "Sandbox provider" }, + }, + { + label: "Cost per GB of memory", + secureExec: { value: "~$0.10/hr", bar: 3 }, + sandbox: { value: "~$3.20/hr", bar: 100, label: "Sandbox provider" }, + }, + { + label: "Extra infrastructure", + secureExec: { value: "None", bar: 0 }, + sandbox: { value: "Cloud API + account", bar: 100, label: "Sandbox provider" }, + }, +]; + +function BarRow({ + label, + secureExec, + sandbox, +}: { + label: string; + secureExec: { value: string; bar: number }; + sandbox: { value: string; bar: number; label: string }; +}) { + return ( +
+

{label}

+
+ {/* secure-exec bar */} +
+ Secure Exec +
+ + + {secureExec.value} + +
+
+ {/* Sandbox provider bar */} +
+ {sandbox.label} +
+ + + {sandbox.value} + +
+
+
+
+ ); +} + +export function Benchmarks() { + return ( +
+
+
+ + Benchmarks + + + V8 isolates vs. container-based sandboxes. Same security guarantees, fundamentally different overhead. + +
+ + + {metrics.map((metric) => ( + + ))} + + + + Sandbox provider numbers based on published documentation and pricing. Cost comparison: secure-exec memory cost based on EC2 on-demand pricing; sandbox provider cost based on published per-sandbox pricing. Secure Exec measured on Apple M-series, Node.js 22. + +
+
+ ); +} diff --git a/packages/website/src/components/FeatureGrid.tsx b/packages/website/src/components/FeatureGrid.tsx new file mode 100644 index 000000000..5848e98fb --- /dev/null +++ b/packages/website/src/components/FeatureGrid.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Server, Package, Shield, Gauge, Globe, Cpu } from "lucide-react"; + +const features = [ + { + icon: Server, + title: "No infrastructure required", + description: + "No Docker daemon, no hypervisor, no orchestrator. Runs anywhere Node.js, Bun, or an HTML5 browser runs. Deploy to Lambda, a VPS, or a static site — your existing deployment works.", + hoverColor: "group-hover:text-blue-400", + }, + { + icon: Package, + title: "Node.js & npm compatibility", + description: + "fs, child_process, http, dns, process, os — bridged to real host capabilities, not stubbed. Run Express, Hono, Next.js, and any npm package.", + hoverColor: "group-hover:text-green-400", + link: { href: "/docs/node-compatability", label: "Compatibility matrix" }, + }, + { + icon: Shield, + title: "Deny-by-default permissions", + description: + "Filesystem, network, child processes, and env vars are all blocked unless explicitly allowed. Permissions are composable functions — grant read but not write, allow fetch but block spawn.", + hoverColor: "group-hover:text-purple-400", + }, + { + icon: Gauge, + title: "Configurable resource limits", + description: + "CPU time budgets and memory caps. Runaway code is terminated deterministically with exit code 124 — no OOM crashes, no infinite loops, no host exhaustion.", + hoverColor: "group-hover:text-amber-400", + }, + { + icon: Cpu, + title: "Fast cold starts & low memory", + description: + "No container to boot, no filesystem image to mount, no process to fork. Isolate creation is measured in milliseconds. Memory overhead is measured in single-digit megabytes.", + hoverColor: "group-hover:text-pink-400", + }, + { + icon: Globe, + title: "Powered by V8 isolates", + description: + "The same isolation primitive behind Cloudflare Workers for Platforms and every browser tab. Battle-tested at scale by the infrastructure you already trust.", + hoverColor: "group-hover:text-orange-400", + }, +]; + +export function FeatureGrid() { + return ( +
+
+
+ + Why Secure Exec + + + V8 isolate-based execution with full Node.js compatibility, granular permissions, and resource limits. + +
+ + + {features.map((feature) => ( +
+
+
+ +
+

{feature.title}

+
+

{feature.description}

+ {feature.link && ( + + {feature.link.label} → + + )} +
+ ))} +
+
+
+ ); +} diff --git a/packages/website/src/components/Footer.tsx b/packages/website/src/components/Footer.tsx new file mode 100644 index 000000000..70dbb0d60 --- /dev/null +++ b/packages/website/src/components/Footer.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { motion } from "framer-motion"; + +const footer = { + products: [ + { name: "Actors", href: "https://rivet.dev/docs/actors" }, + { name: "Secure Exec", href: "/docs" }, + ], + developers: [ + { name: "Documentation", href: "/docs" }, + { name: "Changelog", href: "https://github.com/rivet-dev/secure-exec/releases" }, + { name: "Blog", href: "https://www.rivet.dev/blog/" }, + ], + legal: [ + { name: "Terms", href: "https://rivet.dev/terms" }, + { name: "Privacy Policy", href: "https://rivet.dev/privacy" }, + { name: "Acceptable Use", href: "https://rivet.dev/acceptable-use" }, + ], + social: [ + { + name: "Discord", + href: "https://rivet.dev/discord", + icon: ( + + + + ), + }, + { + name: "GitHub", + href: "https://github.com/rivet-dev/secure-exec", + icon: ( + + + + ), + }, + { + name: "Twitter", + href: "https://x.com/rivet_dev", + icon: ( + + + + ), + }, + ], +}; + +export function Footer() { + return ( +
+
+
+ + + Rivet + +

Infrastructure for software that thinks

+
+ {footer.social.map((item) => ( + + {item.name} + {item.icon} + + ))} +
+
+ +
+ +

Products

+ +
+ + +

Developers

+ +
+ + +

Legal

+ +
+
+
+ + +

© {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved.

+
+
+
+ ); +} diff --git a/packages/website/src/components/FooterCTA.tsx b/packages/website/src/components/FooterCTA.tsx new file mode 100644 index 000000000..d715ac01c --- /dev/null +++ b/packages/website/src/components/FooterCTA.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ArrowRight } from "lucide-react"; + +export function FooterCTA() { + return ( +
+
+ +

+ Get started in minutes +

+

+ Install Secure Exec, create a runtime, and execute untrusted code. All in a few lines of TypeScript. +

+ +
+
+
+ ); +} diff --git a/packages/website/src/components/GitHubStars.tsx b/packages/website/src/components/GitHubStars.tsx new file mode 100644 index 000000000..afe86190f --- /dev/null +++ b/packages/website/src/components/GitHubStars.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface GitHubStarsProps extends React.AnchorHTMLAttributes { + repo?: string; +} + +function formatNumber(num: number): string { + if (num >= 1000) { + return `${(num / 1000).toFixed(1)}k`; + } + return num.toString(); +} + +export function GitHubStars({ repo = "rivet-dev/secure-exec", className, ...props }: GitHubStarsProps) { + const [stars, setStars] = useState(null); + + useEffect(() => { + const cacheKey = `github-stars-${repo}`; + const cachedData = sessionStorage.getItem(cacheKey); + + if (cachedData) { + const { stars: cachedStars, timestamp } = JSON.parse(cachedData); + if (Date.now() - timestamp < 5 * 60 * 1000) { + setStars(cachedStars); + return; + } + } + + fetch(`https://api.github.com/repos/${repo}`) + .then((response) => { + if (!response.ok) throw new Error("Failed to fetch"); + return response.json(); + }) + .then((data) => { + const newStars = data.stargazers_count; + setStars(newStars); + sessionStorage.setItem( + cacheKey, + JSON.stringify({ + stars: newStars, + timestamp: Date.now(), + }), + ); + }) + .catch((err) => { + console.error("Failed to fetch stars", err); + }); + }, [repo]); + + return ( + + + + + {stars ? `${formatNumber(stars)} Stars` : "GitHub"} + + ); +} diff --git a/packages/website/src/components/Hero.tsx b/packages/website/src/components/Hero.tsx new file mode 100644 index 000000000..af453c073 --- /dev/null +++ b/packages/website/src/components/Hero.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { ArrowRight, Terminal, Check } from "lucide-react"; +import { CopyButton } from "./ui/CopyButton"; + +const codeRaw = `import { generateText, tool } from "ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { NodeRuntime, createNodeDriver, createNodeRuntimeDriverFactory } from "secure-exec"; +import { z } from "zod"; + +const runtime = new NodeRuntime({ + systemDriver: createNodeDriver({ permissions: { fs: true, network: true } }), + runtimeDriverFactory: createNodeRuntimeDriverFactory(), + memoryLimit: 64, + cpuTimeLimitMs: 5000, +}); + +const result = await generateText({ + model: anthropic("claude-sonnet-4-20250514"), + tools: { + execute: tool({ + description: "Run JavaScript in a secure sandbox", + parameters: z.object({ code: z.string() }), + execute: async ({ code }) => { + const logs: string[] = []; + const res = await runtime.exec(code, { + onStdio: (e) => logs.push(e.message), + }); + return { exitCode: res.code, output: logs.join("\\n") }; + }, + }), + }, + prompt: "Calculate the first 20 fibonacci numbers", +});`; + +function CodeBlock() { + return ( +
+
+ agent.ts + +
+
+        
+          import
+          {" { "}
+          generateText
+          , 
+          tool
+          {" } "}
+          from
+           
+          "ai"
+          ;
+          {"\n"}
+          import
+          {" { "}
+          anthropic
+          {" } "}
+          from
+           
+          "@ai-sdk/anthropic"
+          ;
+          {"\n"}
+          import
+          {" { "}
+          NodeRuntime
+          , 
+          createNodeDriver
+          , 
+          createNodeRuntimeDriverFactory
+          {" } "}
+          from
+           
+          "secure-exec"
+          ;
+          {"\n"}
+          import
+          {" { "}
+          z
+          {" } "}
+          from
+           
+          "zod"
+          ;
+          {"\n\n"}
+
+          // Create a sandboxed runtime
+          {"\n"}
+          const
+           runtime = 
+          new
+           
+          NodeRuntime
+          {"({"}
+          {"\n"}
+          {"  systemDriver: "}
+          createNodeDriver
+          {"({ permissions: { fs: "}
+          true
+          {", network: "}
+          true
+          {" } }),"}
+          {"\n"}
+          {"  runtimeDriverFactory: "}
+          createNodeRuntimeDriverFactory
+          {"(),"}
+          {"\n"}
+          {"  memoryLimit: "}
+          64
+          ,
+          {"\n"}
+          {"  cpuTimeLimitMs: "}
+          5000
+          ,
+          {"\n"}
+          {"});"}
+          {"\n\n"}
+
+          // Expose as an AI SDK tool
+          {"\n"}
+          const
+           result = 
+          await
+           
+          generateText
+          {"({"}
+          {"\n"}
+          {"  model: "}
+          anthropic
+          {"("}
+          "claude-sonnet-4-20250514"
+          {"),"}
+          {"\n"}
+          {"  tools: {"}
+          {"\n"}
+          {"    execute: "}
+          tool
+          {"({"}
+          {"\n"}
+          {"      description: "}
+          "Run JavaScript in a secure sandbox"
+          ,
+          {"\n"}
+          {"      parameters: z."}
+          object
+          {"({ code: z."}
+          string
+          {"() }),"}
+          {"\n"}
+          {"      execute: "}
+          async
+          {" ({ code }) => {"}
+          {"\n"}
+          {"        "}
+          const
+          {" logs: "}
+          string
+          {"[] = [];"}
+          {"\n"}
+          {"        "}
+          const
+          {" res = "}
+          await
+          {" runtime."}
+          exec
+          {"(code, {"}
+          {"\n"}
+          {"          onStdio: (e) => logs."}
+          push
+          {"(e.message),"}
+          {"\n"}
+          {"        });"}
+          {"\n"}
+          {"        "}
+          return
+          {" { exitCode: res.code, output: logs."}
+          join
+          {"("}
+          "\\n"
+          {") };"}
+          {"\n"}
+          {"      },"}
+          {"\n"}
+          {"    }),"}
+          {"\n"}
+          {"  },"}
+          {"\n"}
+          {"  prompt: "}
+          "Calculate the first 20 fibonacci numbers"
+          ,
+          {"\n"}
+          {"});"}
+        
+      
+
+ ); +} + +const CopyInstallButton = () => { + const [copied, setCopied] = useState(false); + const installCommand = "npm install secure-exec"; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(installCommand); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + return ( + + ); +}; + +export function Hero() { + const [scrollOpacity, setScrollOpacity] = useState(1); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const updateViewportMode = () => { + const mobile = window.innerWidth < 1024; + setIsMobile(mobile); + if (mobile) { + setScrollOpacity(1); + } + }; + + const handleScroll = () => { + if (window.innerWidth < 1024) { + setScrollOpacity(1); + return; + } + const scrollY = window.scrollY; + const windowHeight = window.innerHeight; + const fadeStart = windowHeight * 0.15; + const fadeEnd = windowHeight * 0.5; + const opacity = 1 - Math.min(1, Math.max(0, (scrollY - fadeStart) / (fadeEnd - fadeStart))); + setScrollOpacity(opacity); + }; + + updateViewportMode(); + handleScroll(); + window.addEventListener("resize", updateViewportMode); + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("resize", updateViewportMode); + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + return ( + <> +
+
+
+ +
+
+ + + + + + Secure Node.js Execution +
+ Without a Sandbox +
+ + + Supports Node.js and npm packages natively. +
+ Just a library. No containers, no VMs, no external services. +
+ Powered by the same V8 isolate technology behind Cloudflare Workers and Chrome. +
+ + + + Get Started + + + + +
+
+
+ +
+
+ +

Give your AI agent a secure sandbox

+

+ Expose secure-exec as a tool with the Vercel AI SDK. Your agent can execute arbitrary code without risking your infrastructure. +

+
+ + + +
+
+ + ); +} diff --git a/packages/website/src/components/Navigation.tsx b/packages/website/src/components/Navigation.tsx new file mode 100644 index 000000000..311d7e65e --- /dev/null +++ b/packages/website/src/components/Navigation.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Menu, X } from "lucide-react"; +import { GitHubStars } from "./GitHubStars"; + +function NavItem({ href, children }: { href: string; children: React.ReactNode }) { + return ( + + {children} + + ); +} + +export function Navigation() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const [showLogo, setShowLogo] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 20); + + const heroLogo = document.getElementById("hero-logo"); + if (heroLogo) { + const rect = heroLogo.getBoundingClientRect(); + setShowLogo(rect.bottom < 0); + } else { + setShowLogo(true); + } + }; + + handleScroll(); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( +
+
+ + + {mobileMenuOpen && ( + + )} +
+ ); +} diff --git a/packages/website/src/components/ui/Badge.tsx b/packages/website/src/components/ui/Badge.tsx new file mode 100644 index 000000000..847d2fee4 --- /dev/null +++ b/packages/website/src/components/ui/Badge.tsx @@ -0,0 +1,7 @@ +interface BadgeProps { + children: React.ReactNode; +} + +export function Badge({ children }: BadgeProps) { + return {children}; +} diff --git a/packages/website/src/components/ui/Button.tsx b/packages/website/src/components/ui/Button.tsx new file mode 100644 index 000000000..591f99624 --- /dev/null +++ b/packages/website/src/components/ui/Button.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from "react"; + +interface ButtonProps { + children: ReactNode; + variant?: "primary" | "secondary" | "ghost"; + size?: "sm" | "md" | "lg"; + href?: string; + onClick?: () => void; + className?: string; +} + +export function Button({ children, variant = "primary", size = "md", href, onClick, className = "" }: ButtonProps) { + const baseStyles = "inline-flex items-center justify-center font-medium rounded-lg transition-all"; + + const variants = { + primary: "btn-chrome", + secondary: "btn-chrome-outline", + ghost: "text-zinc-400 hover:text-white", + }; + + const sizes = { + sm: "h-9 px-4 text-sm", + md: "h-12 px-8 text-sm", + lg: "h-14 px-10 text-base", + }; + + const classes = `${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`; + + if (href) { + return ( + + {children} + + ); + } + + return ( + + ); +} diff --git a/packages/website/src/components/ui/CopyButton.tsx b/packages/website/src/components/ui/CopyButton.tsx new file mode 100644 index 000000000..0fe31b313 --- /dev/null +++ b/packages/website/src/components/ui/CopyButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; +import { Copy, CheckCircle2 } from "lucide-react"; + +interface CopyButtonProps { + text: string; +} + +export function CopyButton({ text }: CopyButtonProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} diff --git a/packages/website/src/components/ui/FeatureIcon.tsx b/packages/website/src/components/ui/FeatureIcon.tsx new file mode 100644 index 000000000..b5a6ae053 --- /dev/null +++ b/packages/website/src/components/ui/FeatureIcon.tsx @@ -0,0 +1,23 @@ +import type { LucideIcon } from "lucide-react"; + +interface FeatureIconProps { + icon: LucideIcon; + color?: string; + bgColor?: string; + hoverBgColor?: string; + glowShadow?: string; +} + +export function FeatureIcon({ + icon: Icon, + color = "text-accent", + bgColor = "bg-accent/10", + hoverBgColor = "group-hover:bg-accent/20", + glowShadow = "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", +}: FeatureIconProps) { + return ( +
+ +
+ ); +} diff --git a/packages/website/src/layouts/Layout.astro b/packages/website/src/layouts/Layout.astro new file mode 100644 index 000000000..6eb5a8e1e --- /dev/null +++ b/packages/website/src/layouts/Layout.astro @@ -0,0 +1,73 @@ +--- +interface Props { + title: string; + description?: string; +} + +const { title, description = "Secure Node.js execution without a sandbox. V8 isolate-based code execution with full Node.js and npm compatibility." } = + Astro.props; +const canonicalURL = new URL(Astro.url.pathname, "https://secure-exec.dev"); + +const structuredData = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "Secure Exec", + applicationCategory: "DeveloperApplication", + operatingSystem: "Linux, macOS, Windows", + description: description, + url: "https://secure-exec.dev", + author: { + "@type": "Organization", + name: "Rivet", + url: "https://rivet.dev", + }, + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + keywords: "secure code execution, V8 isolate, Node.js sandbox, untrusted code, npm, sandboxed runtime, developer tools", +}; +--- + + + + + + + + + + + + + + + + + + + {title} + + + + + + + + + + + + + +