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

@callcorpacd/platform-bridge

v1.9.1

Published

Guest/host postMessage bridge for sandboxed partner content in the CallCorp portal.

Readme

@callcorpacd/platform-bridge

Guest/host postMessage bridge so partner content in a sandboxed iframe can call portal APIs without receiving access tokens.

Used by the Vue3 SandboxedApp control. Designed to grow into a published npm package (partners + internal apps).

Authoring a guest app (create / localhost / publish): see AUTHORING.md.

Install (local / monorepo)

Vue3 already depends on this package via file:../PlatformBridge and Vite aliases:

  • @callcorpacd/platform-bridge
  • @callcorpacd/platform-bridge/client
  • @callcorpacd/platform-bridge/host
  • @callcorpacd/platform-bridge/protocol

Guest (inside the iframe)

import { createPlatformBridge } from '@callcorpacd/platform-bridge/client';

const platform = await createPlatformBridge();

// Document Params / Route (interpolated on the host) arrive on handshake:
const { RecordId, Mode } = platform.context.params;
const path = platform.context.route;
platform.context.subscribe((params) => {
  // Live updates when host Params or Route re-evaluate
  // Read platform.context.route for the current path
});

const data = await platform.api.get('api/v1/users/me/full-name');
await platform.api.post('api/v1/some-resource', { hello: 'world' });

const label = await platform.ui.translate('Save');
const canEdit = await platform.ui.hasAccess('guid-of-access-permission');
await platform.events.runEventActions('OnSave', { id: RecordId });

// Realtime feeds (display-agnostic snapshots; host owns the WebSocket):
const kpi = await platform.realtime.open({ kind: 'widget', name: 'QueueDepth' });
kpi.subscribe(({ data, revision }) => {
  // render however you want — no grid dependency
});
await kpi.close();

// Server/session events (host owns SignalR + Eventing Subscribe):
const callSub = await platform.serverEvents.subscribe(
  { sessionId: platform.context.params.SessionId, names: ['CallDisconnect'] },
  (event) => { /* { sessionId, name, time, content, id } */ }
);
await callSub.unsubscribe();

// Portal EventBus (subscribe-only):
const busSub = await platform.bus.subscribe('SelectedCustomerChanged', (payload) => {});
await busSub.unsubscribe();

v4 surface:

  • platform.protocolVersion / platform.capabilities — negotiated version and host-advertised feature ids from ReadyAck
  • platform.api.get | cachedGet | post | put | patch | delete
  • platform.context.params — Record<string, unknown> from host ControlData.Params (top-level primitives are strings; nested objects/arrays preserved)
  • platform.context.route — string from host ControlData.Route (Shared Panel SPA path)
  • platform.context.subscribe(listener) — notified on bridge.context.update (Params and/or Route)
  • platform.theme.get() / platform.theme.subscribe(listener) — active portal / white-label color snapshot ({ colors, name }); also on ReadyAck
  • platform.realtime.open({ kind, name, … }) — widget or named-list feed; returns a handle with data, status, subscribe, setQuery (lists), close
  • platform.serverEvents.subscribe({ sessionId, names? }, listener) — session events; returns { subscriptionId, unsubscribe() }
  • platform.bus.subscribe(name, listener) — portal EventBus; returns { subscriptionId, unsubscribe() }
  • platform.ui.translate(text, isToolTip?) — portal translation via host Translate
  • platform.ui.hasAccess(id, name?) — UI AccessPermission check via host utils.document_cache.hasAccess
  • platform.events.runEventActions(name, data?) — runs matching ControlData.Events actions on the host SandboxedApp (document actions only — not EventBus/server-event pub/sub)

Apply theme CSS vars in the guest with @callcorpacd/design-tokens → applyBridgeTheme(platform).

Reserved (not implemented on the host yet): platform.navigate. To extend the bridge, see Extending the bridge.

Published API

