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

@docyrus/chrome-extension-bridge

v0.1.0

Published

Iframe SDK and host adapter for talking to the Docyrus chrome extension shell over postMessage

Readme

@docyrus/chrome-extension-bridge

Iframe SDK and host adapter for talking to the Docyrus Chrome extension shell over postMessage.

External Docyrus apps run as iframes inside the Docyrus shell (web, desktop, chrome side panel). The shell already exposes Docyrus auth and data APIs to the iframe via a postMessage RPC channel. When the user has the Docyrus Chrome extension installed, the chrome side panel has additional access to the user's active browser tab — navigation, scripting, cookies, network inspection, screenshots, etc. — implemented in the extension's background service worker.

This package gives external apps a small, typed SDK to reach those capabilities. The same code keeps working in the desktop and web shells — it just reports isAvailable === false and rejects method calls with a typed ChromeExtensionUnavailableError.

Status: 0.1.x — protocol stable enough to build against, but still iterating; minor versions may add new RPC methods. Host and SDK pin a major version against CHROME_EXTENSION_PROTOCOL_VERSION to fail loudly if they diverge.

Install

pnpm add @docyrus/chrome-extension-bridge
# or
npm install @docyrus/chrome-extension-bridge

There are no required peer dependencies. The iframe SDK is framework-agnostic.

Quick start (iframe app)

import { createChromeExtensionBridge } from "@docyrus/chrome-extension-bridge"

const chromeExt = createChromeExtensionBridge()

await chromeExt.ready()

if (!chromeExt.isAvailable) {
  // Running outside the Docyrus chrome extension. Hide the chrome-only UI
  // and keep going.
  return
}

const tab = await chromeExt.tabs.getActive()
console.log("Active tab:", tab.url, tab.title)

await chromeExt.tabs.navigate({ url: "https://example.com" })
await chromeExt.tabs.wait({ idle: true, timeout: 10_000 })

const off = chromeExt.on("tabChanged", info => {
  console.log("User switched tabs →", info.url)
})

// later, when your component unmounts
off()
chromeExt.dispose()

React example

import { useEffect, useState } from "react"
import {
  ChromeExtensionUnavailableError,
  createChromeExtensionBridge,
  type TargetTabInfo
} from "@docyrus/chrome-extension-bridge"

function ActiveTabPanel() {
  const [tab, setTab] = useState<TargetTabInfo | null>(null)
  const [available, setAvailable] = useState<boolean | null>(null)

  useEffect(() => {
    const bridge = createChromeExtensionBridge()
    let cancelled = false

    bridge.ready().then(async () => {
      if (cancelled) return
      setAvailable(bridge.isAvailable)
      if (!bridge.isAvailable) return
      try {
        setTab(await bridge.tabs.getActive())
      } catch (error) {
        if (error instanceof ChromeExtensionUnavailableError) return
        console.error(error)
      }
    })

    const off = bridge.on("tabChanged", info => {
      if (!cancelled) setTab(info)
    })

    return () => {
      cancelled = true
      off()
      bridge.dispose()
    }
  }, [])

  if (available === false) return <p>Open this app in the Docyrus Chrome extension to see the active tab.</p>
  if (!tab) return <p>Loading…</p>
  return <p>{tab.title} — {tab.url}</p>
}

API

createChromeExtensionBridge(options?)

Creates a bridge instance bound to the parent window (window.parent).

| Option | Type | Default | Description | | ------------ | -------- | ----------------- | ----------- | | hostOrigin | string | ?hostOrigin= query param or "*" | Origin to post to. The Docyrus host injects ?hostOrigin= via getEmbeddedAppUrl; the SDK reads it automatically. | | timeout | number | 30000 | RPC timeout in milliseconds. Set to 0 to disable. | | target | Window | window.parent | Override the postMessage target (rarely needed). |

Returns a ChromeExtensionBridge:

interface ChromeExtensionBridge {
  ready(): Promise<void>
  readonly isAvailable: boolean
  readonly shell: "chrome" | "desktop" | "web" | null
  readonly protocolVersion: number

  tabs: { /* see below */ }
  cookies: { /* see below */ }
  console: { /* see below */ }
  network: { /* see below */ }

  on<E>(event: E, listener): () => void
  dispose(): void
}

ready() resolves once the bridge has handshaked with the host and probed availability. Awaiting ready() is optional — every method internally waits on ready() before sending — but it's the easiest way to branch on isAvailable.

If isAvailable === false, every method rejects with ChromeExtensionUnavailableError.

bridge.tabs

