Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@docyrus/chrome-extension-bridge

Package Overview
Dependencies
Maintainers
4
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@docyrus/chrome-extension-bridge

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

latest
Source
npmnpm
Version
0.1.0
Version published
Maintainers
4
Created
Source

@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).

OptionTypeDefaultDescription
hostOriginstring?hostOrigin= query param or "*"Origin to post to. The Docyrus host injects ?hostOrigin= via getEmbeddedAppUrl; the SDK reads it automatically.
timeoutnumber30000RPC timeout in milliseconds. Set to 0 to disable.
targetWindowwindow.parentOverride 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.

MethodSignatureNotes
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

MethodSignatureNotes
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

Keywords

docyrus

FAQs

Package last updated on 18 May 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts