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 rewrites all page content — HTML elements, CSS URLs, and JavaScript source — so that every network request and every global variable access flows through the proxy layer rather than going directly to the browser. HTML is parsed with DOMParser, then each element is recursively patched to rewrite URLs and inline scripts. CSS is scanned for url() references and each resource is fetched through the network layer and replaced with a blob URL. JavaScript is parsed into an AST with Meriyah, walked with astray, and patched at the source level before evaluation. This page documents that internal machinery. If you are embedding Sandstone in your own page, you interact with none of this directly — it all happens automatically inside the sandboxed iframe.
This is an internals reference. The rewriting pipeline runs entirely inside the sandboxed iframe. You do not call any of these functions from host-page code.

JavaScript rewriting

rewrite_js

// src/frame/rewrite/css.mjs
// Returns a string synchronously if no url() references are found,
// or a Promise<string> when external resources need to be fetched.
function parse_css(css_str, css_url)
Parse js with Meriyah (a standards-compliant JavaScript parser), walk the resulting AST with astray, collect a list of rewrite operations, apply them as text patches in source order, and return the rewritten source string. If Meriyah cannot parse js (for example, because it is malformed), the original string is returned unchanged and the parse error is logged. The function tracks cumulative parse and patch time and logs it to the console so you can profile the rewriting overhead in the browser DevTools.

Rewrite type: this

Every ThisExpression node is replaced with a call to __get_this__(this). If the this expression appears inside a NewExpression, the replacement is wrapped in parentheses to preserve operator precedence.
// Before rewriting
function example() {
  return this.value;
}

// After rewriting
function example() {
  return __get_this__(this).value;
}
At global scope, __get_this__ returns ctx.__proxy__ instead of the real window, so scripts that reference this at the top level get the proxied global.

Rewrite type: identifier

Every Identifier node whose name is in ctx_vars is replaced — unless it is in a position that does not represent a value read (function parameter names, variable declarator names, object property keys, etc.). For most ctx_vars identifiers, the replacement uses the __get_var__ helper:
// Before rewriting
window.location.href = "https://example.com";

// After rewriting
(__get_var__(window, "window")).location.href = "https://example.com";
For identifiers in unreadable_vars (such as localStorage and sessionStorage) and for location on the left-hand side of an assignment, the replacement uses direct __ctx__ access instead:
// Before rewriting
localStorage.setItem("key", "value");

// After rewriting
__ctx__.localStorage.setItem("key", "value");

Skipping use asm blocks

If the first statement in a BlockStatement is the "use asm" directive, astray skips the entire block. ASM.js modules rely on exact code shapes that would be broken by identifier substitution, so they are left untouched.

HTML rewriting pipeline

HTML rewriting starts in loader.mjs once the host sends the raw HTML string to the frame via RPC. The frame’s "html" handler (load_html) drives the process:
// src/frame/parser.mjs — returns the rewritten source string
function rewrite_js(js)

rewrite_element (src/frame/rewrite/element.mjs)

rewrite_element is a recursive function that visits an element and all of its descendants. For each element it calls the appropriate single-element rewriter based on tag type, then patches event handler attributes and inline styles, and finally recurses into children. The element is marked __rewritten__ = true after its first visit so it is never processed twice. Tag-specific rewriters called by rewrite_element:
Tag / matchRewriterWhat it does
img, source, video, audio, input[type='image']rewrite_mediaRewrites src to fetch the resource through the proxy.
<link> (non-stylesheet)rewrite_linkRewrites href (e.g. favicons, preload hints).
<link rel="stylesheet">rewrite_stylesheetFetches the stylesheet through the network layer and rewrites CSS url() references.
<style>rewrite_styleRewrites CSS url() references in inline <style> blocks.
<script>rewrite_scriptDownloads the script source (if external) and rewrites the JS with rewrite_js.
<form>rewrite_formRewrites the action attribute.
<iframe>rewrite_iframeRewrites the src attribute so nested frames are also proxied.
<meta>rewrite_metaHandles http-equiv="refresh" redirects.
<noscript>rewrite_noscriptParses and rewrites the <noscript> content.
Event handler attributes: Any attribute whose name starts with on (e.g. onclick, onload) is removed from the element, stored as __onXxx, and replaced with an addEventListener call that runs the original handler script through run_script. This ensures the handler code is rewritten before execution. Inline styles: The style attribute is read, cleared, and then passed through parse_css. If parse_css returns synchronously (no external resources), element.style.cssText is set immediately. Otherwise the async result is awaited as a Promise.

rewrite_script (src/frame/rewrite/script.mjs)

Script elements are only processed when site_settings.allow_js is true and the script type is empty, "application/javascript", or "text/javascript". For external scripts (script.src is set), the source is downloaded through network.fetch, converted to text, and stored. The src attribute is removed from the element and replaced with a __src attribute that holds the original URL. This prevents the browser from trying to fetch the script itself. Scripts encountered before the page is fully loaded are queued in pending_scripts (sorted by document order). After the entire HTML tree has been rewritten and inserted into the document, evaluate_scripts drains the queue — running synchronous scripts first, then deferred and async scripts. Scripts encountered after page load (e.g. dynamically created <script> elements) are executed immediately via rewrite_js and a temporary <script> element appended to document.body.

CSS rewriting

parse_css (src/frame/rewrite/css.mjs)

function parse_css(css_str, css_url): string | Promise<string>
Scan css_str for all url(...) references using a regular expression. For each URL that is not already a data: or blob: URI, fetch the resource through network.fetch using css_url as the base for relative paths. Once all fetches complete, replace each url(...) with a url("blob:...") pointing to the newly cached blob. parse_css returns a plain string synchronously if there are no url() references. When at least one fetch is needed it returns a Promise<string>. Callers must handle both cases.
let result = parse_css(inlineStyle, ctx.location.href);
if (typeof result === "string") {
  element.style.cssText = result;
} else {
  element.style.cssText = await result;
}
Resources are fetched in parallel using util.run_parallel. Failed fetches are filtered out and their original url() references are left unchanged in the output.
CSS @import is not yet rewritten. Only url() references inside rule bodies are patched.

End-to-end flow summary

The following steps happen in order for every navigation:
  1. The host fetches the raw HTML over the network and sends it to the frame via the "html" RPC call.
  2. load_html in loader.mjs calls update_ctx() to reset all proxied globals.
  3. DOMParser.parseFromString turns the raw HTML string into a detached document.
  4. rewrite.element(html.documentElement) walks every element and rewrites URLs, scripts, styles, and event handlers.
  5. document.documentElement.replaceWith(...) swaps the live document with the rewritten tree.
  6. evaluate_scripts runs all queued <script> blocks through rewrite_js and inserts them into the document.
  7. Synthetic DOMContentLoaded, readystatechange, and load events are dispatched on ctx.document and ctx.window to trigger page-level event listeners.

Build docs developers (and LLMs) love