Prefer curated /api/v1/... routes for guest demos and partner apps (e.g. api/v1/users/me/full-name). Discover the contract at:

  • OpenAPI: {apiBase}/api/v1/openapi.json
  • Human docs (Scalar): {apiBase}/api/v1/docs

Allowlist published paths with AllowedApiPrefixes: ["api/v1/"] (or a narrower prefix). Legacy Apps/... remain callable when explicitly allowlisted.

Security

  • Do not call the band API with fetch and a token from guest code; use the bridge.
  • The host allowlists paths via ControlData.AllowedApiPrefixes on SandboxedApp.
  • Realtime feeds are gated by AllowAllRealtimeFeeds or AllowedRealtimeFeeds (see below).
  • Server/session events are gated by AllowAllServerEvents or AllowedServerEventNames.
  • EventBus subscriptions are gated by AllowAllBusEvents or AllowedBusEvents.
  • Absolute http(s) URLs are rejected by default.
  • Do not put secrets or tokens in Params.

Host (portal)

import { createHostBridgeHandler } from '@callcorpacd/platform-bridge/host';
import api from '@/Services/api';

const handler = createHostBridgeHandler({
  getAllowedOrigin: () => contentOrigin,
  // Required when multiple same-origin iframes are mounted — scopes Ready/Params to this frame.
  getGuestWindow: () => iframeEl?.contentWindow || null,
  getAllowedApiPrefixes: () => ['api/v1/', 'Document/'],
  getContextParams: () => ({ RecordId: '123', Mode: 'edit' }),
  getContextRoute: () => selectedMenuName || '',
  getTheme: () => ({ colors: theme.getActiveTheme(), name: theme.getActiveThemeName() }),
  getRealtimeSource: () => globalVars.RTData,
  getAllowAllRealtimeFeeds: () => false,
  getAllowedRealtimeFeeds: () => [{ kind: 'widget', name: 'QueueDepth' }],
  watchRealtimeView: (viewKey, cb) => /* Vue $watch → unwatch */,
  ui: {
    translate: (text, isToolTip) => /* portal Translate */,
    hasAccess: (id, name) => /* utils.document_cache.hasAccess(name || '', { ID: id, Enabled: true }) */,
  },
  events: { runEventActions: (name, data) => /* SandboxedApp.runEventActions */ },
  getAllowAllServerEvents: () => false,
  getAllowedServerEventNames: () => ['CallDisconnect'],
  subscribeServerEvents: (opts, onEvent) => /* eventCoordinator → teardown */,
  getAllowAllBusEvents: () => false,
  getAllowedBusEvents: () => ['SelectedCustomerChanged'],
  subscribeBus: (opts, onPayload) => /* EventBus.$on → teardown */,
  api,
});
window.addEventListener('message', handler.handleMessage);
// Later, when params change:
handler.pushContextUpdate();

SandboxedApp.jsx wires this for you from ControlData (Params, Route, RTEventSource, AllowAllRealtimeFeeds, AllowedRealtimeFeeds, AllowAllServerEvents, AllowedServerEventNames, AllowAllBusEvents, AllowedBusEvents).

Passing params from a UserControl document

ControlData.Params is a map of JSON-serializable values. String fields (and other top-level primitives) may use portal interpolations ({{...}} or {#...#}). The host evaluates them and delivers the result as platform.context.params.

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerApp",
    "SourceKind": "PlatformFile",
    "ContentPath": "PartnerContent/hello/index.html",
    "AllowedApiPrefixes": ["api/v1/"],
    "Params": {
      "RecordId": "{{ParamByName(\"Id\")}}",
      "Mode": "edit",
      "Title": "Hello {# Root.UserName #}",
      "AnotherArg": {
        "ChildField": "And Value",
        "ChildNumber": 123
      }
    }
  }
}

Keys are static identifiers. Top-level primitives are coerced to strings (null/undefined → ''). Nested objects/arrays are preserved (including nested numbers, booleans, and null). Use platform.api.* for large or dynamic data — do not put secrets in Params.

