/** * @oxpulse/chat-widget — shared type definitions. * * All public-facing types are defined here and re-exported from index.ts. * Keep this file pure types — no runtime code. */ import type { OutboxOp } from '@oxpulse/chat-sdk'; /** Widget initialisation config. */ export interface WidgetConfig { /** OxPulse app ID (from the admin panel). */ appId: string; /** Signed JWT from your backend (POST /api/sdk/tokens). */ jwt: string; /** Room ID to open. */ roomId: string; /** Render mode. 'inline' = shadow DOM in-page; 'iframe' = sandboxed iframe. Default: 'inline'. */ mode?: 'inline' | 'iframe'; /** Colour scheme. Default: 'auto' (follows prefers-color-scheme). */ theme?: 'light' | 'dark' | 'auto'; /** BCP 47 locale override. Defaults to document / browser locale. */ lang?: string; /** Override the OxPulse API base URL. Default: 'https://oxpulse.chat'. */ baseUrl?: string; /** * Called when the JWT expires (HTTP 401 from API). * Return a fresh JWT to automatically reconnect. */ onTokenExpired?: () => Promise; /** Called on unrecoverable widget errors. */ onError?: (err: WidgetError) => void; /** * Failure-counter hook (issue #78): fires on EVERY write-op failure * (reaction add/remove, message send) — not just auth errors — so an * integrator can count silent write failures without parsing the * `oxpulse-chat:write-error` DOM event. Mirrors `onError`'s callback shape. */ onWriteError?: (detail: WriteFailureDetail) => void; /** * Allow JWTs without aud_origins claim (pre-W1.1 issuers). Default false (deny — recommended). * Set to true only when migrating from a legacy token-minting service. */ allowLegacyToken?: boolean; /** * UID of the currently authenticated user. * Used to determine which reaction chips are "own" (data-own=true) and * to align own messages right (self/other bubble alignment). * * Resolution precedence: explicit `selfUid` / `self-uid` attribute > * JWT `sub` claim (auto-derived via `selfUidFromJwt`) > anon-read mint * `userId` (anon mode only). In most cases this can be left unset — * the widget derives it from the JWT automatically. */ selfUid?: string; /** * Enable anonymous read-only mode. * When true and no `jwt` is provided, the widget mints a short-lived anon-read * token via POST /api/sdk/auth/anon-read-mint and mounts in read-only mode * (composer hidden). The token is re-minted automatically before expiry. */ allowAnonRead?: boolean; /** * @internal — test-only mint override. * When provided, `element.ts` calls this instead of the real mintAnonReadToken. * Allows unit tests to inject a fake mint call without a network. * Never set in production code. */ _mintAnonReadToken?: (opts: { baseUrl: string; appId: string; roomId: string; }) => Promise<{ token: string; userId: string; expiresAt: number; }>; /** * @internal — test-only factory override. * When provided, `element.ts` calls this instead of constructing a real SDKChatClient. * Allows unit tests to inject a mock client without a network. * Never set in production code. */ _createClient?: (opts: { baseUrl: string; jwt: string; appId: string; }) => { list(roomId: string, args: { limit: number; }): Promise<{ items: import('./ui/message-list.js').MessageRow[]; hasNext: boolean; }>; subscribe(roomId: string, args: { onMessage: (row: import('./ui/message-list.js').MessageRow) => void; onError?: (err: unknown) => void; onRosterSignal?: () => void; onMutation?: (event: { msgId: string; op: string; deletedAt?: string; editedAt?: string; [k: string]: unknown; }) => void; onReaction?: (event: { msgId: string; op: 'reaction_add' | 'reaction_remove'; reaction: string; userId: string; [k: string]: unknown; }) => void; }): () => void; sendText(roomId: string, args: { senderUid: string; text: string; msgId?: string; threadRootMsgId?: string; productRef?: string; productMeta?: ProductMeta; }): Promise<{ seq?: number; msgId: string; }>; getReactions?(roomId: string, msgId: string): Promise<{ counts: Record; users: Record; truncated: boolean; }>; sendReaction?(roomId: string, msgId: string, emoji: string): Promise; removeReaction?(roomId: string, msgId: string, emoji: string): Promise; }; /** * Enable named-write (authed compose) mode. * * When true the widget renders a compose UI (input + send button). A write * token is obtained from `writeMintEndpoint` (server-side mint on the * embedding client's backend). When false (default) the widget is read-only. * * The write token is separate from the read `jwt` — it is minted with * named-identity write capability via the Phase B grant flow. * * **Supported modes:** `mode:'inline'` (shadow DOM) only. Setting `allowWrite:true` * with `mode:'iframe'` logs a console warning and the compose UI is not shown * (iframe named-write support is planned for W5). */ allowWrite?: boolean; /** * URL of the embedding client's own named-write mint endpoint. * * Required when `allowWrite` is true. The widget POSTs `{ room_id }` to this * URL and expects `{ token }` in the JSON response (same contract as * `mintNamedWriteToken` in `@oxpulse/chat-sdk`). * * The backend should exchange the user's session for a named-write grant via * POST /api/sdk/auth/group-grant-mint and return the resulting SDK JWT. * * Example: `writeMintEndpoint: '/api/oxpulse-write-token'` */ writeMintEndpoint?: string; /** * @internal — test-only mint override for named-write. * When provided, `element.ts` calls this instead of `mintNamedWriteToken`. * Allows unit tests to inject a fake mint without a network. * Never set in production code. */ _mintNamedWriteToken?: (opts: { mintEndpoint: string; roomId: string; }) => Promise; /** * Label overrides for the roster role badge shown next to a privileged * member's name (e.g. `{ moderator: "Seller", owner: "Store owner" }`). * * Presentation only — a role with no override falls back to the built-in * i18n label ("mod" / "owner", localized per `lang`). Roles are sourced * from the server's roster response and are NOT client-side authorization: * do not use them to gate a privileged operation. */ roleLabels?: Record; /** * Enable/disable reaction UI. When false, the reaction add button and * reaction clusters are hidden and the widget does not subscribe to live * reaction events. Default: true. */ reactionsEnabled?: boolean; /** * Enable/disable pinned messages UI. When false, the pinned messages * banner is not mounted and pin/unpin buttons are hidden on bubbles. * Default: true. * * Note: only supported in `mode:'inline'`. In `mode:'iframe'` (experimental) * no MessageList is mounted, so this flag has no effect. */ pinnedMessagesEnabled?: boolean; /** * Opt-in: render the seller product-catalog picker button in the composer * toolbar. When true (and a composer is mounted — i.e. a write-capable * client exists), the widget constructs an `SDKCatalogClient` from the * SAME `jwt` + `baseUrl` it already uses for its main SDK client and passes * it to the `Composer` as `catalogClient`, so the product button renders * and opens the `ProductPicker`. * * Default: false (backward compatible — no catalog client, no button). * * The JWT must carry `catalog:read:*` scope (server-enforced); a token * without it will surface a load error inside the picker, not crash the * widget. */ sellerCatalog?: boolean; /** * @internal — test-only factory override for the catalog client. * When provided, `element.ts` calls this instead of constructing a real * `SDKCatalogClient`. Allows unit tests to inject a mock catalog client * without a network. Never set in production code. */ _createCatalogClient?: (opts: { jwt: string; baseUrl: string; }) => import('@oxpulse/chat-sdk').SDKCatalogClient; } /** Attribute names observed by . */ export declare const OBSERVED_ATTRIBUTES: readonly ["app-id", "jwt", "room-id", "mode", "theme", "lang", "self-uid", "base-url", "allow-anon-read", "allow-write", "write-mint-endpoint", "reactions-enabled", "pinned-messages-enabled", "seller-catalog"]; /** @internal Not part of the package's public API surface; not re-exported from index.ts. Kept exported for cross-file use within the package. */ export type ObservedAttribute = (typeof OBSERVED_ATTRIBUTES)[number]; /** Map of event types dispatched on and the window in iframe mode. */ export interface WidgetEventMap { /** Fired when the widget has connected and passed origin check. */ 'oxpulse-chat:ready': CustomEvent<{ roomId: string; }>; /** Fired on unrecoverable error (origin mismatch, bad JWT shape, etc). */ 'oxpulse-chat:error': CustomEvent; /** Fired when the server returns 401; handler should call element.refreshToken(). */ 'oxpulse-chat:token-expired': CustomEvent<{ roomId: string; }>; /** Fired after a named-write message is successfully sent. */ 'oxpulse-chat:message-sent': CustomEvent<{ roomId: string; msgId: string; }>; /** Fired when a named-write send attempt fails (non-recoverable, after error chip shown). */ 'oxpulse-chat:write-error': CustomEvent; /** * Review finding #4: fired when an attachment's authenticated hydration * reaches FINAL failure (after retries exhaust, or immediately for a * permanent HTTP 403/404/410). Dispatched from the host element, bubbling + * composed. Deduped to once per attachment per final failure (not per retry). */ 'oxpulse-chat:attachment-error': CustomEvent<{ msgId: string; attachmentId: string; reason: 'hydrate_failed'; }>; /** * Observability: fired when a row carrying an `unsealError` (chat-sdk's * classifyUnsealError reason 'replay' | 'auth' | 'unknown') is rendered — * a replay-attack signature and a benign timeout are otherwise * indistinguishable to the host. Dispatched from the host element, bubbling + * composed. Deduped to once per msgId per widget lifetime (not per re-render). * The replay reason is the one that matters most on an untrusted server. */ 'oxpulse-chat:decrypt-error': CustomEvent<{ roomId: string; msgId: string; seq: number; reason: 'replay' | 'auth' | 'unknown'; }>; /** * Observability: fired when the Reconnector exhausts all retry attempts * (MAX_ATTEMPTS=10) — a permanently-dead room is otherwise invisible to host * monitoring (contrast oxpulse-chat:token-expired which fires on auth * expiry). Dispatched from the host element, bubbling + composed. */ 'oxpulse-chat:reconnect-exhausted': CustomEvent<{ roomId: string; attempts: number; }>; } export type WidgetErrorCode = 'ORIGIN_NOT_ALLOWED' | 'JWT_MALFORMED' | 'JWT_EXPIRED' | 'TOKEN_REFRESH_FAILED' | 'NETWORK_ERROR' | 'WRITE_MINT_FAILED' | 'WRITE_SEND_FAILED' | 'WRITE_REACTION_FAILED' /** #261: IndexedDB is unavailable, so sends are not persisted and will not be * retried after a reload. Sending still works — this is a degradation, not a * failure. Reported once per widget instance; two widgets on a page each * report, because each has its own onError. */ | 'OUTBOX_UNAVAILABLE' | 'UNKNOWN'; /** * Structured detail carried alongside the message. Discriminated so a caller * reads a field rather than parsing the message string — #261 shipped with the * failing op only in prose, which is not an API. */ export type WidgetErrorDetail = { op: WriteFailureOp; reason: WriteFailureReason; } | { outboxOp: OutboxOp; }; export declare class WidgetError extends Error { readonly code: WidgetErrorCode; /** Present on write-failure events (issue #78) — which op failed. */ readonly op?: WriteFailureOp; /** Present on write-failure events (issue #78) — coarse failure reason. */ readonly reason?: WriteFailureReason; /** Present on OUTBOX_UNAVAILABLE (#261) — which storage op lost durability. */ readonly outboxOp?: OutboxOp; constructor(code: WidgetErrorCode, message: string, detail?: WidgetErrorDetail); } /** Which write operation failed. */ export type WriteFailureOp = 'reaction_add' | 'reaction_remove' | 'send'; /** Coarse failure-reason bucket for write-failure telemetry. */ export type WriteFailureReason = 'auth_expired' | 'network' | 'other'; /** Detail payload for the write-failure counter hook (config.onWriteError). */ export interface WriteFailureDetail { op: WriteFailureOp; reason: WriteFailureReason; } export declare class OriginNotAllowedError extends WidgetError { constructor(origin: string, allowed: string[]); } /** * Messages sent FROM the parent page INTO the iframe (parent → iframe). */ export type ParentMessage = { type: 'init'; config: WidgetConfig; } | { type: 'refresh-token'; jwt: string; } | { type: 'set-theme'; theme: 'light' | 'dark' | 'auto'; }; /** * Messages sent FROM the iframe OUT to the parent page (iframe → parent). */ export type IframeMessage = { type: 'ready'; roomId: string; } | { type: 'error'; code: WidgetErrorCode; message: string; } | { type: 'token-expired'; roomId: string; } | { type: 'resize'; height: number; } | { type: 'user-action'; event: 'send' | 'reaction' | 'typing'; }; export interface OriginCheckResult { allowed: boolean; /** Populated when allowed=true. */ matchedPattern?: string; /** Populated when allowed=false. */ reason?: string; } /** Options for the programmatic mount() API (superset of WidgetConfig). */ export interface MountOptions extends WidgetConfig { /** Shadow DOM mode. Default: 'open'. */ shadowMode?: 'open' | 'closed'; } /** W9: Marketplace product display metadata. Non-sensitive catalog info. */ export interface ProductMeta { title: string; /** * Raw numeric price amount (e.g. 19.99, 1200). The server requires a * non-negative JSON number — NOT a pre-formatted display string. * * #207: was `string` (host-pre-formatted display text rendered verbatim); * now `number` to match the finalized server contract. Locale-aware * formatting (Intl.NumberFormat) is applied at render time in a later * batch — until then the widget renders the raw number verbatim as * `${price} ${currency}`. */ price: number; currency: string; imageUrl: string; productUrl: string; } //# sourceMappingURL=types.d.ts.map