@_solaris/messenger-widget

Embeddable chat messenger widget — Vue 3 library + iframe-hosted UI + snippet drop-in. Wired to messenger-server (cookie-based session).
35,544
Install
npm install @_solaris/messenger-widget
Latest Version:0.6.49
Vue:3
License:Custom
Last Updated:Jul 12, 2026
Links: npm
Maintainer: _solaris

messenger-widget

Embeddable chat messenger widget. Same UI as the former ww-messenger WeWeb plugin, repackaged as a framework-agnostic npm package.

Published as @_solaris/messenger-widget.

Two ways to use it

1. Vue 3 library

npm i @_solaris/messenger-widget vue
import { Messenger } from '@_solaris/messenger-widget';
import '@_solaris/messenger-widget/style.css';

// Linked mode (merchant identifies the user):
//   <Messenger :base-url="..." :widget-id="..." :token="merchantJwt" display-mode="floating" />
//
// Visitor mode (anonymous): omit `:token` — the widget calls POST /client/session
// on boot and persists a brispr-issued JWT in localStorage. Requires the widget's
// `security.allow_unauthenticated=true` server-side.
//   <Messenger :base-url="..." :widget-id="..." display-mode="floating" />
//
// UI language: pass `language` ('fr' | 'en'). When omitted, it falls back to
// the authenticated customer's `language`, then the widget config's
// `default_language`, then French:
//   <Messenger ... language="en" />
//
// Optionally push declared variables. Nested by category :
//   :variables="{ customer: { name: 'Jane', email: 'jane@acme.io', plan: 'pro' }, loyalty: { tier: 'gold' } }"
// Routed server-side by the declared `scope` of each variable (customer /
// conversation → persisted ; context-scoped vars should be set via
// `Messenger.context({...})` at runtime instead).

Auth model

The widget sends Authorization: Bearer <jwt> on every call. The JWT comes from one of two sources, depending on whether token is passed:

  • linked (merchant backend signs): sign a JWT HS256 with the widget secret, claims { sub: <external_id>, wid: <widget_id>, kind: "linked", exp }. Pass the resulting JWT as token. Typical TTL 1h. For long-lived SPAs that outlive the token, push a fresh one without tear-down via the rotation hook below — same pattern as Intercom's Intercom('update', { ... }).
  • visitor (omit token): the widget calls POST /client/session once, the server creates a customers row (identity_type='visitor') and returns a 30-day JWT. The widget persists it in localStorage[brispr_token_<wid>] for cross-reload continuity. Server-side opt-in via widgets.security.allow_unauthenticated.

It also exports the individual presentational components, the design tokens and helpers — used by the WeWeb "conversation admin" plugin which supplies its own WeWeb-managed data instead of the built-in JWT/SSE transport:

import {
    MessageList,
    Bubble,
    Composer,
    InlineCallbacks,
    FormCard,
    ArtifactRenderer,
    AIAvatar,
    HumanAvatar,
    TeamAvatars,
    AttachmentPreview,
    tokens,
    renderMarkdown,
    uuid,
    createStore,
    createTransport,
} from '@_solaris/messenger-widget';

2. Standalone embed (any site, no build step)

<script src="https://your-cdn/messenger.embed.js"></script>
<script>
    Messenger.init({
        baseUrl: 'https://messenger.your-saas.com',
        widgetId: '…',
        token: '…', // optional — JWT HS256 signed by your backend with
        // the widget secret. Omit to run in visitor mode
        // (anonymous, brispr-issued token persisted in
        // localStorage, server-side opt-in required).
        displayMode: 'floating', // 'floating' | 'sheet' | 'embedded'
        language: 'en', // optional — 'fr' | 'en'; else customer.language / widget.default_language
        // Variables déclarées à pousser au boot, nested par catégorie.
        // Routées serveur par scope déclaré (customer / conversation →
        // persisté ; les scope=context devraient être posés via
        // `Messenger.context()` plutôt que via `init({ variables })`).
        // `customer.{name,email,language}` montent aux colonnes canoniques.
        variables: {
            customer: {
                name: 'Jane Doe',
                email: 'jane@acme.io',
                plan: 'pro',
                seats: 12,
            },
        },
    });
    // Messenger.destroy() to tear it down.
</script>