Passing route for Shared Panel SPAs

ControlData.Route is a single interpolatable string (same {{ }} / {# #} rules as Url). The host delivers it as platform.context.route on handshake and pushes updates on bridge.context.update whenever the expression re-evaluates (for example when CallCenterHome updates Control.SelectedMenuName).

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerSpa",
    "SourceKind": "PlatformFile",
    "ContentPath": "PartnerContent/spa/index.html",
    "AllowedApiPrefixes": ["api/v1/"],
    "Route": "{{Control.SelectedMenuName}}",
    "Params": {
      "Mode": "edit"
    }
  }
}
const platform = await createPlatformBridge();
console.log(platform.context.route);
platform.context.subscribe(() => {
  // Params and/or Route changed — drive vue-router / show-hide panels from context.route
  router.replace(platform.context.route || '/');
});

Scaffold a demo with create-callcorp-sandbox and --starter route on any stack.

Realtime feeds

platform.realtime exposes the portal RTEventHandler pipeline as data only — full snapshots, no UI opinions. The host page must still host an RTEventHandler that publishes the source (typically GlobalVars.RTData).

const agents = await platform.realtime.open({
  kind: 'widget',
  name: 'AgentStatusSummary',
  filters: { CenterId: '...' },
});
console.log(agents.data); // opaque JSON
agents.subscribe(({ data, revision, at }) => { /* … */ });
await agents.close();

const queue = await platform.realtime.open({
  kind: 'list',
  name: 'ActiveInteractions',
  columns: ['Id', 'State', 'StartTime'],
  filters: { _any_: 'smith' },
  sort: { field: 'StartTime', descending: true },
  range: { start: 0, length: 50 },
});
// queue.data => { total, rows }
await queue.setQuery({ range: { start: 50, length: 50 } });
await queue.close();

| Kind | Snapshot shape | RTEventHandler API | |------|----------------|--------------------| | widget | Opaque JSON (object or array) | AddView / RemoveView | | list | { total: number, rows: object[] } | GetNamedListForRealTime / RemoveNamedListForRealTime |

SandboxedApp ControlData

| Field | Role | |-------|------| | RTEventSource | Expression for the RT accessor (default GlobalVars.RTData) | | AllowAllRealtimeFeeds | When true, any kind/name is permitted; supersedes AllowedRealtimeFeeds | | AllowedRealtimeFeeds | [{ kind, name }, …] allowlist when AllowAllRealtimeFeeds is false/absent |

Default both unset → no realtime feeds (not_allowed).

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "RealtimeDashboard",
    "SourceKind": "PlatformFile",
    "ContentPath": "PartnerContent/realtime/index.html",
    "RTEventSource": "GlobalVars.RTData",
    "AllowAllRealtimeFeeds": false,
    "AllowedRealtimeFeeds": [
      { "kind": "widget", "name": "AgentStatusSummary" },
      { "kind": "list", "name": "ActiveInteractions" }
    ],
    "AllowedApiPrefixes": ["api/v1/"]
  }
}

Trusted embeds may set "AllowAllRealtimeFeeds": true and omit the list. See samples/realtime/ for a snapshot-logging guest.

Server events and EventBus

