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

@dialpad/dialapp-external-sdk

v0.4.2

Published

Headless SDK (no UI components) for partner apps in Dialpad's right sidebar. It talks to the Dialpad web and desktop apps over postMessage, keeps the current user, contact, call and theme in one immutable state snapshot, reports the panel's height and con

Readme

@dialpad/dialapp-external-sdk

The SDK for partner apps in Dialpad's right sidebar. It's headless: it ships no UI components, so you build the panel with your own (the reference app uses Vue 3 and Dialtone 10). The SDK owns everything that talks to Dialpad:

  • The host protocol over postMessage, which you never call directly.
  • One immutable state snapshot: the Dialpad user, the contact being viewed, the active call, the theme, and the connection to your system.
  • Connect: linking the Dialpad user to your system (for example, your CRM) through an OAuth popup, completed by polling your backend.
  • The panel's height, and the menu you give the panel's header in Dialpad.
  • Mock Dialpad hosts for unit tests and browser tests.

Your backend renders the panel page and puts the page data this SDK reads into it. The reference app, sample-dialapp-external, shows how in its server/, and its guide/ walks through building an app.

Install

npm install @dialpad/dialapp-external-sdk

The /vue and /vue/testing entry points need vue 3.3 or later, and /vue/testing needs @vue/test-utils 2.4 or later; the other entry points need neither. While the SDK is 0.x, a minor version can change what your app sees, and a patch version doesn't.

Supported browsers: Chrome 109 and later, and the Dialpad desktop app. The package is ES modules only; use a bundler.

The panel is 318 pixels wide in the right sidebar by default (266 to 414 as the user resizes it) and a fixed 230 pixels in the mini call bar, so lay it out for 230.

How it fits together

  1. Dialpad frames your panel page with ?idtoken=…. Your backend decodes it, renders the page, and puts the page data in it: an access token you issue and the user's external connection to your system. The reference app's dialpad/idtoken.py and dialpad/panel_page.py do both.
  2. In the page, createRightbar() reads that page data, completes the handshake with Dialpad, and loads the active call, theme, Dialpad user and contact.
  3. Your UI reads rightbar.state and calls the SDK's actions. Requests to your own API carry rightbar.authHeaders(), so your backend knows the user.
  4. For Connect, your backend serves two endpoints under /dialpad, which the SDK calls with that header: GET /dialpad/ext-connection, answering with an ExternalConnection ({ connected, authorization_urls, name, scopes }), and POST /dialpad/ext-disconnect, answering 204 with no body. The contract is the package's openapi.json (@dialpad/dialapp-external-sdk/openapi.json).

Start

import { createApp } from 'vue';
import { createRightbar } from '@dialpad/dialapp-external-sdk';
import { rightbarPlugin } from '@dialpad/dialapp-external-sdk/vue';
import App from './App.vue';
import { api } from './api'; // your own HTTP module, with the fetch signature

const root = document.getElementById('app')!;
const rightbar = await createRightbar({ heightRoot: root, fetch: api });
createApp(App).use(rightbarPlugin, rightbar).mount(root);