All methods operate on the user's currently active (or pinned) browser tab. URLs the extension can't touch (chrome://, chrome-extension://, about:) are filtered out by the host; the SDK surfaces a clear error in that case.

| Method | Signature | Notes | | --- | --- | --- | | getActive() | () => Promise<TargetTabInfo> | { tabId, url, title, pinned }. | | pin({ tabId }) | (p) => Promise<TargetTabInfo> | Pin a specific tab so future calls target it across user navigation. | | unpin() | () => Promise<TargetTabInfo> | Resume tracking whichever tab the user is on. | | navigate({ url?, reload? }) | (p) => Promise<{ url }> | Navigate or reload the target tab. | | wait({ idle?, selector?, url?, ms?, timeout? }) | (p) => Promise<{ waited, url }> | Wait for idle/network-quiet, a CSS selector, a URL match, or a fixed delay. | | snapshot({ all?, selector? }) | (p) => Promise<{ count, snapshot }> | Capture an interactive snapshot of the DOM (accessibility-style tree). | | click({ target, x?, y?, timeout? }) | (p) => Promise<{ clicked, url }> | Dispatch a click via CDP at a selector or coordinates. | | fill({ target, value, timeout? }) | (p) => Promise<{ filled, value }> | Focus a field and type a value. | | select({ target, value }) | (p) => Promise<{ selected, value }> | Set the value of a <select>. | | evaluate({ code }) | (p) => Promise<{ result }> | Run JavaScript in the page main world and return the value. | | screenshot({ full? }) | (p) => Promise<{ base64 }> | PNG (base64); full: true captures the full page. | | getContent() | () => Promise<{ url, title, content }> | Readable text content of the page. | | getInfo() | () => Promise<Record<string, unknown>> | Misc metadata (meta tags, language, etc.). | | openExternal({ url }) | (p) => Promise<{ webviewId }> | Open a tracked side tab. | | closeExternal({ webviewId }) | (p) => Promise<void> | Close a tab opened by openExternal. |

bridge.cookies

| Method | Signature | Notes | | --- | --- | --- | | getAll({ domain?, name?, url? }) | (p) => Promise<{ cookies }> | Returns name, value, domain, path, secure, httpOnly, expirationDate. |

bridge.console / bridge.network

Both expose a CDP-backed ring buffer the extension fills while the side panel is open.

const { messages } = await bridge.console.read({ level: "error" })
const { requests } = await bridge.network.read({ status: 500 })

Events

bridge.on("tabChanged", (info) => {
  // info: TargetTabInfo
})

tabChanged fires whenever the host detects the target tab moved (navigated, reloaded, or the user switched tabs/windows). Multiple listeners are supported; the return value of on removes the listener.

Capability detection

The same app can run in the chrome extension, the Docyrus desktop app, and the Docyrus web app. The SDK keeps that cross-shell story simple:

const bridge = createChromeExtensionBridge()
await bridge.ready()

if (bridge.shell === "chrome") {
  // Chrome-only UI: show active tab tools.
} else {
  // Desktop or web: hide the panel, or surface a "Install the Docyrus
  // Chrome extension" upsell.
}

bridge.isAvailable is a strict alias for bridge.shell === "chrome".

Host integration

The host adapter is what makes the SDK actually talk to chrome.* APIs. It lives in the same package, and is consumed by the chrome extension shell — not by your app code. Most readers can skip this section.

import { createChromeExtensionHostApi } from "@docyrus/chrome-extension-bridge/host"
import { getDocyrusDesktopBridge } from "@workspace/client-web/desktop"

const hostApi = createChromeExtensionHostApi({
  getBridge: () => getDocyrusDesktopBridge() ?? null,
  shell: "chrome"
})

useEffect(() => {
  const detach = hostApi.attach()

  return () => {
    detach()
    hostApi.dispose()
  }
}, [hostApi])

<ExternalAppContainer
  ref={hostApi.containerRefFor(app.slug)}
  rootApi={hostApi.rootApi}
  src={getEmbeddedAppUrl(src)}
  title={app.name}
/>

hostApi.rootApi is a fragment of an ExternalAppApiMap (Record<string, fn>). It merges into the default root API exposed by ExternalAppContainer without colliding (all methods are prefixed chromeExtension.).

hostApi.containerRefFor(key) returns a stable MutableRefObject keyed per iframe so events broadcast from the background worker fan out to every mounted bridge.

hostApi.attach() subscribes to chrome.runtime.onMessage and forwards docyrus:browser:target-tab-changed broadcasts as the SDK-visible tabChanged event.

Permissions

This package does not request any chrome permissions itself. It only forwards calls to the Docyrus extension, whose manifest already declares:

  • tabs, activeTab, scripting, webNavigation — required for tab inspection and automation.
  • cookies — required for cookies.getAll.
  • debugger — required for evaluate, screenshot, console, network, and CDP-backed input dispatch (click, fill).
  • storage, sidePanel, identity — used by the shell itself; the bridge does not surface them.

Limitations

  • One target tab at a time. The extension automation surface drives whichever tab is active or pinned via tabs.pin.
  • Privileged URLs are unreachable. chrome://, chrome-extension://, about:, edge://, brave:// are filtered out by the host. Methods return an error if the user is on one of those pages.
  • CDP-backed methods require the side panel to be open. Console and network buffers only fill while the Docyrus side panel is attached; closing it detaches CDP.
  • No bidirectional event channel. The bridge currently only pushes events from host → iframe. If you need to push from iframe → host, open an issue.

Versioning

CHROME_EXTENSION_PROTOCOL_VERSION is exported from the package root and will increment on every breaking RPC/event change. The SDK refuses to talk to a host that reports a lower major protocol version.

The package follows semver; pre-1.0.0 the protocol is still allowed to shift between minor versions, but every change is documented in CHANGELOG.md.

License

MIT