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

@finamx/host-bridge

v1.0.0

Published

Primitives for embedding FinamX extensions into host applications — web iframes, React apps, and MCP agents.

Downloads

13

Readme

@finamx/host-bridge

Primitives for embedding FinamX extensions into host applications — web iframes, React apps, and MCP agents.

Install

npm install @finamx/host-bridge @finamx/bridge-protocol

Quick Start — Native-like embed (React)

import {
  ExtensionEmbedFrame,
  fetchEmbedLaunch,
  launchToBridgeSession,
} from '@finamx/host-bridge';

const launch = await fetchEmbedLaunch({
  extensionId: 'com.example.my-extension',
  platformBase: process.env.NEXT_PUBLIC_CMS_URL!,
  userId: 'poc-user-1',
});

<ExtensionEmbedFrame
  session={launchToBridgeSession(launch)}
  launchUrl={launch.launchUrl}
/>;

ExtensionEmbedFrame applies server-driven embedLayout (height bounds, scroll policy, gesture mode), handles ui:resize, ui:scroll-lock, and ui:gesture-claim. See docs/architecture/native-embed-sdk.md.


Quick Start — Web Host

import { createWebHostBridge, CapabilityRouter, ExtensionSessionConfig } from '@finamx/host-bridge';

const config: ExtensionSessionConfig = {
  sessionId: crypto.randomUUID(),
  extensionId: 'com.example.my-extension',
  allowedOrigin: 'https://my-extension.example.com',
  grantedScopes: ['market.read', 'ui.toast'],
};

const bridge = createWebHostBridge({
  iframe: document.getElementById('extension-frame') as HTMLIFrameElement,
  allowedOrigin: config.allowedOrigin,
  session: config,
});

// Tear down (removes window message listener):
bridge.destroy();

Quick Start — Fill-container layout (D-04)

For host UI where the embed should fill a card or modal rather than using a fixed height:

import { ExtensionEmbedFrame } from '@finamx/host-bridge';

// The container div controls the available height; ExtensionEmbedFrame
// fills it and sends host:layout with measured px dimensions via ResizeObserver
// so the extension can respond with appropriate layout (not minHeight: 100vh).
<div style={{ height: 320 }}>
  <ExtensionEmbedFrame
    session={session}
    launchUrl={launch.launchUrl}
    fillContainer  // iframe height = 100% of container; ResizeObserver pushes dimensions
  />
</div>

With useExtensionEmbed, mount containerRef on the wrapper div:

const bridge = useExtensionEmbed({ ..., fillContainer: true });

<div ref={bridge.containerRef} style={{ height: '100%' }}>
  <iframe ref={bridge.iframeRef} {...bridge.frameProps} />
</div>

Quick Start — Design tokens + debug tooling

Pass real design tokens so embedded extensions inherit your host's theme instead of the generic shadcn defaults, and opt into bridge traffic / token inspection panels.

Unified debug kit (D-05, D-06)

BridgeHostDebugKit composes the log panel, token preview, and scope revoke controls into a single opt-in component. Always gate it behind a debug flag — never render in production embeds (T-48-02):

import {
  useExtensionEmbed,
  BridgeHostDebugKit,
  CapabilityRouter,
} from '@finamx/host-bridge';

const [debug, setDebug] = useState(false);
const [grantedScopes, setGrantedScopes] = useState(session.grantedScopes);

const bridge = useExtensionEmbed({ ..., debug });

// Render conditionally — never default on in production
{debug && (
  <BridgeHostDebugKit
    entries={bridge.bridgeLog}
    onClear={bridge.clearBridgeLog}
    tokens={uiTokens}
    grantedScopes={grantedScopes}
    onScopesChange={setGrantedScopes}  // simulate scope revoke (dev-only)
    uiTokensEnabled={tokensEnabled}
    onUiTokensEnabledChange={setTokensEnabled}
  />
)}

Individual panels (manual composition)

import {
  useExtensionEmbed,
  BridgeDebugLogPanel,
  BridgeTokenPreviewPanel,
  CapabilityRouter,
} from '@finamx/host-bridge';
import { mergeUITokens, type UITokens } from '@finamx/bridge-protocol';

function readHostUITokens(): UITokens {
  const style = getComputedStyle(document.documentElement);
  const colors = { ...mergeUITokens({}).colors };
  for (const key of Object.keys(colors) as (keyof UITokens['colors'])[]) {
    const v = style.getPropertyValue(`--${key}`).trim();
    if (v) colors[key] = v;
  }
  return mergeUITokens({ colors });
}

