
Research
/Security News
Laravel Lang Compromised with RCE Backdoor Across 700+ Versions
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.
@docyrus/chrome-extension-bridge
Advanced tools
Iframe SDK and host adapter for talking to the Docyrus chrome extension shell over postMessage
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 againstCHROME_EXTENSION_PROTOCOL_VERSIONto fail loudly if they diverge.
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.
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()
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>
}
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.tabsAll 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.networkBoth 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 })
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.
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".
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.
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.tabs.pin.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.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.
MIT
FAQs
Iframe SDK and host adapter for talking to the Docyrus chrome extension shell over postMessage
We found that @docyrus/chrome-extension-bridge demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?

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.

Research
/Security News
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.

Security News
Socket found a malicious postinstall hook across 700+ GitHub repos, including PHP packages on Packagist and Node.js project repositories.

Security News
Vibe coding at scale is reshaping how packages are created, contributed, and selected across the software supply chain