@belonguniverseai/react-sdk
v0.6.28
Published
Belong — embeddable React AI assistant dock. Mount <BelongWidget> in your app tree with getToken dependency-injection auth.
Maintainers
Readme
@belonguniverseai/react-sdk
Embeddable Belong AI assistant dock as a React component. Mount <BelongWidget>
inside your own React tree — no <script> tag, no globals. Authentication is
dependency-injected via a getToken callback (called per request; Belong never
caches your token).
Install
npm install @belonguniverseai/react-sdkreact and react-dom (>= 18.2) are peer dependencies — the component mounts on
your app's single React copy.
Usage
import { BelongWidget } from "@belonguniverseai/react-sdk";
export function App() {
return (
<BelongWidget
getToken={async () => {
// Return a fresh Belong-audience JWT. Called per request.
return await getBelongToken();
}}
backend="https://your-belong-backend.example.com"
tenant="your-tenant-id-or-slug"
/>
);
}Props
| Prop | Type | Required | Description |
| ---------------- | ------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| getToken | () => Promise<string> | yes | Resolves a bearer token per request (DI auth; never cached). |
| backend | string | yes | Belong backend base URL. |
| tenant | string | no | Public tenant ID or slug for branding. Required when the API hosts multiple tenants; omit on a single-tenant API. Auth still comes from the JWT. |
| universeOrigin | string | no | Deprecated, ignored. Cross-origin navigation keys off window.location; safe to remove. |
| agentId | string | no | Sent as X-Belong-Agent-Id on every request. |
| locale | BelongLocale | no | Host/tenant default language (user choice still wins). |
| panelEnabled | boolean | no | Whether the active workspace shows the assistant panel. |
| agentGrant | { provider?: string; scopeExternalId?: string; scopeUrlParam?: string } | no | Optional CLI agent-grant overrides. Most hosts need none of this — a scoped workspace already offers the default grant. |
| workspace | string \| null | no | Active workspace id — re-scopes sessions, files, chat, and the CLI grant on switch. null disables scoped surfaces while resolving; omit if unused. |
The dock renders into its own shadow root, so its styles never leak into (or inherit from) your app.
Tenant default theme
Both the React component and window.belong.init({ apiBaseUrl, tenant }) load
GET /v1/config/theme?tenant=<id-or-slug> before rendering the dock. The API
resolves the active tenant and reads tenants/<tenantId>/config/theme.json from
its private asset store, with the existing knowledge-domain theme as a fallback.
The browser needs no bucket credentials or direct bucket access.
The published tenant theme is the default. Appearance settings offer Company
default, Belong, Simetrik, and Custom. Explicit style choices are
stored per API endpoint and tenant. Selecting Company default restores the latest
published branding; old unscoped style preferences cannot silently replace it.
The SDK refreshes the baseline on page focus or when the tab becomes visible,
while preserving an explicit style and personal color-mode/position preferences.
The script embed can also restore it with belong.applyPreset("tenant").
Theme reads bypass HTTP caches. API instances refresh their tenant registry and
asset cache using the existing assets_revision signal, so publishing must
advance that revision. A first read also syncs revision-0 tenants added after boot.
Network failures retain the last good theme or built-in/inline fallback; initial
theme reads time out after five seconds so an unavailable API cannot block the dock.
Rollout requires releasing the API and both SDK bundles, then upgrading the SDK installed in each host application. No admin-page change or public bucket access is required. Configure each deployment with the intended environment/region's tenant bucket and backend read permissions.
Host context (window.belong.init)
The script-tag embed's init takes two optional host-context props that
personalise what the dock shows before the first message:
| Prop | Type | Required | Description |
| ------------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------- |
| user | { displayName?: string } | no | Signed-in user; personalises the first message and the avatar menu. |
| pageContext | { label?: string; fromDocumentTitle?: boolean } | no | Page label for the first message; defaults to the tab title. |
Both have runtime setters on window.belong, alongside setLocale and
setWorkspace:
belong.setUser({ displayName })— wins overinit({ user }).setUser(null)drops the override back to the init value;setUser({ displayName: null })means "no name for this user" and shows none even wheninitseeded one.belong.setPageContext(label)— wins overinit({ pageContext })and the tab title;setPageContext(null)drops the override. With no label anywhere, the dock readsdocument.titlelive (SPA navigation updates it), unless the host passedpageContext: { fromDocumentTitle: false }.
A re-init states the host's context in full: omitting user or
pageContext CLEARS what a previous init seeded (the config is replaced
wholesale, not merged). A live setUser / setPageContext override outranks
init and survives a later one — clear it with null to hand control back to
the init values.
Neither value is persisted — a reload re-derives both from the host's next
init.
Opening the dock from the host (window.belong.open)
belong.open({ size?, draft? }) shows the chat view at the requested size:
"fullscreen" covers the viewport on desktop (a phone resolves it to the open
sheet, like the rail's own toggle), "expanded" docks it beside the page, and
omitting size keeps the current size (a collapsed dock expands). draft
prefills the composer — never sends it, only when the composer is empty, at
most 4,000 characters — and focuses it. The user can always leave fullscreen.
It is safe to call BEFORE init resolves: a request made before the dock
mounts is applied at mount, so open({ size: "fullscreen" }) followed by
init(...) mounts straight into fullscreen with no flash of the docked size.
Host-bridged MCP (init({ hostMcp }))
Some tools can only be reached from the signed-in user's own page — an MCP
server behind an identity-aware proxy, an intranet, a cookie session. A tenant
publishes such a server as an MCP plugin entry with transport: "host" and a
same-origin mcpUri path (see docs/guides/009-manual-agent-integration.md);
the agent's calls then travel to the dock, which POSTs each JSON-RPC
tools/call to that path on the page's own origin with credentials:
"same-origin" and relays the raw answer back.
The host must opt in, per path:
window.belong.init({
// …
hostMcp: { allowedPaths: ["/mcp"] }, // root-relative, exact match, at most 8
});A path not listed is refused in the page (mcp_not_allowed) whatever the
tenant publishes; omitting hostMcp disables the feature. Requests carry
x-belong-source: dock so the host can label its own audit logs; they follow no
redirects (an expired proxy session reads as "sign-in expired", not as a login
page), abort after 55 s, and relay at most 1,000,000 characters of body.
Restricting what the agent can do on the page (init({ browserControl }))
By default the dock runs every browser-control op the agent sends: reading the
page (snapshot, read), driving it (click, type, select, press,
navigate), running JavaScript in it (eval) and host MCP calls (mcp_call).
All of them act with the signed-in user's own session on your origin.
A host can limit that to the ops it wants:
window.belong.init({
// …
hostMcp: { allowedPaths: ["/mcp"] },
browserControl: { allowedOps: ["mcp_call"] }, // only the bridged MCP calls
});Every other op is refused in the page before it runs (op_not_allowed), and the
agent is told the page does not allow it. allowedOps: [] refuses everything.
Omitting browserControl keeps every op, as before. A value that is present but
invalid (an unknown op name, a misspelt key) refuses everything and logs a
console warning, because a host that passed one meant to restrict the page. Like
hostMcp, it is restated on every init.
If your page only bridges MCP, pass allowedOps: ["mcp_call"]. Otherwise
the agent can also script and click through the page with the viewer's session,
which reaches everything hostMcp.allowedPaths was meant to fence off. That
matters most when the agent reads text written by other people (customer
conversations, tickets, email), since such text can carry instructions aimed at
the agent.
Account authorization and Content Security Policy
Browser-bound account authorization opens an about:blank popup, clears its
opener, and submits a form POST to the configured API's browserBindingUrl.
The popup inherits the embedding document's CSP even after its opener is cleared.
If that document restricts form-action, allow both the API bootstrap origin and
the authorization provider's origin in every enforced policy. Chromium also checks
the bootstrap's 303 redirect against this policy; allowing only the API is not enough.
For example, a deployment using the public Microsoft login service could use:
Content-Security-Policy: form-action 'self' https://your-belong-backend.example.com https://login.microsoftonline.comMerge those hosts into your existing policy, using your actual API and configured
provider origins (including any required redirect destinations). A same-origin API
proxy is covered by 'self'; the external provider still needs permission. Existing
connect-src and API allowed-origin requirements continue to apply independently.
An enforced form-action violation closes the popup and shows the existing
authorization failure UI. Users can retry with the Connect/Reconnect action after
the host policy is fixed. The SDK does not bypass CSP or offer direct provider
navigation for bound responses. Proofs travel only in the POST body, never in URLs.
The popup uses referrer policy origin so bootstrap POSTs retain a trustworthy
Origin header; the backend redirect should send Referrer-Policy: no-referrer.
Run the SDK's real Chromium checks (including cross-origin CSP and manual fallback)
from this package with node --test tests/browser/authorization-popup.mjs.
Voice calls (new in 0.2.0)
The dock's rail now carries a phone button: a real two-way voice call with a GPT-realtime voice operator that delegates work to Belong agents. The operator runs a team of up to 3 Belong agents at once ("Main agent" + background agents 2–3), routes same-scope follow-ups onto the agent already doing that work, spawns a new agent for unrelated asks — and says which it chose. Files the agents publish open on screen automatically; approvals and scheduling work by voice.
Voice calls require the backend to be configured with an OPENAI_API_KEY
(model/voice are tuned via OPENAI_REALTIME_MODEL / OPENAI_REALTIME_VOICE);
on an unconfigured deployment the call button shows "Call mode is not enabled
on this deployment." when tapped. Embedding hosts need no CSP changes:
the WebRTC handshake (SDP offer/answer) relays through the Belong backend —
already in your connect-src if the dock works at all — and the call audio
itself is a direct browser↔OpenAI WebRTC media stream, which connect-src
does not govern (audio never relays through the Belong backend).
Interactive HTML artifacts
Registered HTML reports receive the versioned window.belongArtifact helper.
The parent SDK owns authentication and approval; the opaque iframe submits named
actions and observes durable operation results without a sandbox port or tokens.
See the authoring contract and demo.
