Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ading2210/sandstone/llms.txt

Use this file to discover all available pages before exploring further.

Sandstone ships as two bundled ES modules: sandstone.mjs for your host page and an internal frame bundle that is embedded automatically. This guide walks you through cloning the repository, building both bundles, and writing the minimal host-side code needed to proxy a URL inside a sandboxed iframe. You will have a working proxy by the end of step 6.
1

Clone the repository and build

Clone the Sandstone repository, install its dependencies, and produce the production bundles.
git clone https://github.com/ading2210/sandstone
cd sandstone
npm install
npm run build:prod
After the build completes, dist/sandstone.mjs contains the host bundle and dist/sandstone_frame.js contains the frame bundle. The host bundle inlines the frame bundle automatically, so you only need to import sandstone.mjs in your own code.
2

Import the ES module

Import the host bundle as a namespace so you have access to all public exports.
import * as sandstone from "./dist/sandstone.mjs";
The sandstone namespace exposes sandstone.controller, sandstone.network, sandstone.libcurl, and sandstone.version.
3

Create a ProxyFrame

Instantiate a ProxyFrame. The constructor creates an <iframe> element with the correct sandbox attributes and registers it internally.
const frame = new sandstone.controller.ProxyFrame();
Each ProxyFrame has its own iframe element, its own RPC channel, and its own local storage namespace. You can create multiple frames on the same page.
4

Append the iframe to the DOM

Add the frame’s <iframe> element to the page so it is visible. You can place it inside any container element.
document.body.appendChild(frame.iframe);
Style the iframe with CSS as needed. The frame starts with a dark (#222222) background while loading.
5

Set the Wisp server

Sandstone uses libcurl.js for network requests, which requires a Wisp WebSocket server. Point it at one before navigating.
sandstone.libcurl.set_websocket("wss://wisp.mercurywork.shop/");
You can also host your own Wisp server. See the Wisp server guide for details.
6

Navigate to a URL

Call navigate_to with any valid http: or https: URL. The method fetches the page through the Wisp connection, rewrites the HTML and JavaScript, and loads it into the sandboxed iframe.
await frame.navigate_to("https://example.com");
navigate_to returns a promise that resolves once the page is loaded and all inline scripts have been evaluated.

Complete minimal example

The following self-contained HTML file puts all six steps together. Save it next to the dist/ directory and open it in a browser.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Sandstone minimal example</title>
    <style>
      body { margin: 0; background: #111; }
      iframe { width: 100vw; height: 100vh; border: none; }
    </style>
  </head>
  <body>
    <script type="module">
      import * as sandstone from "./dist/sandstone.mjs";

      const frame = new sandstone.controller.ProxyFrame();
      document.body.appendChild(frame.iframe);

      sandstone.libcurl.set_websocket("wss://wisp.mercurywork.shop/");

      await frame.navigate_to("https://example.com");
    </script>
  </body>
</html>

Reacting to navigation events

ProxyFrame exposes three callback properties you can set after construction. The example frontend in example/main.mjs uses all three to keep a URL bar in sync and to fetch the page favicon:
// Called as soon as navigate_to begins — before the new page is fetched.
// Use this to show a loading indicator or clear stale UI.
frame.on_navigate = () => {
  urlBox.value = frame.url.href;
};

// Called once the page is fully loaded and scripts have run.
// frame.get_favicon() returns the URL of the page's favicon.
frame.on_load = async () => {
  urlBox.value = frame.url.href;
  let faviconUrl = await frame.get_favicon();
  if (!faviconUrl.startsWith("data:")) {
    let response = await sandstone.libcurl.fetch(faviconUrl);
    if (response.ok) {
      let blob = await response.blob();
      faviconUrl = URL.createObjectURL(blob);
    }
  }
  faviconImg.src = faviconUrl;
};

// Called when the proxied page changes the URL without a full reload
// (e.g. pushState or hash changes).
frame.on_url_change = () => {
  urlBox.value = frame.url.href;
};
For a more complete integration — including a URL bar, eval console, and options panel — see Embedding Sandstone.

Build docs developers (and LLMs) love