init() is idempotent (no-op if re-called with identical options) and requires only baseUrl + widgetId to attempt a boot — without token, the widget falls back to visitor mode automatically. See examples/weweb-embed.md for the WeWeb integration (reactive JWT via WeWeb variables + workflow).

Token rotation (linked mode)

When the merchant JWT is about to expire on a long-lived SPA, push a fresh one without tear-down. The widget swaps it in-place and rotates the SSE stream — the open conversation is preserved.

Embed (standalone) — call Messenger.update({ token }):

// Around 5min before expiration, fetch a new token from your backend, then:
Messenger.update({ token: newJwt });

Vue lib — just change the token prop. A watcher catches the change and calls the same rotation under the hood:

<Messenger :base-url="..." :widget-id="..." :token="currentJwt" />

When currentJwt updates (e.g. after your auth store fetches a new one), the widget rotates without re-mounting.

Messenger.update() (or the watcher) is a no-op if called before the widget boots — the new token will be used on the next boot.

Variables : identité vs contexte

Le messenger expose deux canaux distincts pour pousser des variables côté serveur, selon leur scope déclaré côté admin :

  • Messenger.update({ customer: {...}, loyalty: {...} }) — push autoritatif des variables scope ∈ {customer, conversation} (identité user, état conv durable). Persisté serveur dans customers.metadata / conversations.metadata (ou en attente dans sessions.metadata si l'entité n'existe pas encore — créée lazy au premier message). Replace par scope sur les buckets pending.

  • Messenger.context({ article: {...}, cart: {...} }) — set un buffer client-side pour les variables scope=context (état de la page visible). Aucun appel HTTP. Le buffer est inliné dans le body de chaque send_message et lu par l'agent au tour, sans persistance serveur.

Standalone embed :

<script>
  Messenger.init({
    baseUrl: '...', widgetId: '...', token: '...',
    // Push durable au boot (scope ∈ {customer, conversation}).
    variables: {
      customer: { name: 'Jane', plan: 'pro' },
    },
  });
  // Le buffer scope=context se pose après l'init :
  Messenger.context({ article: { title: 'Hello', url: '...' } });

  // Plus tard, depuis le code marchand :
  Messenger.update({ customer: { plan: 'enterprise' } }); // push serveur
  Messenger.context({ article: { title: 'New page' } });   // buffer client
</script>

Une catégorie poussée sur le mauvais canal (ex. Messenger.update({ article: {...} }) alors qu'article est déclarée scope=context) est warn-and-drop côté serveur — la déclaration reste la source de vérité, le canal doit matcher.

URLs starting with # are intercepted by the widget and navigate inside the messenger instead of opening a new tab. Useful for quick links in the widget config or for the agent to point the user at a specific conversation from inside a message.

URL Behavior
#conversation/<convId> Open that conversation in the panel
#new (or #new-conversation) Start a new draft (composer focused)
#<anything else> Logs a warning, does nothing
https://..., mailto:..., etc External — opens in a new tab as usual

Works in :

  • Quick links (widgets.quick_links config) :
[
    { "label": "My latest ticket", "url": "#conversation/abc-123" },
    { "label": "New request", "url": "#new" },
    { "label": "Help center", "url": "https://help.example.com" }
]
  • Agent messages (markdown links) :
See your [previous conversation](#conversation/abc-123) for context.

The interception is done at the panel level — any <a href="#..."> rendered inside the messenger DOM is caught, preventDefault'd, and dispatched to the internal router. External URLs and other schemes (mailto:, tel:, etc.) pass through untouched.

Build

npm install
npm run build        # build:lib + build:embed + build:types → dist/
Output Target Vue
dist/messenger.js / .cjs + dist/style.css lib external (peer)
dist/messenger.embed.js embed bundled in
dist/types/ types from JSDoc

License

Proprietary and source-available. The Software may be embedded in your own application only to interoperate with the Licensor's backend service; no modification, no use with any other backend, no redistribution. Public availability on npm grants no rights beyond the license. See LICENSE.

Version History

Version Released Vue License
0.6.49 ^3.3.4 Custom
0.6.48 ^3.3.4 Custom
0.6.47 ^3.3.4 Custom
0.6.46 ^3.3.4 Custom
0.6.45 ^3.3.4 Custom