platform.serverEvents exposes session events from the host WebSocket / serverEventCoordinator pipeline. Guests never talk to SignalR or Apps/Eventing/* directly.

const sub = await platform.serverEvents.subscribe(
  {
    sessionId: platform.context.params.SessionId,
    names: ['InboundAnswered', 'CallDisconnect'], // omit = all names (requires AllowAllServerEvents)
  },
  (event) => {
    // { sessionId, name, time, content, id }
  },
);
await sub.unsubscribe();

platform.bus listens to the portal in-memory EventBus (subscribe-only — no guest emit):

const sub = await platform.bus.subscribe('SelectedCustomerChanged', (payload) => {});
await sub.unsubscribe();

platform.events.runEventActions remains a separate document-action RPC on ControlData.Events. It is not EventBus or server-event pub/sub.

SandboxedApp ControlData

| Field | Role | |-------|------| | AllowAllServerEvents | When true, any session event name (including “all names”) is permitted | | AllowedServerEventNames | string[] allowlist when allow-all is false; empty/absent → no server-event subscriptions | | AllowAllBusEvents | When true, any bus channel name is permitted | | AllowedBusEvents | string[] exact name allowlist when allow-all is false; empty/absent → no bus subscriptions |

Default all unset → subscriptions rejected (not_allowed). Prefer passing sessionId via interpolatable Params. See samples/events/.

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "CallListener",
    "SourceKind": "PlatformFile",
    "ContentPath": "PartnerContent/events/index.html",
    "Params": {
      "SessionId": "{{ParamByName(\"SessionId\")}}"
    },
    "AllowedServerEventNames": ["InboundAnswered", "CallDisconnect"],
    "AllowedBusEvents": ["SelectedCustomerChanged"],
    "AllowedApiPrefixes": ["api/v1/"]
  }
}

Protocol v4

| Type | Direction | Purpose | |------|-----------|---------| | bridge.ready | guest → host | Handshake | | bridge.ready.ack | host → guest | Handshake complete; payload includes { protocolVersion, negotiatedVersion, minProtocolVersion, capabilities, params, route, theme? } | | bridge.ready.reject | host → guest | Handshake rejected; e.g. { code: 'version_mismatch', min, max, guestVersion } | | bridge.context.update | host → guest | { params, route } when host Params and/or Route change | | bridge.theme.update | host → guest | { colors, name } theme push | | bridge.api.request | guest → host | { method, url, data? } | | bridge.api.response | host → guest | { result } | | bridge.api.error | host → guest | { message, code, status? } | | bridge.realtime.open | guest → host | { kind, name, filters?, columns?, sort?, range? } | | bridge.realtime.open.result | host → guest | { subscriptionId, snapshot } | | bridge.realtime.setQuery | guest → host | { subscriptionId, filters?, columns?, sort?, range? } | | bridge.realtime.setQuery.result | host → guest | { snapshot } | | bridge.realtime.close | guest → host | { subscriptionId } | | bridge.realtime.close.result | host → guest | {} | | bridge.realtime.update | host → guest | { subscriptionId, snapshot, revision } (push) | | bridge.realtime.status | host → guest | { subscriptionId?, status, message? } (push) | | bridge.realtime.error | host → guest | { message, code } | | bridge.serverEvents.subscribe | guest → host | { sessionId, names? } | | bridge.serverEvents.subscribe.result | host → guest | { subscriptionId } | | bridge.serverEvents.unsubscribe | guest → host | { subscriptionId } | | bridge.serverEvents.unsubscribe.result | host → guest | {} | | bridge.serverEvents.event | host → guest | { subscriptionId, event } (push) | | bridge.serverEvents.error | host → guest | { message, code } | | bridge.bus.subscribe | guest → host | { name } | | bridge.bus.subscribe.result | host → guest | { subscriptionId } | | bridge.bus.unsubscribe | guest → host | { subscriptionId } | | bridge.bus.unsubscribe.result | host → guest | {} | | bridge.bus.event | host → guest | { subscriptionId, payload } (push) | | bridge.bus.error | host → guest | { message, code } | | bridge.ui.request | guest → host | { method, args } (e.g. translate) | | bridge.ui.response | host → guest | { result } | | bridge.ui.error | host → guest | { message, code } | | bridge.events.request | guest → host | { method, args } (e.g. runEventActions) | | bridge.events.response | host → guest | { result } | | bridge.events.error | host → guest | { message, code } |

Channel id: callcorp.platform-bridge. Version field must be in [MIN_PROTOCOL_VERSION, PROTOCOL_VERSION] (currently both 4). The host negotiates the guest's version and replies on that session version. Out-of-range handshakes get bridge.ready.reject (version_mismatch) instead of a silent timeout.

ReadyAck also includes optional capabilities (string ids) and negotiatedVersion so guests can feature-detect without a protocol bump. Older guests ignore unknown ReadyAck fields.

Extending the bridge (maintainers)

Partner guests only see the published client surface. New host capabilities (portal helpers, navigation, UI chrome, etc.) require coordinated changes across four layers:

| Layer | File | Responsibility | |-------|------|----------------| | Protocol | src/protocol.js | MessageType / payload shapes; bump PROTOCOL_VERSION when shapes or required behavior change | | Guest | src/client.js | Public platform.* API; request/response correlation | | Host | src/host.js | Origin checks, dispatch, call into injected portal callbacks | | Portal wiring | Vue3/.../SandboxedApp.jsx | Pass host callbacks into createHostBridgeHandler (component mixins, api, params, …) |

samples/hello/ mirrors protocol/client for offline demos—update it when you change the wire format.

Design rules

  • Prefer a request → response/error pair with requestId (same pattern as bridge.api.*). Host → guest pushes (like bridge.context.update) are for broadcasts only.
  • Keep auth and secrets on the host. Guests receive only safe, intentional results.
  • Put portal UI helpers under the reserved platform.ui namespace; navigation under platform.navigate.
  • Session/WebSocket events live under platform.serverEvents; portal EventBus under platform.bus. Keep platform.events for document-action RPC (runEventActions) only.
  • When changing the guest surface, keep scaffolded template AGENTS.md files in create-callcorp-sandbox/templates/** in sync (see AUTHORING.md and AGENTS.md).
  • Additive fields on existing messages (e.g. params / capabilities on ReadyAck) can stay on the current protocol version if older guests ignore unknown payload keys. New message types or required guest behavior → bump PROTOCOL_VERSION; host accepts [MIN_PROTOCOL_VERSION, PROTOCOL_VERSION] and sends bridge.ready.reject on mismatch. Raise MIN_PROTOCOL_VERSION only when deliberately force-upgrading old guests.
  • New optional features → ReadyAck capabilities + guest docs — not a protocol bump.
  • Wire host logic through options callbacks on createHostBridgeHandler so the package stays free of Vue/methods.js imports. SandboxedApp closes over this.

Checklist: add a host method

  1. Protocol — Add message types in MessageType (e.g. bridge.ui.request / bridge.ui.response / bridge.ui.error, or a dedicated bridge.ui.translate if you want a one-off). Document the payload.
  2. Client — Implement the guest method (usually async), post the request, resolve/reject from the matching response/error. Replace any notImplemented stub on that namespace.
  3. Host — In handleMessage, accept the new type (today non-ApiRequest traffic is ignored after Ready). Validate payload, call an injected option (e.g. ui.translate), reply with result or error. Never trust the guest for authorization decisions the host must enforce.
  4. SandboxedApp — Pass that option when creating the handler so the host can use component methods/services.
  5. Docs / samples / agents — Update this README’s guest surface list, AUTHORING.md if partners should call it, samples/** mirrors, AGENTS.md (this package), scaffold template AGENTS.md files, and in-repo UIPages/*/AGENTS.md when protocol/capability text drifts.
  6. Version — Prefer staying on the current PROTOCOL_VERSION for additive work; bump package semver as needed. Bump PROTOCOL_VERSION only for incompatible wire breaks; adjust MIN_PROTOCOL_VERSION only when forcing upgrades.

