@_solaris/messenger-widget
v0.6.49
Published
Embeddable chat messenger widget — Vue 3 library + iframe-hosted UI + snippet drop-in. Wired to messenger-server (cookie-based session).
Readme
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 vueimport { 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: '[email protected]', 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 astoken. 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'sIntercom('update', { ... }). - visitor (omit
token): the widget callsPOST /client/sessiononce, the server creates acustomersrow (identity_type='visitor') and returns a 30-day JWT. The widget persists it inlocalStorage[brispr_token_<wid>]for cross-reload continuity. Server-side opt-in viawidgets.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: '[email protected]',
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 danscustomers.metadata/conversations.metadata(ou en attente danssessions.metadatasi 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 chaquesend_messageet 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.
Deep links
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_linksconfig) :
[
{ "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.