const capabilityRouter = useMemo(() => new CapabilityRouter(), []);
const uiTokens = useMemo(() => readHostUITokens(), []);
const [debugEnabled, setDebugEnabled] = useState(false);

const bridge = useExtensionEmbed({
  launchUrl: launch.launchUrl,
  allowedOrigin: session.allowedOrigin,
  session,
  capabilityRouter,
  uiTokens,
  debug: debugEnabled,
});

// ...render the iframe via bridge.iframeRef / bridge.frameProps as usual, then:
{debugEnabled && (
  <BridgeDebugLogPanel entries={bridge.bridgeLog} onClear={bridge.clearBridgeLog} />
)}
<BridgeTokenPreviewPanel tokens={uiTokens} />

bridgeLog/clearBridgeLog only populate while debug: true — leave it off by default and gate the toggle behind an internal/dev flag, not exposed to end users.


Quick Start — MCP Agent

import { createMcpWidgetTools } from '@finamx/host-bridge/mcp';

const tools = createMcpWidgetTools({
  widgetCatalog: [
    {
      extensionId: 'com.example.widgets',
      name: 'Portfolio',
      description: 'Portfolio summary widget',
      routes: ['/widgets/portfolio-summary'],
    },
  ],
  launchWidget: async (extensionId, route, params) => {
    // Return { launchUrl, sessionId } from your launch service
    return {
      launchUrl: `https://app.example.com/embed?ext=${extensionId}&route=${route}`,
      sessionId: crypto.randomUUID(),
    };
  },
});

// Register with your MCP server:
// tools.forEach(t => server.tool(t.name, t.description, t.inputSchema, t.handler));

Exports

Main package (@finamx/host-bridge)

| Export | Description | |--------|-------------| | createWebHostBridge(options) | Browser iframe bridge with automatic window message listener management | | createHostBridge(options) | Low-level factory — caller manages window events | | createCapabilityRouter(options?) | Factory helper for CapabilityRouter | | CapabilityRouter | Validates scopes, dispatches platform:invoke to handler | | ExtensionEmbedFrame | Drop-in React iframe with embed layout + scroll-lock side effects | | resolveEmbedLayout() | Merge session embedLayout with defaults and overrides | | getEmbedFrameStyles() | CSS for iframe from layout + scroll/gesture state | | runScrollLockSideEffects() | Apply parent scroll lock when extension sends ui:scroll-lock | | sendHostLayoutEvent() / sendHostScrollStateEvent() | Dispatch platform:event to embed | | useExtensionEmbed(options) | React hook — iframe ref, frameProps, layout, lock/claim state, bridgeLog, bridgeReady | | fetchEmbedLaunch(options) | Calls POST /api/v1/embed/launch, returns LaunchResponse | | launchToBridgeSession(launch, userId?) | Converts LaunchResponseBridgeSessionConfig | | ExtensionSessionConfig | Session config with optional uiTokens and embedLayout | | UITokens | Design token shape (re-exported from @finamx/bridge-protocol) | | BridgeLogEntry | Shape of one bridge traffic log entry (useExtensionEmbed({ debug: true })) | | BridgeDebugLogPanel | Opt-in component — live view of bridge send/recv traffic, latency, ok/error | | BridgeTokenPreviewPanel | Opt-in component — swatch preview of the UITokens actually sent to the extension | | BridgeHostDebugKit | Unified dev-only debug UI: log + token preview + scope revoke + token toggle (D-05/D-06) |

Widgets sub-package (@finamx/host-bridge/widgets)

DivKit + custom-id widget host helpers for routing CMS-authored widgets (D-07).

Requires optional peer @divkitframework/react for DivKitWidgetHost rendering.

DivKit custom components — shadow DOM workaround

DivKit renders type: "custom" elements by inserting a <template shadowrootmode="open"> into the DOM via JavaScript (insertBefore). Browsers only process declarative shadow DOM during HTML parsing — dynamic JS insertion is silently ignored. The SVG/HTML inside the template ends up in an inert #document-fragment and never renders.

DivKitWidgetHost and WidgetBySlugEmbed handle this automatically: after DivKit mounts, they scan for <template shadowrootmode> elements, call host.attachShadow() imperatively, and move the template content into the real shadow root. A MutationObserver repeats this for any subsequent DivKit DOM updates.

Known limitation — reactive custom_props

The workaround removes the <template> from the DOM after processing (tmpl.remove()). DivKit still holds a reference to that detached element. If DivKit reactively updates custom_props (e.g. the CMS pushes new chartPoints via a live binding), it writes new SVG content to r.content.innerHTML on the detached template — the shadow root is not updated and shows stale content.

