Sandbox APIs
dom.* API reference
Every DOM call available to remixlet code, including signatures, return values, limits, and page-agent security checks.
dom is the sandbox API for reading and changing the current page. Every asynchronous call sends an operation to the page agent. The page agent owns the real document, resolves handles, applies limits, checks writes, and returns JSON data. No dom.* call needs a manifest capability.
Security model#
| Boundary | Enforcement |
|---|---|
| Document access | Remixlet code has no document. It receives opaque Handle objects instead of DOM nodes. |
| Reads | The page agent permits reads, caps their size, and returns serializable snapshots. Read calls do not perform network or extension actions. |
| Writes | The page agent checks tags, attributes, CSS, HTML, URLs, clicks, ownership marks, and tree size before touching the page. |
| Isolation | Each remixlet has its own handle table, listeners, and observers. Dropping its sandbox removes all three. |
| Navigation and loading | Direct navigation, script injection, frame creation, form creation, and arbitrary resource URLs are blocked. Links, images, and clicks use the URL policy under Write security. |
| Failures | Policy denials reject with refused: <reason> and log each distinct reason once. Invalid arguments and stale handles also reject. |
Root handles#
| Property | Type | Description |
|---|---|---|
dom.document | Handle | Query root for the current document. |
dom.body | Handle | Handle for document.body. |
dom.head | Handle | Handle for document.head. |
dom.window | WindowHandle | Event-only handle for approved window events. |
dom.document, dom.body, and dom.head remain valid for the sandbox lifetime. dom.window supports only on and off.
Queries#
| Call | Returns | Description and limits |
|---|---|---|
await dom.query(selector) | Handle | null | Returns the first document match. The selector may contain at most 4096 characters. |
await dom.queryAll(selector, options?) | Handle[] | Returns document matches. options.limit lowers the default maximum of 500. options.info: true fills each handle's snapshot in the same round trip. |
await dom.waitFor(selector, options?) | Handle | null | Returns the first current or future match. Rechecks after page mutations, at most once per 50 ms. timeoutMs defaults to 10000 and may not exceed 120000. |
Invalid selectors reject with invalid selector.
Creating page content#
dom.create#
const handle = await dom.create(tag, {
text,
html,
attrs,
classes,
style,
children,
});Creates a detached element and returns its Handle. children accepts strings and nested { tag, ...options } objects. The page agent validates the complete tree before creating any node. A tree may contain at most 1000 nodes and may be at most 32 levels deep. Text and HTML fields may contain at most 256 KB each.
The returned tree is stamped with data-rmx-owner. Refused tags, attributes, styles, or URLs reject the whole call.
dom.clone#
const handle = await dom.clone(selector, { text, strip, keepIds });Equivalent to dom.query(selector) followed by handle.clone(options). Returns null when the selector has no match.
The clone is detached and deep. Event listeners are not copied. strip removes matches before text edits the clone. A string text replaces all content; a { selector: text } map updates the first match for each selector. keepIds preserves id and for, which are removed by default. The page agent sanitizes the clone with the same rules as dom.create and logs removed content.
dom.addStyle#
const style = await dom.addStyle(css);Adds an owned <style> element to the page head and returns its handle. CSS may contain at most 256 KB. The page agent rejects CSS that can load a resource or execute legacy behavior. See CSS.
Page facts and scrolling#
| Call | Returns | Description |
|---|---|---|
await dom.location() | { href, origin, pathname, search, hash, title } | Reads the current values from the page agent. |
dom.viewport() | { width, height, scrollX, scrollY } | Returns the latest pushed viewport snapshot without a round trip. |
await dom.viewportNow() | { width, height, scrollX, scrollY } | Reads the current viewport from the page agent. |
await dom.scrollTo(x?, y?, behavior?) | void | Sets the page window's scroll position. Also accepts { left, top, behavior }. |
await dom.scrollBy(x?, y?, behavior?) | void | Moves the page window's scroll position. Also accepts { left, top, behavior }. |
behavior accepts auto, smooth, or instant. The runtime converts instant to auto before sending the call.
Mutation observation#
dom.observe#
const stop = dom.observe(callback, {
root,
childList,
subtree,
attributes,
characterData,
});Registers a page-agent observer and returns a synchronous unsubscribe function. root defaults to dom.document. All four mutation flags default to true. The callback receives { deliveries }, where deliveries is the number of raw MutationObserver deliveries folded into the notice. It does not receive mutation records.
The first notice after a quiet period arrives immediately. Later deliveries within 100 ms are coalesced. Each observer accepts at most 40 raw deliveries per second. A fast refill caused by callback writes is held to one trailing notice per second and logged as a feedback loop. Sustained page-driven changes remain coalesced and produce one informational log entry.
Handle properties#
| Property | Type | Description |
|---|---|---|
handle.id | string | Opaque id minted by the page agent. |
handle.snapshot | ElementInfo | undefined | Last value returned by info() or populated by queryAll(..., { info: true }). It is not live. |
The page agent keeps at most 20000 handles per sandbox. Call release() when a large batch is no longer needed. The sandbox permits at most 5000 unanswered DOM calls.
Handle queries and traversal#
| Call | Returns | Description |
|---|---|---|
await handle.query(selector) | Handle | null | Returns the first descendant match. |
await handle.queryAll(selector, options?) | Handle[] | Returns descendant matches with the same options and 500-handle maximum as dom.queryAll. |
await handle.matches(selector) | boolean | Tests the element against a selector. |
await handle.closest(selector) | Handle | null | Returns the nearest matching ancestor, including the element itself. |
await handle.parent() | Handle | null | Returns the parent element. |
await handle.children() | Handle[] | Returns up to 500 child elements. |
await handle.first() | Handle | null | Returns the first child element. |
await handle.last() | Handle | null | Returns the last child element. |
await handle.next() | Handle | null | Returns the next element sibling. |
await handle.prev() | Handle | null | Returns the previous element sibling. |
await handle.contains(other) | boolean | Tests whether this handle contains another handle. |
await handle.shadow() | Handle | null | Returns an open shadow root as a query handle. Closed shadow roots return null. |
await handle.isConnected() | boolean | Reports whether the node is still in the document. |
Handle reads#
| Call | Returns | Description and limits |
|---|---|---|
await handle.info() | ElementInfo | Returns { handle, tag, id, className, text, attrs, dataset, value?, checked?, rect, visible } and updates snapshot. text is trimmed to 2000 characters. Password values are replaced with an empty string. |
await handle.text() | string | Returns textContent, capped at 64 KB. |
await handle.innerText() | string | Returns rendered text, capped at 64 KB. |
await handle.html() | string | Returns innerHTML, capped at 64 KB. Password value attributes are replaced with an empty string. |
await handle.attr(name) | string | null | Returns an attribute. A password field's value attribute returns an empty string. |
await handle.data(name) | string | null | Reads a dataset key. data("itemId") reads data-item-id. |
await handle.value() | string | Returns a form control value, or an empty string for other elements and password fields. |
await handle.computed(prop) | string | Returns one computed CSS value. |
await handle.computed(props) | Record<string, string> | Returns up to 100 computed CSS values. |
await handle.rect() | Rect | Returns { x, y, width, height, top, left, bottom, right }. |
await handle.visible() | boolean | Returns true for a non-zero laid-out element that is not display: none or visibility: hidden. |
Handle content and attributes#
| Call | Returns | Effect and security |
|---|---|---|
await handle.setText(text) | void | Sets textContent. Limited to 256 KB. Allowed on page and owned elements. |
await handle.setHTML(html) | void | Replaces children with inertly parsed, sanitized markup. Limited to 256 KB. |
await handle.setAttr(name, value) | void | Sets an attribute. Page-owned elements accept only this remixlet's marks. URL attributes use the URL policy. |
await handle.removeAttr(name) | void | Removes an attribute. Page-owned elements accept only this remixlet's marks. |
await handle.setData(name, value) | void | Sets a dataset key through setAttr. |
await handle.hide() | void | Sets hidden. Refused on page-owned elements because hidden is not an ownership mark. |
await handle.show() | void | Removes hidden. Refused on page-owned elements unless the element is owned. |
await handle.setDisabled(disabled) | void | Adds or removes disabled. Refused on page-owned elements unless the element is owned. |
await handle.addClass(...names) | string[] | Adds classes and returns the resulting class list. Page-owned elements accept only prefixed classes. |
await handle.removeClass(...names) | string[] | Removes classes and returns the resulting class list. Page-owned elements accept only prefixed classes. |
await handle.toggleClass(...names) | string[] | Toggles classes and returns the resulting class list. A final boolean forces add or remove. |
await handle.style(props) | void | Sets inline properties. Empty values remove properties. Camel-case and kebab-case names are accepted. A trailing !important is preserved. CSS security rules apply. |
await handle.setValue(value) | void | Sets a form value, or a checkbox/radio from a boolean, then dispatches input and change. Allowed on page and owned controls. |
Handle placement and actions#
| Call | Returns | Effect and security |
|---|---|---|
await handle.clone(options?) | Handle | Returns a detached, sanitized deep copy. See dom.clone. |
await parent.append(child) | void | Appends a handled node. Browser-invalid placements reject. |
await parent.prepend(child) | void | Prepends a handled node. Browser-invalid placements reject. |
await reference.before(node) | void | Inserts a handled node before this node. |
await reference.after(node) | void | Inserts a handled node after this node. |
await handle.remove() | void | Removes the node. Allowed for page and owned elements. |
await handle.click() | void | Dispatches the element's click behavior after checking links, labels, controls, and form actions against the URL policy. |
await handle.focus() | void | Focuses the element. |
await handle.blur() | void | Blurs the element. |
await handle.scrollIntoView(block?) | void | Scrolls the element into view. block accepts start, center, end, or nearest. |
await handle.scrollTo(x?, y?, behavior?) | void | Sets the element's scroll position. |
await handle.scrollBy(x?, y?, behavior?) | void | Moves the element's scroll position. |
await handle.release() | void | Removes the id from the sandbox's handle table. |
Events#
handle.on#
const stop = handle.on(type, callback, {
selector,
preventDefault,
stopPropagation,
once,
capture,
passive,
});Registers an event listener and returns a synchronous unsubscribe function. selector delegates from the current handle and makes the matched descendant the event's target.
The callback receives a plain object with type, target, currentTarget, key, code, modifier keys, button, clientX, clientY, form value and checked, and defaultPrevented. target and currentTarget are handles. Fields that do not apply to the event are omitted. Events from password fields omit key and code and replace value with an empty string.
Callbacks run in the sandbox after a message round trip. To cancel the page event synchronously, set preventDefault or stopPropagation in the registration options. passive: true overrides preventDefault.
If the page removes a listened node, the page agent removes its listeners and marks the handle stale. Every later call except isConnected() and release() rejects. A listener registered during rmx.keep(...).apply() causes that keep to reapply once so it can bind to the replacement node.
dom.window.on and dom.window.off#
const stop = dom.window.on(type, callback, options);
dom.window.off(type, callback);Supports scroll, resize, keydown, keyup, focus, blur, and visibilitychange. Re-registering the same type and callback returns the existing subscription. The sandbox's window.addEventListener and window.removeEventListener facades route to these methods.
Write security#
Ownership marks#
The page agent stamps elements created, cloned, or inserted through sanitized HTML with data-rmx-owner. Remixlet code cannot set or remove that attribute.
Owned elements accept ordinary attributes and classes. Page-owned elements accept only this remixlet's marks, written with the id slot:
| Kind | You write | The page carries |
|---|---|---|
| Attribute | data-rmx--mix | data-rmx-<id>--mix |
| Class | rmx--on | rmx-<id>--on |
The double hyphen is the slot for the remixlet id. The page agent fills it in every name, selector, attribute value, markup string, and stylesheet the remixlet hands it, the extension fills it in style.css on injection, and reads (info(), html(), class lists) show the slot form again, so a name is spelled one way everywhere. Names that spell the id in full (data-${rmx.prefix}-<name>, ${rmx.prefix}-<name>) still pass for code written before the slot.
Content writes, values, inline styles, placement, removal, and approved clicks do not use the mark rule.
Tags and attributes#
Creation rejects unlisted tags and tags that can load, execute, submit, or embed content. This includes form, iframe, frame, object, embed, script, base, meta, link, style, template, media source tags, math, and noscript.
Attribute writes reject event handlers and attributes that can load, execute, submit, or redirect. This includes on*, action, srcdoc, srcset, formaction, ping, poster, target, http-equiv, style, is, form, rel, and download. href on links and src on images use the URL policy instead.
setHTML, create({ html }), and clone parse markup in an inert document. The sanitizer removes comments, refused attributes, and refused tags with their subtrees before importing the result into the page.
URLs#
The page agent resolves link href and image src values to absolute URLs. It permits:
- the current page origin;
- a host covered by the remixlet's
matches; - a host covered by an approved
fetch:capability; - an exact URL the page already loads;
data:imageURLs.
It rejects javascript:, blob:, and hosts granted only through network:observe:. Stored attributes contain the resolved absolute URL.
click() applies the same rule to the element, an enclosing link, the control activated through a label, and a submitted form or formaction target.
CSS#
Inline styles and dom.addStyle reject CSS containing url(, image-set(, @import, src(, expression(, or behavior: in any spelling accepted by CSS.
Sandbox browser facades#
Remixlet files have no live browser globals. The sandbox provides read-only facades:
| Facade | Available members |
|---|---|
location | href, origin, protocol, host, hostname, port, pathname, search, hash, toString(), valueOf(), toJSON() |
navigator | language, languages, userAgent, platform, hardwareConcurrency, onLine |
window | Timers, requestAnimationFrame, queueMicrotask, structuredClone, JavaScript built-ins, console, the facades above, viewport fields, approved event methods, scrolling, dom, and rmx |
Writes to these facades reject. location.assign, location.replace, and location.reload reject. Page-shaped globals such as document, fetch, XMLHttpRequest, MutationObserver, localStorage, WebSocket, Image, Worker, alert, and open are blocked with an error that names the corresponding sandbox API where one exists.
Limits#
| Resource | Limit |
|---|---|
| Handles per sandbox | 20000 |
| Calls in flight | 5000 |
| Selector | 4096 characters |
queryAll and children | 500 handles |
waitFor | 10 seconds by default, 120 seconds maximum |
info().text | 2000 characters |
text(), innerText(), html() | 64 KB |
| Text, HTML, attribute, and stylesheet writes | 256 KB |
| Created or cloned tree | 1000 nodes, 32 levels |
computed() | 100 properties |
| Observer | 40 raw deliveries per second, notices coalesced over 100 ms |
dom.* shares the rmx.* bridge compatibility version. The extension does not run a remixlet whose stored version falls outside its supported range. See rmx.* API compatibility.