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

@ada-cx/messaging-bridge

v1.3.0

Published

Types, test mocks, and a thin CDN loader for building a custom app UI on Ada Messaging's bridge contract. The bridge runtime always loads from Ada's CDN.

Readme

@ada-cx/messaging-bridge

Build your own chat UI (a "custom app") on Ada Messaging's bridge contract.

This package carries TypeScript types, test mocks, and a thin loader.

Ada core stamps its build identifier into your app frame's window.name. The loader uses it to load the matching bridge runtime.

Your installed package never contains the runtime. Ada can update runtime security logic through a coordinated core release.

Requirements

  • Your custom app runs inside an iframe that Ada's core frame mounts. The core frame is served from Ada's asset host, and it sits inside the page that embeds the widget. Both origins are in your app's ancestor chain.
  • You point the Web SDK at your app with its appUrl setting.
  • Your app's origin must be in your handle's Allowed websites list. Add it in your Ada dashboard, under Channels > Chat. Until the list allows your origin, a configured appUrl is dropped with a console warning and Ada's default app mounts (with appUrlFallback: false the activation fails instead). An entry that carries a path, query, or fragment does not authorize a custom app. Add the bare origin as its own entry. Local development needs no entry when both your app and the embedding page run on loopback hosts.
  • If your app's responses send no X-Frame-Options header and no CSP frame-ancestors directive, browsers permit framing, and no server change is needed. If your app restricts framing, frame-ancestors must allow Ada's asset hosts and every site origin that embeds the widget:
Content-Security-Policy: frame-ancestors https://messaging-assets.ada.support https://static.ada.support https://your-site.com

Replace https://your-site.com with the origins of the pages that embed the Ada widget. Browsers check frame-ancestors against every ancestor frame, so a policy that lists only Ada's hosts blocks your app.

Quick start

Install the package:

npm install @ada-cx/messaging-bridge

Load the runtime, complete the handshake, then render from state:

import { loadMessagingBridge } from "@ada-cx/messaging-bridge";

const bridge = await loadMessagingBridge();
const client = bridge.createBridgeClient();

// REQUIRED: send app.initialize within 15 seconds of your frame loading.
// If the handshake does not arrive, core unmounts your frame.
client.sendEvent("app.initialize");

// Subscribe to display-state updates from core.
const unsubscribe = client.subscribe(() => {
  const state = client.getState();
  renderMessages(state?.["chat.messages"] ?? []);
});

// Send a user message through the operations layer.
const handle = client.operations.sendMessage("Hello");
const message = await handle.settled();

// Tear down when your app unmounts.
unsubscribe();
client.destroy();

loadMessagingBridge() memoizes the load. Repeated calls return the same promise. A failed load is forgotten, so a later call retries.

Use typed state keys through the loaded module:

const botName = client.getState()?.[bridge.STATE.CONFIG_BOT_NAME];

Operations

client.operations provides typed helpers over the raw event contract. Each helper carries the guards, debounces, and correlation logic that Ada's own app uses. Prefer them over hand-built sendEvent calls.

// Correlated send: the handle resolves with the message row.
const handle = client.operations.sendMessage("Hello");
const message = await handle.settled({ timeoutMs: 10_000 });

// Read tracking: debounced, monotonic, reset per conversation.
client.operations.markRead(message.cursor ?? "");

// Correlated survey submit.
const result = await client.operations
  .submitCsat({ score: 5 }, { surveyType: "bot", conversationId })
  .settled();

Highlights:

  • sendMessage(body, { secret? }) returns a handle. settled() resolves with the message once it appears in chat.messages, and rejects when core reports a send error first (rate limit, over-length body, session not ready, dispatch failure) — detected on the chat.error.seq advance, so a repeat of the identical error text still rejects. A secret send writes no transcript row, so its handle never resolves. Pass timeoutMs or skip settled() for secret sends.
  • submitCapture(value).settled() reports the server verdict for the active capture field.
  • checkEndChatEligibility() resolves with the End Chat survey decision.
  • endLiveChat() ends only the live-agent leg. On a bot with config.features.endLiveChat, call it — not skipCsatAndEndChat() or an end_chat submitCsat() — when End Chat is confirmed during a live chat (liveAgent.inLiveChat), and offer no end-chat survey: api routes the post-agent survey as an inline live_chat row once the leg has ended. Once liveAgent.liveChatEndedInConversation is true on such a bot, End Chat calls skipCsatAndEndChat() directly, with no confirmation or eligibility check.
  • trackCsatShown("live_chat") is dropped by core: api records that survey as shown when it routes its row.
  • markRead(cursor) debounces 400 ms and keeps a monotonic watermark. Do not reimplement read tracking.
  • startNewConversation() applies a 5 second cooldown and returns false while cooling down or while app.conversationStartPending is true. A pending refusal does not extend the cooldown. true means the request was sent.
  • A restart requested while proactive requests are in flight waits for them. Render both restart controls busy from app.conversationStartPending (absent means false). Subscribe with client.subscribeKey or read React bridge state. A successful proactive keeps its messages and prior transcript; a completed success awaiting conversation delivery can satisfy one restart too. Another explicit restart after completion is a new intent, allowing recovery when no conversation arrives. Without pending work or an unused success awaiting delivery, the normal reset begins immediately. If all fail and the request remains current, Core performs the latest accepted reset once. A newer conversation/session or an accepted user send or retry cancels that wait; rejected sends leave it intact. resetApp() still sends while pending. Wait for Core to clear the pending state; it is transient and is not a conversation-start completion acknowledgement.
  • startFileUpload(file) retains the { file, uploadId } pair so retry() replays exactly it.
  • notifyComposerChanged() is payload-free and suppressed in secret mode. Composer text never crosses the frame boundary.
  • Chrome and settings helpers: close, minimize, dismissError, setLanguage, setTheme, emailTranscript, and more. See the BridgeOperations type for the full surface.

