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

squarefi-livechat-widget

v1.0.7

Published

Host-side widget for the Squarefi CRM support chat: launcher, sandboxed frame, Clerk template token hand-off.

Readme

squarefi-livechat-widget

Host-side widget for the Squarefi CRM support chat. It adds a launcher button and a sandboxed iframe from the CRM to your app, hands the frame a short-lived JWT, and gets out of the way when the chat is not available, so your app can show its own support entry point.

The CRM serves the chat itself (/livechat/frame, /api/livechat/*). This package replaces the CRM's /livechat/v1/loader.js script tag: no CRM-hosted script runs in your origin, and the code that does run is pinned by the version in your package.json.

Install

npm install --save-exact squarefi-livechat-widget

Pin the exact version. An update is a reviewed PR in your app, not a deploy on someone else's side.

React

'use client';

import { useAuth } from '@clerk/nextjs';
import { toCrmOrigin } from 'squarefi-livechat-widget';
import { useLivechat } from 'squarefi-livechat-widget/react';

const crmOrigin = toCrmOrigin(process.env.LIVECHAT_CRM_ORIGIN);

export const SupportChat = ({ origin }: { origin: string }) => {
  const { getToken, isSignedIn } = useAuth();

  const { fallbackReason } = useLivechat({
    enabled: !!isSignedIn,
    crmOrigin: origin,
    brand: 'mosta',
    locale: 'en',
    getToken: async () => {
      const token = await getToken({ template: 'mosta-crm-livechat', skipCache: true });
      if (!token) throw new Error('signed out');
      return token;
    },
    appearance: { zIndex: 40, launcherBackground: 'var(--accent)', launcherColor: 'var(--accent-foreground)' },
  });

  return fallbackReason ? <YourOldSupportWidget /> : null;
};

useLivechat boots once while enabled is true and shuts the chat down when it turns false or the component unmounts. getToken, onFallback and appearance are read through refs, so new function identities on every render do not reboot the chat; a reboot would close a chat the user has open. A change of crmOrigin, brand or locale reboots.

theme (light | dark, e.g. next-themes resolvedTheme) is passed to the frame; when it changes the hook sends it to the running chat without a reboot. Without React, call setLivechatTheme(theme). Anything but dark is light.

Without React

import { bootLivechat, isLivechatActive, showLivechat, shutdownLivechat } from 'squarefi-livechat-widget';

bootLivechat({ crmOrigin, brand: 'mosta', locale: 'en', getToken, onFallback: (reason) => showOldSupport(reason) });

// "Contact us" button:
if (isLivechatActive()) showLivechat();

// Sign-out:
shutdownLivechat();

API: bootLivechat(options) (returns a dispose function for that session), setLivechatTheme(theme), showLivechat(), hideLivechat(), shutdownLivechat(), isLivechatActive(), toCrmOrigin(value) (an https origin, plain http only for localhost, or null), LivechatFallbackReason. crmOrigin goes through toCrmOrigin inside bootLivechat; a value it rejects falls back with unavailable before any request.

The token

getToken must return the JWT template token, never the plain session token:

  • getToken({ template: '<livechat template>', skipCache: true }) from the browser hook (useAuth());
  • the template carries aud, brand, email, name and no tenant_id, so your own APIs refuse it if it leaks;
  • a server-side auth().getToken() has no azp, and the CRM answers 401 bad_azp;
  • without skipCache: true the auth provider returns its cached token and the refresh loops.

The widget keeps the token in memory, sends it only to the frame (postMessage with the CRM origin as target) and the config probe, and never logs it. It refreshes the token 60 s before exp, at most once per 5 s when the frame keeps asking after 401s, and retries every 30 s when the auth provider fails. More than six auth_required in a minute means the frame is looping: the widget falls back with unavailable.

Fallback

The launcher appears only after GET <crm>/api/livechat/config answers 200 with rollout.enabled === true. Otherwise onFallback(reason) is called once and nothing is left on the page:

| reason | when | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | token | getToken rejected, returned nothing, or did not answer within 10 s | | timeout | the config probe did not answer within 3 s | | unavailable | network error, non-200 other than 404/503, three 401s on fresh tokens, more than six auth_required a minute, no frame ready in 15 s, a rejected crmOrigin | | disabled | 404/503 from the config probe, or rollout.enabled !== true |

Isolation

  • The frame is sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads": without allow-top-navigation it cannot navigate your tab, and without allow-popups-to-escape-sandbox a popup it opens inherits the sandbox and cannot navigate your tab either. It is cross-origin, so it cannot read your DOM, cookies or storage. Attachments open in a sandboxed tab: images show, files download; a browser may refuse to render a PDF there.
  • Only the user opens the panel (launcher, showLivechat()): an open from the frame is ignored.
  • allow="clipboard-write" only; pasted images reach the frame through the paste event.
  • Messages from the frame are accepted only from the CRM origin and the frame's own window, and only in the protocol v1 shape.
  • What the CRM receives: the template token, page_url (origin and path, no query or hash), locale, brand.

Your app's side

  • The CRM must list your exact origin in LIVECHAT_ALLOWED_ORIGINS (CORS, frame-ancestors and the token's azp).
  • With a Content-Security-Policy, add the CRM origin to frame-src and connect-src. script-src is not needed.
  • appearance sets zIndex (default 2147483000), the launcher, badge and frame colors, frameBorderColor (a 0.5px ring around the panel, drawn as a shadow so it takes no space; none by default) and frameRadius (desktop panel corners, 0–40 px, default 18). A color is any CSS value built from letters, digits, #%.,-+/, spaces and balanced parentheses (hex, names, rgb(), hsl(), oklch(), color-mix(), var(--token, fallback)); anything else, such as quotes, :, *, ; or braces, falls back to the default.
  • The launcher itself: launcherSize (40–72 px, default 56; the panel opens 12 px above it), launcherShadow (same characters as a color, up to 240, none allowed), launcherBackdropBlur (0–40 px, for a translucent glass background), launcherIcon and launcherIconSize (12–32 px, default 26). launcherIcon replaces the chat bubble with a monochrome image used as a mask, so it takes launcherColor: a same-origin path (/brand/symbol.svg) or an https URL, letters, digits and /_.- only. Anything else keeps the bubble. A cross-origin icon needs CORS, as any CSS mask. Theme-dependent values belong in CSS variables, e.g. { launcherBackground: 'var(--launcher-bg)', launcherShadow: 'var(--launcher-shadow)', launcherBackdropBlur: 20, launcherSize: 60, launcherIcon: '/brand/symbol.svg', launcherIconSize: 22 }.
  • A CRM e-mail link to your page ending in #support-chat opens the chat: the widget opens the panel once the frame is ready (also when the fragment changes on an open page) and removes the fragment, keeping the path and the query. Nothing to do on your side; if the chat falls back, the fragment stays for your old widget.

Protocol

The widget speaks the CRM's postMessage protocol v1 (envelope { v: 1, ns: 'mosta-chat', type, ... }):

  • frame → host: ready, unread, close, resize, auth_required, unavailable (open is ignored);
  • host → frame: token, open, close, locale, theme (light | dark, after ready and on every change).

The frame URL carries brand, host, locale and theme (first paint).

A new protocol version in the CRM needs a release of this package; until then the widget keeps speaking v1.

Development

npm ci
npm run check-types && npm run format && npm test
npx playwright install chromium && npm run test:e2e   # browser tests against a fake CRM on another origin
npm run build

Every merge into main releases a new patch version: the release bot bumps package.json, moves the CHANGELOG.md [Unreleased] notes under the new version, tags v<version> and publishes to npm. Write your change notes under [Unreleased] in the PR; for a minor or major release, bump the version in the PR yourself (the bot adds a patch on top). The bot's key is a secret of the npm GitHub environment (only main and v* tags may deploy to it) and the npm trusted publisher is bound to that environment, so write access to the repository does not allow publishing.