createRightbar() rejects with a RightbarError whose code is bad_options, not_framed (the page isn't open inside Dialpad), bad_page_data or init_timeout. Show a short "Open this panel from Dialpad" message in that case.

In a component:

const { state, authorize, disconnect } = useRightbar();
const contact = computed(() => state.value.contact);

Without Vue, read the state and follow its changes yourself:

const rightbar = await createRightbar({
  heightRoot: document.getElementById('app')!,
});
rightbar.watch(
  (state) => state.call,
  (call) => renderCall(call),
);
rightbar.watch(
  (state) => state.contact,
  (contact) => renderContact(contact),
);

State

rightbar.state;
// { ready, user, contact, call, theme: { mode, brand },
//   connection: { connected, pending, authorizationUrls, name, scopes, error } }

Every change produces a new snapshot that keeps the references of unchanged parts. rightbar.subscribe((next, prev) => …) and rightbar.watch((s) => s.contact, (contact) => …) follow it. connection.error is null, { code: 'request_failed', status }, { code: 'network' } or { code: 'timeout' }.

user is what Dialpad's get/user gives at start (IDs, names, emails, phone numbers, licenses, admin flags and availability), or null. Dialpad sends no event when it changes, so its status fields are as of the panel's start. connection.scopes lists the OAuth scopes your client has, so you can hide actions the user can't take.

When the contact changes

contact changes while the panel runs. When the user opens another contact, Dialpad keeps your panel loaded and sends the new contact (or none, null): the panel stays connected, and its menu, refresh button and height stay as they were. Watch state.contact and, on each change:

  • Unlink at once with linkRecord(null). Dialpad clears the link itself, but a link you sent for the previous contact just before the change can land on the new one.
  • Show a loading state, never the previous contact's record, until your lookup of the new contact answers. Ignore a lookup that answers for a contact no longer shown.
  • Link the new contact's record once you find it.

refreshContact doesn't fire for a new contact: it means the refresh button, or a change to the open contact's record in Dialpad.

Actions and events

| Call | Does | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | authorize(client?) | Asks Dialpad to open an authorize URL; polls until connected or 2 minutes | | refresh() / disconnect() | Calls your backend's connection and disconnect endpoints | | authHeaders() | { Authorization: 'Bearer <access token>' } for your own API calls | | setMenu([{ id, label }, …]) / setMenu([]) | Sets the panel's menu in Dialpad to exactly these items, in order (see The panel menu) | | on('menuSelect', (id) => …) | The user picked the menu item with this id; returns an unsubscribe function | | linkRecord({ type, url }) / linkRecord(null) | Links a record in your system: Dialpad shows View <type> in the panel's header | | showRefreshButton(show), showToast({ text, type }), screenPop() | Dialpad's refresh button, toasts and screen pops | | on('refreshContact', callback) | Dialpad's header refresh button or a change to the contact's record; returns an unsubscribe function | | createRightbar({ onDisconnect }) | Called after disconnect() succeeds |

The panel menu

Dialpad draws a menu in your panel's header, and you decide what's in it: each item is your own { id, label }. When the user picks one, your menuSelect listener gets its id. Dialpad gives no id a meaning of its own, so a Disconnect item does whatever your listener does, usually disconnect():

rightbar.on('menuSelect', (id) => {
  if (id === 'disconnect') void rightbar.disconnect();
  if (id === 'rematch') rematch(); // your own: forget the matched record, look it up again
});

rightbar.watch(
  (state) => state.connection.connected,
  (connected) => {
    const items = connected
      ? [
          { id: 'rematch', label: t('Rematch') },
          { id: 'disconnect', label: t('Disconnect') },
        ]
      : [];
    rightbar.setMenu(items).catch(console.error);
  },
);
  • The menu is exactly what you set, in that order, whether or not the user is connected; [] means no menu. Each call replaces the last. A reloaded panel has no menu until it calls setMenu(), so set it whenever the panel starts, as the watch above does. The SDK never sets it for you.
  • The labels are yours to translate. Dialpad shows them as plain text, never HTML, and cuts off one too wide for the menu with an ellipsis. The user's language is state.user.language (a Dialpad language code such as en, de or ja), read when the panel starts; fall back to your default language when you have no translation.
  • Dialpad's limits: at most 5 items; each id 1 to 64 characters and unique in the menu; each label 1 to 40 characters, not only whitespace. More than 5 items rejects the whole call, and the menu stays as it was. An item that breaks the other rules, or repeats an earlier id, is left out, and the SDK logs one warning naming the ids left out.
  • View <type> in the header follows linkRecord() alone, whatever the menu holds.
  • The SDK doesn't tell Dialpad whether the user is connected: your menu says what the user can do.

Connect

Connect links the Dialpad user to their account in your system (for example, your CRM) through your own OAuth flow.

  1. Your Connect button calls authorize(). Dialpad opens the URL in connection.authorizationUrls (a browser tab on desktop, a popup on the web), and connection.pending turns true. When a company has several OAuth clients, show their names and pass the chosen one: authorize('sandbox').
  2. The user signs in. Your OAuth callback stores their tokens and returns the close-popup page (the reference app's connect/close_popup.py).
  3. Meanwhile the SDK checks GET /dialpad/ext-connection after 7 seconds, then every 5 seconds. A failed check doesn't end Connect; the next one decides.
  4. When /dialpad/ext-connection answers connected: true, the state turns connected: true, pending: false. After 2 minutes without that, Connect stops with pending: false and error: { code: 'timeout' }; offer to try again.

Disconnect works the same way from your button and from a Disconnect item in your menu, as both call disconnect(): the SDK calls POST /dialpad/ext-disconnect, shows the panel as not connected (keeping the authorization URLs, so Connect shows right away, and checking /dialpad/ext-connection for current ones when the answer has no body), and calls onDisconnect. If the call fails, the state keeps connected and sets connection.error.

Your backend's side

Your backend implements its side of the contract in any stack; the reference app's server/ is one implementation to copy.

| Endpoint | Authenticated by | Answers | | -------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | GET <conversation sidebar URI> | ?idtoken=<JWE> | 200 the page, with the headers below; 401 a minimal error page when the token doesn't decode; 403 when you refuse the user | | GET /dialpad/ext-connection | Authorization: Bearer <token> | 200 an ExternalConnection | | POST /dialpad/ext-disconnect | Authorization: Bearer <token> | 204 with no body (a 200 ExternalConnection also works) | | Your OAuth callback | Your own OAuth state | 200 the close-popup page, after storing the tokens; the path is yours |

The idtoken. A JWE (dir, A192CBC-HS384, keyed with the first 48 characters of your client secret) wrapping an HS256 JWT signed with the whole secret. Allow only those algorithms, and answer 401 on any failure. It carries the Dialpad user (user_id, company_id, office_id, and profile fields) and an api_key for Dialpad's public API. It never expires and neither does the key, so keep the key on your server and never log the page's query string. Drop the csr claim and ignore claims you don't know.

Your access token. The idtoken authenticates only the page request. While rendering the page, issue or look up your own access token for that Dialpad user and put it in the page data; the SDK sends it as a bearer token, and authHeaders() gives the same header to your own calls. A 401 from your backend means your token was rejected, and what the panel does then is up to you; a 401 from Dialpad's API means the api_key no longer exists, so drop it.

The page data, JSON-encoded with <, >, & and ' escaped, in the page you render:

<script type="application/json" id="dialpad-page-data" nonce="…">
  {"version": 1, "dialpad_origin": "https://dialpad.com",
   "access_token": "…", "external_connection": {…}, "app": null}
</script>

app is passed through to rightbar.app for your own bootstrap data.

Headers on the page: Content-Security-Policy with frame-ancestors <dialpad origin>, script-src 'self' 'nonce-<nonce>' (plus your asset origin), object-src 'none' and base-uri 'none'; a fresh nonce per request on every inline script; Referrer-Policy: no-referrer, so the idtoken in the URL doesn't leak to your asset hosts; Cache-Control: no-store; and X-Content-Type-Options: nosniff. The Dialpad origins are https://dialpad.com and, for beta, https://dialpadbeta.com.

The close-popup page is required. It posts close_popup to its opener, to the Dialpad origin only, when it has one, then closes itself, with a short "you can close this window" fallback. On the web that resolves Dialpad's pending Connect request; the desktop app's login window has no opener, so there the page only closes it and the SDK's polling completes Connect.

The package's openapi.json (@dialpad/dialapp-external-sdk/openapi.json) has the full schemas.

Testing

| Helper | Entry point | | -------------------------------------------------------------------------------------- | ---------------- | | createMockHost(), with host.createRightbar(), advance(), settle(), received | /testing | | mountMockHostPage({ src }) for real browsers (Playwright, a dev page) | /testing | | makePageData(), makeUser(), makeContact(), makeCall(), disposeAllMockHosts() | /testing | | Setup file for Vitest or Jest | /testing/setup | | mountWithRightbar(component, options) | /vue/testing |

// vitest.config.ts
test: { environment: 'jsdom', globals: true, setupFiles: ['@dialpad/dialapp-external-sdk/testing/setup'] }

createMockHost() runs in memory with its own clock, so a Connect test calls await host.advance(7_000) instead of using fake timers. mountMockHostPage() frames your real page, whose backend renders the page data from a mock idtoken (the reference app's local mode serves such a URL at /dev/frame-url), so it takes no page data itself.

Both mock hosts answer as Dialpad does, with its limits, and record what the panel asked for in host.received: linkedRecord, refreshButtonShown, toasts, screenPops, authorizeRequests, every message, and menu: the items Dialpad kept from the panel's latest setMenu(), as { id, label }, or null until the panel sets one after its latest init. To play the user, call host.selectMenuItem(id), which picks an item of that menu (it returns false, sending nothing, for an id not in it), and host.trigger('refresh_contact') for the header's refresh button.

host.sendContact(contact) opens another contact, as Dialpad does: it clears received.linkedRecord, and the panel gets the new contact in state.contact without a reload or a refreshContact. host.updateContact(contact) changes the open contact's record instead: the host sends its silent refresh, so the contact is reloaded and refreshContact fires.

A first test, with Vitest and the setup file above:

import { expect, test } from 'vitest';
import {
  createMockHost,
  makeCall,
  makeContact,
} from '@dialpad/dialapp-external-sdk/testing';

test('shows the call as it connects', async () => {
  const host = createMockHost({ contact: makeContact({ displayName: 'Ada' }) });
  const rightbar = await host.createRightbar({
    heightRoot: document.createElement('div'),
  });
  expect(rightbar.state.contact?.displayName).toBe('Ada');

  host.sendCall(makeCall({ state: 'connected' }));
  await host.settle();
  expect(rightbar.state.call?.state).toBe('connected');

  await rightbar.linkRecord({ type: 'Contact', url: 'https://crm.example/1' });
  expect(host.received.linkedRecord?.url).toBe('https://crm.example/1');
});

Pass fetch to host.createRightbar() to answer the Connect endpoints. With globals: true (or in Jest) the setup file disposes every mock host after each test; otherwise call disposeAllMockHosts() in your own afterEach.

License

MIT, copyright 2026 Dialpad, Inc. The full text is in the LICENSE file in the installed package (node_modules/@dialpad/dialapp-external-sdk/LICENSE).