Use outage.connectivityLost as the connectivity signal. Do not substitute navigator.onLine. It reports false positives on VPN and virtual-adapter transitions.

Observation

client.subscribeKey(key, callback) fires only when one key's value changes. client.select(selector, callback) does the same for a derived projection. Both default to Object.is equality and accept a custom equals. Core reuses references for unchanged keys, so identity equality is sound for every key except chat.messages.

client.subscribeKey("chat.isGenerating", (generating) => {
  toggleTypingIndicator(generating === true);
});

Derivation helpers

The loaded module exports pure helpers mined from Ada's reference app: messageKey, isHistoricalRow, findFirstUnread, selectUnread, groupMessages, filterDisplayable, resolveBotName, resolveDisplayBotName, isAgentTyping, and isConnectivityLost.

resolveBotName is the attribution name. Use it for sender labels and announcements. It falls back to the bot handle.

resolveDisplayBotName is the display name for your own chrome, such as a header title or a greeting. It is empty when the customer configured no bot name, so a handle never reaches the page as a name.

resolveDisplayBotName is the one optional helper. The mounting core frame picks the runtime build, not your installed package version. A build older than this helper can therefore load. This happens during a rollout, behind an ?ada-messaging-version pin, or after a rollback. Guard the call:

const name = bridge.resolveDisplayBotName?.(state) ?? "";
const { messageKey, filterDisplayable, groupMessages } = bridge;
const rows = filterDisplayable(messages, state?.["chat.isConversationActive"] ?? false);
const grouping = groupMessages(rows);

Use messageKey as your render key. Raw message ids change twice: a streaming bubble swaps to its final id, and an optimistic send swaps to its durable id. Keying on the raw id replays animations and breaks unread anchors.

Options

| Option | Default | Purpose | | --- | --- | --- | | cdnBase | https://messaging-assets.ada.support | Asset origin for immutable bridge builds. It must use HTTPS. Plain HTTP works only for loopback development. Override it only when your Ada team directs you to during a joint validation. |

Frame-only asset URL resolution

resolveBridgeCdnUrl() returns the bridge asset URL selected for the current custom app frame. It uses the same validation as loadMessagingBridge().

This helper is frame-only. It throws a MessagingBridgeLoadError outside an Ada-mounted custom app frame. Call it after your app starts inside that frame. Do not evaluate it at module scope in code that also runs elsewhere.

Errors: loadMessagingBridge rejects with a MessagingBridgeLoadError. Its code property identifies the failure:

| Code | Meaning | | --- | --- | | invalid_cdn_base | The cdnBase option violates the HTTPS or loopback HTTP policy. | | missing_build_sha | The frame's window.name carries no core build marker, or the mounting core predates build-marker delivery. | | invalid_build_sha | The frame-name marker matches no accepted build identifier form. | | dev_marker_not_loopback | The frame uses the dev marker with a non-loopback asset host. | | bridge_import_failed | The asset failed to load (network, CSP, 404). | | bridge_module_invalid | The loaded module is not an Ada bridge build. |

Matching the core build

Ada core mounts your frame with window.name set to ada-custom-app:<build identifier> before your app code runs. Your URL and its fragment mount untouched, so hash routes keep working.

| Marker | Meaning | Resolved asset | | --- | --- | --- | | A bare 40-character hexadecimal SHA | An immutable production build. | <cdnBase>/<sha>/bridge/bridge.js | | Lowercase pr-<N> | An Ada-internal preview build. | <cdnBase>/pr-<N>/bridge/bridge.js | | dev | An Ada-internal local core build. Customer pages never receive this marker. | <cdnBase>/bridge/bridge.js on loopback only. |

The loader accepts only these marker shapes. It never accepts a URL or path from the frame name.