Implemented: platform.ui.translate, platform.ui.hasAccess, and platform.events.runEventActions

These follow the checklist above (generic bridge.ui.* / bridge.events.* RPC).

const label = await platform.ui.translate('Save');
const canEdit = await platform.ui.hasAccess('guid-here');
// optional debug label: await platform.ui.hasAccess('guid-here', 'PartnerApp');
await platform.events.runEventActions('OnSave', { id: '123' });

SandboxedApp.attachBridge injects:

ui: {
  translate: (text, isToolTip) => this.Translate(text, isToolTip),
  hasAccess: (id, name) => utils.document_cache.hasAccess(name || '', { ID: id, Enabled: true }),
},
events: {
  runEventActions: (name, data) => this.runEventActions(name, data),
},

Notes:

  • Translate reads portal translation lists / controlData.ShouldTranslate on the host instance—do not reimplement translation inside the iframe. Return only the translated string (or the original text).
  • Tooltip flag (isToolTip) is optional and boolean-coerced on the host.
  • hasAccess evaluates against the host permission table (Apps/Permissions/GetUIElementsPermissionTable). Pass the AccessPermission id string (required) and an optional debug name; the host wraps it as { ID: id, Enabled: true }.
  • runEventActions looks up ControlData.Events by Name and runs that action list; unknown names are a no-op. Guest data is JSON-cloned before it reaches the host.

