npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vendoai/stage

v0.1.0

Published

Sandboxed stage runtime and bridge for Vendo agent-generated UI.

Downloads

21

Readme

@vendoai/stage

Secure sandboxed-stage runtime and bridge for agent-generated UI. Provides the host-side sandbox lifecycle (createStage, connectStage) and the Vite build preset (vendoHostPreset) that host bundles use.

Security model

The stage runs in an opaque-origin <iframe sandbox="allow-scripts"> using srcdoc — no allow-same-origin, so the sandbox cannot access host storage, cookies, or the DOM outside the frame. A strict CSP is set inline:

default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline';
img-src data:; font-src data:; connect-src 'none'

Key invariants:

  • Code crosses host → sandbox as data. The bundle is fetched by the host and passed as text; the sandbox creates a blob: URL and import()s it. No network access from inside the sandbox (egress jail: connect-src 'none').
  • Sandbox → host actions only through the audited chokepoint. The sandbox calls window.__vendoDispatch(descriptor, originNodeId), which serializes to a tools/call postMessage. The host validates, audits, and returns an ActionResult.
  • Bundle, theme tokens, and state projection are data. They cross the postMessage boundary as structured-clone-safe values, not code.

Threat model & v1 limitations

These are deliberate v1 boundaries, not bugs. The follow-up items are tracked under F2/F3.

  • Capability tokens are host-validated provenance, not in-sandbox isolation. Each node carries a per-id token; the host rejects a tools/call whose token does not match the origin node. But all bundle code in the frame can read every token in its tree (they live in the rendered tree), so a token proves "this action came from a node the host placed", not "node A cannot speak for node B". v1 assumes host-supplied presentational bundles. Cross-node isolation and a real token policy (expiry/rotation, per-node scoping) are F2. The host map and the runtime map are rebuilt from scratch on every initialize/update, so a replaced or removed node id loses its token immediately — tokens never accumulate.
  • $state prop binding resolves top-level only. A prop whose direct value is { "$state": "key" } is substituted from the state projection. Nested references (inside an object or array prop) are passed through unresolved — F3b's renderer owns deep resolution. See the bindProps unit test for the pinned behavior.
  • Standalone teardown removes the iframe is the caller's job. connectStage(...).dispose() tears down the RPC bridge and best-effort posts ui/teardown to reject outstanding approvals; it does not remove the iframe created by createStage. The VendoStage React adapter removes the iframe for you; standalone callers must remove it themselves.
  • Theme token values are interpolated unsanitized. Theme keys/values are written verbatim into a :root{} block. This is contained by the egress CSP today (values cannot exfiltrate). When theme tokens become agent-influenced, F2 should allowlist --[a-z-]+ names and sanitize values.
  • Approvals have no wall-clock timeout. Approval-pending actions are intentionally long-lived. They are settled by resolveAction / cancelAction, and any still-outstanding on dispose() are rejected via ui/teardown — there is no automatic expiry.

Public API

createStage(slot, opts?)

Mounts a sandboxed <iframe> into slot (a host DOM element) and returns { iframe, endpoints }. Optional opts.reactSource injects a React ESM shim so the sandbox shares a single React instance with the host bundle.

connectStage(endpoints, opts)

Returns a StageController with:

  • ready: Promise<void> — resolves when the sandbox runtime signals ready, or rejects with a sandbox error if it never signals within readyTimeoutMs (default 10s).
  • initialize(params) — sends ui/initialize to the sandbox (theme, state, bundleSource, tree).
  • update(params) — sends ui/update. A node patch travels as one unit: update({ replace: { nodeId, node } }). theme / state may be patched on their own. Replacing an unknown nodeId rejects (no silent success).
  • resolveAction(actionId, result) — resolves a parked approval-pending action.
  • cancelAction(actionId, reason?) — cancels a parked approval-pending action (settles the runtime promise with an error so it never leaks).
  • dispose() — tears down the RPC bridge and posts ui/teardown. Does not remove the iframe (see Threat model & v1 limitations).

connectStage(endpoints, { onAction, readyTimeoutMs? }).

StageController

The object returned by connectStage. Exposes ready, initialize, update, resolveAction, cancelAction, and dispose.

StageCapabilities

The interface F3b's renderer consumes (spec §7, frozen from the F3a spike):

interface StageCapabilities {
  resolveComponent(name: string, source: "prewired" | "host"): ComponentImpl | undefined;
  theme: ThemeTokens;
  getState(): Readonly<StateProjection>;
  subscribe(cb: () => void): () => void;   // provisional — not exercised in F3a
  dispatch(action: ActionRequest): Promise<ActionResult>;
}

Host-side GenUI resolution + ui-delta

createGenUISession(payload) validates a Vendo GenUI v1 payload and resolves its flat, id-addressed graph into the nested UINode tree the sandbox renders. On { ok: true } it returns a session:

  • session.tree: the resolved tree, passed as StageInitPayload.tree to controller.initialize.
  • session.applyDataPatch(path, value?): applies a JSON-Pointer patch to the live data model and returns the minimal node replacements a prop-level update needs: Array<{ nodeId, node }>. Omit value to delete the pointer; pass it (even undefined) to set. Each replacement is driven into the sandbox via controller.update({ replace: { nodeId, node } }).

Because structure is unchanged on a data patch, only bound nodes are replaced (no remount). The VendoStage React adapter wires this automatically: a data-only payload change takes the applyDataPatch path, while a structural change re-initializes.

Host build step

Host component bundles must be built with vendoHostPreset from @vendoai/stage/build:

// vite.config.ts (host bundle)
import { vendoHostPreset } from "@vendoai/stage/build";

export default vendoHostPreset({
  entry: "src/entry.tsx",
  version: "1.0.0",
  outDir: "dist",
});

The preset encodes two hard requirements from the F3a spike:

  1. React (react, react-dom, react-dom/client, react/jsx-runtime) is externalized — the sandbox imports it from its import map, so there is exactly one React instance in the sandbox.
  2. process.env.NODE_ENV is defined at build time — the sandbox has no process global and React throws without this.

It also stamps __VENDO_BUNDLE_VERSION__ into the output for traceability.

Host component bundles must be presentational only: pure props in, action descriptors out. No network calls, no side effects that escape the sandbox.

vite and @vitejs/plugin-react are peer dependencies — the host project provides them.

Running tests

Unit tests (30 cases, no browser needed):

pnpm --filter @vendoai/stage test

Browser tests (15 gates, requires Chromium via Playwright):

pnpm --filter @vendoai/stage test:browser