The loader snapshots window.name when your app first evaluates the package, through either entry: @ada-cx/messaging-bridge or @ada-cx/messaging-bridge/react. Keep that import in a module your page evaluates at startup. A route-level code split evaluates too late. Code that runs first, for example a library that uses window.name, could overwrite the name. If the route that renders AdaBridgeProvider is lazily loaded, add a bare import "@ada-cx/messaging-bridge"; to your entry module. The loader keeps the snapshot value if code overwrites window.name later.

Do not persist or replay the frame-name marker. Core supplies the authoritative value for every frame load.

Do not set or change window.name. The loader fails closed when your page runs outside an Ada-mounted custom app frame.

The dev marker and PR preview aliases are Ada-internal workflows. Local Ada core builds stamp ada-custom-app:dev, which works only with a loopback cdnBase. An Ada preview core selects its matching preview bridge automatically. You do not need a cdnBase override for a joint validation when the page loads sdk.js from the production asset root. A preview alias against an asset host that lacks the preview fails closed.

React

The ./react entry provides a provider and hooks. They delegate to a BridgeClient instance. They contain no runtime logic of their own.

The simplest path handles loading, the app.initialize handshake, and teardown for you:

import { createBridgeProvider, useBridgeState } from "@ada-cx/messaging-bridge/react";

const AdaBridgeProvider = createBridgeProvider();

function Root() {
  return (
    <AdaBridgeProvider fallback={<Spinner />}>
      <MyChatUi />
    </AdaBridgeProvider>
  );
}

function MyChatUi() {
  const state = useBridgeState();
  return <MessageList messages={state?.["chat.messages"] ?? []} />;
}

If your app lazily loads the route that renders AdaBridgeProvider, add a bare import "@ada-cx/messaging-bridge"; to your entry module. The loader then snapshots the frame name at startup, before other code can overwrite window.name. A statically imported createBridgeProvider already evaluates the loader through a used binding; the bare import matters only when that import itself lives in a lazily loaded chunk.

The provider accepts two more props for load failures. errorFallback renders when the runtime fails to load. onError receives the MessagingBridgeLoadError. Without them, the provider logs the error and keeps rendering fallback.

To own the lifecycle yourself, pass a client you created:

import { BridgeProvider } from "@ada-cx/messaging-bridge/react";

<BridgeProvider client={client}>
  <MyChatUi />
</BridgeProvider>

With an injected client, you send app.initialize and call destroy() yourself.

Hooks: useBridgeClient(), useBridgeState(), and useBridgeStateKey(key). useBridgeStateKey re-renders only when its key's value changes. useBridgeState re-renders on every state update. Reach the operations layer through useBridgeClient().operations.

Testing

The ./testing entry provides a scriptable in-memory client for unit tests. It performs no postMessage and no network access.

import { createMockBridgeClient } from "@ada-cx/messaging-bridge/testing";

const mock = createMockBridgeClient();
render(<BridgeProvider client={mock}><MyChatUi /></BridgeProvider>);

mock.updateState({ "chat.isSending": true });
expect(mock.sentEvents).toContainEqual({
  event: "chat.message.send",
  payload: expect.objectContaining({ body: "Hello" }),
});

Mock helpers: setState, updateState, sentEvents, clearSentEvents, subscriberCount, and destroyed.

The mock implements the full client surface, operations included. Every operation records its events in sentEvents. A correlated handle resolves when your test scripts the answering state change:

const handle = mock.operations.sendMessage("Hello");
mock.updateState({
  "chat.messages": [
    { type: "text", id: "m1", sender: "user", timestamp: 1,
      body: "Hello", clientKey: handle.tempMessageUuid },
  ],
});
await handle.settled();

Contract notes

  • AppDisplayState is the full state shape core publishes. AppEvents maps each sendable event to its payload type.
  • app.initialize is mandatory. Send it within 15 seconds of frame load.
  • The runtime derives the core origin from document.referrer, or locks it on the first valid state message. It rejects state from other origins.
  • In Ada's custom-app frame, the first app.initialize hands core a MessagePort. All later state and events ride that port. The port is pinned to your first document, so your app must not navigate or reload its own frame. A navigation disconnects the bridge permanently.
  • The runtime detects the custom-app frame by the frame name core sets (ada-custom-app:<build identifier>), which also selects the bridge build the loader imports. Do not change window.name inside your app document.
  • The custom-app frame keeps your real origin, so your own cookies and storage work inside it. Browsers partition third-party storage by the embedding site. Cookies need Partitioned; Secure; SameSite=None.
  • The custom-app frame needs document.referrer to target the handshake. Core provides the referrer by default. If a browser extension or policy strips the Referer header, the handshake cannot send. The runtime logs an error, and core falls back to Ada's default app after 15 seconds.

Support

This package supports Ada's custom app program. Open issues through your Ada support contact.