The same pattern applies to other mixin helpers or portal services: add protocol + client method + host dispatch + a thin SandboxedApp callback that closes over this / imported services.

Deploying partner content

Preferred path for new apps: scaffold with create-callcorp-sandbox, npm run build, then callcorp-sandbox publish (see AUTHORING.md). Vite dist/ uses base: './'; SandboxedApp PlatformFile mode rewrites relative JS/CSS asset URLs to authenticated blob URLs.

Content path convention: PartnerContent/{AppId}/[version/]index.html

Platform FileAPI (recommended for testing)

Upload via CLI publish or the file browser. Minimal hand-built sample: samples/hello/.

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerApp",
    "SourceKind": "PlatformFile",
    "ContentPath": "PartnerContent/hello/index.html",
    "AllowedApiPrefixes": ["api/v1/"],
    "Params": {
      "Greeting": "Hello from the host"
    }
  }
}

Named sandbox (cross-tenant)

SourceKind: SandboxName loads content owned by another tenant via a sandbox lookup ID. The host fetches through Apps/SandboxedApp/File/{*FilePath}?ID=<SandboxName>; the server resolves the PartnerContent root. ContentPath is relative to that root (e.g. index.html).

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerApp",
    "SourceKind": "SandboxName",
    "SandboxName": "f92aa3cb-257f-41ea-ab20-9f31ebd97115",
    "ContentPath": "index.html",
    "AllowedApiPrefixes": ["api/v1/"],
    "Params": {
      "Greeting": "Hello from the host"
    }
  }
}

Live localhost (dev)

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerAppDev",
    "SourceKind": "Url",
    "Url": "https://localhost:5174/",
    "AllowedApiPrefixes": ["api/v1/"],
    "Params": {
      "Mode": "dev"
    }
  }
}

Mass Storage / S3

{
  "ControlType": "SandboxedApp",
  "ControlData": {
    "Name": "PartnerApp",
    "SourceKind": "MassStorage",
    "OwnerId": "{# Root.CustomerID #}",
    "ContentPath": "PartnerContent/my-app/index.html",
    "AllowedApiPrefixes": ["api/v1/", "Document/"],
    "Params": {
      "RecordId": "{{ParamByName(\"Id\")}}"
    }
  }
}

Roadmap

  • Expand host capabilities (navigate, additional ui / events helpers) with a protocol version bump when required.
  • Optional row-level diff mode for large list feeds (snapshot mode is the v2 default).
  • Optional VuePortal /partner-content/... route for streaming FileBin without blob rewriting.
  • Designer schema: add ControlData.Params (object, additionalProperties allowing string or nested object/array JSON) and ControlData.Route (string) on DynamicControl_SandboxedApp (runtime already accepts them via additionalProperties: true).

Published for partners as public npm packages (@callcorpacd/platform-bridge, @callcorpacd/sandbox-cli, create-callcorp-sandbox). See ../create-callcorp-sandbox/PARTNER.md.