Current mitigations:

  • All existing custom components (finamSparkLine, finamNewsCard, webEmbed) receive static custom_props from the CMS — no live bindings, so the limitation is not triggered.

Required fix when reactive custom components are added:

Instead of removing the template, keep it in the DOM and observe it with a second MutationObserver targeting the template's content. On each change, replace the shadow root's content:

// pseudo-code — implement in activateShadowRoots() in each DivKit host component
const shadow = host.attachShadow({ mode });
const sync = () => {
  shadow.replaceChildren(document.importNode(tmpl.content, true));
};
sync();
// Do NOT remove the template — keep it for DivKit to update
const contentObserver = new MutationObserver(sync);
contentObserver.observe(tmpl.content, { subtree: true, childList: true, characterData: true });

This change is required before shipping any DivKit widget whose custom_props change at runtime.

import {
  resolveWidgetRenderMode,
  fetchDivKitWidget,
  DivKitWidgetHost,
  CustomIdEmbedHost,
} from '@finamx/host-bridge/widgets';

// Route a board card to the correct renderer
const mode = resolveWidgetRenderMode(card.meta);
// → 'divkit' | 'extension-embed' | 'external-embed' | 'custom-id'

// Fetch DivKit JSON from CMS (host passes its own auth headers — T-48-05)
const widget = await fetchDivKitWidget('my-slug', {
  platformBase: 'http://localhost:3001',
  headers: { 'X-User-Id': 'poc-user-1' },
});

// All-in-one routing component (render props per path)
<CustomIdEmbedHost
  meta={card.meta}
  renderDivKit={(slug) => (
    <DivKitWidgetHost
      slug={slug}
      fetchOptions={{ platformBase: 'http://localhost:3001', headers: { 'X-User-Id': userId } }}
    />
  )}
  renderExtension={(extensionId) => <ExtensionEmbed extensionId={extensionId} />}
  renderExternalEmbed={(slug) => <ExternalEmbedContent slug={slug} />}
/>

| Export | Description | |--------|-------------| | resolveWidgetRenderMode(meta) | Returns 'divkit' | 'extension-embed' | 'external-embed' | 'custom-id' from card metadata | | fetchDivKitWidget(slug, options) | GET /api/v1/divkit/widgets/:slug — host passes platformBase + auth headers | | DivKitWidgetResponse | Response type for fetchDivKitWidget | | DivKitWidgetHost | React component — fetches + renders DivKit JSON; dynamic import of @divkitframework/react | | CustomIdEmbedHost | Routes card metadata to render props for divkit/extension/external paths |

MCP sub-package (@finamx/host-bridge/mcp)

| Export | Description | |--------|-------------| | createMcpWidgetTools(options) | Creates plain-object MCP tool definitions for widget rendering | | renderWidgetForAgUi(options) | Builds an ag-ui widget_render event payload | | createWidgetAdapter(options) | Generic launch→render adapter for non-MCP systems | | McpToolDefinition | Plain-object MCP tool type (no @modelcontextprotocol/sdk hard dep) | | AgUiWidgetEvent | ag-ui widget render event type |


Build

npm run build   # tsc → dist/ (includes dist/mcp/)
npm test        # build + run capability-router tests

Output is ESM-only ("type": "module"). Part of the FinamX Mobile monorepo.


Bootstrapping a brand-new host app (Claude Code skill)

Everything in this README is also packaged as a global Claude Code skill — finamx-extension-host — so a new project doesn't need a human to manually replay these steps. It's installed at ~/.claude/skills/finamx-extension-host/SKILL.md (global, not tied to this repo) and covers: installing @finamx/bridge-protocol + @finamx/host-bridge from npm, fetching a launch, rendering via the hook or ExtensionEmbedFrame, wiring real design tokens from the host's own theme, opt-in debug tooling, and capability proxying for sensitive scopes (e.g. portfolio.read).

To run it, in any project, from a Claude Code session:

/finamx-extension-host

or just describe the goal in plain language (e.g. "wire up extension hosting in this app") — Claude Code matches the skill automatically since it's registered globally. The skill's first step always re-checks npm view @finamx/host-bridge version before doing anything, since this package's publish status can change between sessions.

If you're not using Claude Code, follow the "Quick Start" sections above in order: Install → launch → render (hook or component) → design tokens → debug tooling → capability proxying — that's the same sequence the skill automates.

For a full walkthrough that also covers starting the platform locally, picking a seeded test extension, and a concrete verification checklist, see docs/guides/connect-a-new-host.md.