@paramms/chat-widget 1.0.30 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js ADDED
@@ -0,0 +1,146 @@
1
+ var c = Object.defineProperty;
2
+ var d = (t, e, s) => e in t ? c(t, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : t[e] = s;
3
+ var n = (t, e, s) => d(t, typeof e != "symbol" ? e + "" : e, s);
4
+ import { a as o, C as u, b as h } from "./e2e.js";
5
+ import { A as C, E as k, P as A, c as E, d as j, e as U, f as R, g as w, h as x, i as F, j as L, k as N, l as O } from "./e2e.js";
6
+ import { p as l, r as I } from "./uid.js";
7
+ import { h as H, a as P } from "./uid.js";
8
+ import { mountChatList as Y } from "./chatlist.js";
9
+ const m = /* @__PURE__ */ new Set([
10
+ "resolved",
11
+ "closed",
12
+ "sold",
13
+ "issued",
14
+ "checked_out"
15
+ ]);
16
+ function p(t) {
17
+ return m.has(t);
18
+ }
19
+ function T(t) {
20
+ const e = t.effect.type === "form" ? t.effect.fields : void 0;
21
+ return {
22
+ id: t.id,
23
+ label: t.label,
24
+ audience: t.audience,
25
+ surface: t.surface,
26
+ ...t.icon ? { icon: t.icon } : {},
27
+ ...t.confirm ? { confirm: t.confirm } : {},
28
+ ...t.availableInStates ? { availableInStates: t.availableInStates } : {},
29
+ ...e ? { input: e } : {}
30
+ };
31
+ }
32
+ const v = {
33
+ MAX_TEXT_LEN: 8e3,
34
+ MAX_HISTORY_LIMIT: 100,
35
+ DEFAULT_HISTORY: 50
36
+ };
37
+ class f {
38
+ constructor(e) {
39
+ n(this, "store");
40
+ n(this, "conn");
41
+ n(this, "listeners", /* @__PURE__ */ new Set());
42
+ n(this, "msgSeq", 0);
43
+ n(this, "_status", "connecting");
44
+ n(this, "_statusMessage");
45
+ n(this, "_me");
46
+ this.store = new u(e.me);
47
+ const s = {
48
+ type: "open",
49
+ profileId: e.profileId,
50
+ ...e.subjectId ? { subjectId: e.subjectId } : {},
51
+ ...e.subjectTitle ? { subjectTitle: e.subjectTitle } : {},
52
+ ...e.kind === "direct" ? { kind: "direct", peerId: o(e.peerId ?? "") } : {},
53
+ ...e.user ? { userInfo: e.user } : {}
54
+ }, { wsUrl: i } = I(e.url, e.apiUrl);
55
+ this.conn = new h({
56
+ url: i,
57
+ token: e.token ?? e.me,
58
+ open: s,
59
+ getCursor: () => this.store.highestSeq(),
60
+ onFrame: (r) => {
61
+ r.type === "authed" && (this._me = r.userId), this.store.apply(r), this.emit();
62
+ },
63
+ onStatusChange: (r, a) => {
64
+ this._status = r, this._statusMessage = a, this.emit();
65
+ }
66
+ }), this.conn.connect();
67
+ }
68
+ /** Subscribe to any change (message, typing, status). Returns unsubscribe. */
69
+ onChange(e) {
70
+ return this.listeners.add(e), () => this.listeners.delete(e);
71
+ }
72
+ emit() {
73
+ for (const e of this.listeners) e();
74
+ }
75
+ /** Our canonical user id as resolved by the server (JWT sub / userId / anon id). */
76
+ get me() {
77
+ return this._me;
78
+ }
79
+ get conversationId() {
80
+ return this.store.conversationId;
81
+ }
82
+ get status() {
83
+ return this._status;
84
+ }
85
+ get statusMessage() {
86
+ return this._statusMessage;
87
+ }
88
+ send(e) {
89
+ const s = `c_${Date.now().toString(36)}_${++this.msgSeq}`, i = this.store.conversationId;
90
+ i && (this.store.addOptimistic(s, { kind: "text", text: e }), this.conn.send({ type: "send", conversationId: i, clientMsgId: s, content: { kind: "text", text: e } }), this.emit());
91
+ }
92
+ typing(e, s) {
93
+ const i = this.store.conversationId;
94
+ i && this.conn.send({ type: "typing", conversationId: i, isTyping: e, ...s ? { preview: s } : {} });
95
+ }
96
+ markRead() {
97
+ const e = this.store.conversationId;
98
+ e && this.conn.send({ type: "read", conversationId: e, seq: this.store.highestSeq() });
99
+ }
100
+ close() {
101
+ this.conn.close(), this.listeners.clear();
102
+ }
103
+ }
104
+ class b {
105
+ constructor(e) {
106
+ this.opts = e;
107
+ }
108
+ /** The identity this client will act as: the token's subject (resolved
109
+ * server-side), the raw userId, or a persistent anonymous browser id. */
110
+ me() {
111
+ return o(this.opts.token ?? l());
112
+ }
113
+ open(e = {}) {
114
+ return new f({ ...this.opts, ...e, me: this.me() });
115
+ }
116
+ }
117
+ export {
118
+ C as ANONYMOUS_TENANT,
119
+ u as ChatStore,
120
+ h as ConnectionManager,
121
+ k as E2ESession,
122
+ v as LIMITS,
123
+ A as PersistentOutbox,
124
+ b as RelayClient,
125
+ f as RelayConversation,
126
+ m as TERMINAL_STATES,
127
+ E as asActionId,
128
+ j as asConnectionId,
129
+ U as asConversationId,
130
+ R as asMessageId,
131
+ w as asProfileId,
132
+ x as asSubjectId,
133
+ F as asTenantId,
134
+ o as asUserId,
135
+ L as decodeFrame,
136
+ N as encodeFrame,
137
+ H as httpBaseFromWsUrl,
138
+ O as isClientFrame,
139
+ p as isTerminalState,
140
+ Y as mountChatList,
141
+ l as persistentUid,
142
+ I as resolveRelayUrls,
143
+ P as restoreHistory,
144
+ T as toManifestAction
145
+ };
146
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sources":["../src/protocol/entities.ts","../src/protocol/actions.ts","../src/protocol/frames.ts","../src/core.ts"],"sourcesContent":["import type {\n ConversationId, MessageId, ProfileId, SubjectId, TenantId, UserId,\n} from './ids.js'\nimport type { ActionId } from './ids.js'\n\n// ── Message content (discriminated union) ─────────────────────────────────────\n// The runtime stays generic by never hard-coding business content: a message is\n// one of a small, fixed set of shapes. `card`/`form`/`system` are how action\n// results and structured prompts render — they subsume most \"rich messaging\"\n// features without a per-feature content zoo.\n\nexport interface CardField { label: string; value: string }\n\n/** A reference to an action a card/quick-reply can invoke. */\nexport interface InlineActionRef { actionId: ActionId; label: string }\n\nexport type MessageContent =\n | { kind: 'text'; text: string; enc?: boolean; iv?: string }\n | { kind: 'attachment'; url: string; mime: string; name?: string; size?: number }\n | { kind: 'card'; title?: string; body?: string; fields?: CardField[]; actions?: InlineActionRef[] }\n | { kind: 'form'; prompt: string; actionId: ActionId }\n | { kind: 'system'; event: string; data?: Record<string, string | number | boolean> }\n | { kind: 'appointment'; title: string; startIso: string; endIso: string; location?: string; description?: string; googleUrl: string; icalUrl: string; confirmed?: boolean }\n\nexport type SenderRole = 'guest' | 'agent' | 'system' | 'bot'\n\n// ── Message ───────────────────────────────────────────────────────────────────\n// Ordering is by `seq` (server-assigned, monotonic per conversation), never by\n// `ts`. `ts` is wall-clock for display only. This kills the reorder/duplicate/\n// lost-on-reconnect class of bugs that millisecond-timestamp ordering caused.\n\nexport interface Message {\n id: MessageId\n conversationId: ConversationId\n seq: number\n senderId: UserId\n senderRole: SenderRole\n content: MessageContent\n ts: number\n replyToId?: MessageId\n editedAt?: number\n deletedAt?: number\n reactions?: Record<string, UserId[]>\n internal?: boolean // true = internal note, only visible to agents\n}\n\n// ── Conversation (the room; messages partition by conversationId) ─────────────\n// The room is the conversation, NOT the subject. Two guests discussing the same\n// subject get two conversations. `subjectId` is a nullable reference, never part\n// of the room identity — so \"one thread per (guest, subject)\" is enforced as\n// app logic at open-time, and many-threads-per-subject stays possible for free.\n\nexport interface Conversation {\n id: ConversationId\n tenantId: TenantId\n profileId: ProfileId // behavior profile → which actions this room has\n subjectId?: SubjectId // optional: the thing it's about\n guestId: UserId // the end-user\n /** Host-supplied display info for an identified guest (widget `user` option).\n * Display metadata only — identity is still the token/guestId. */\n guestName?: string\n guestEmail?: string\n guestAvatar?: string\n guestMeta?: Record<string, string>\n /** True once the guest's identity has been proven by a signed ES256 JWT\n * against the chatroom's guestPublicKey (secure identity mode). */\n guestVerified?: boolean\n participants: UserId[] // guest + any assigned agents (membership = authz)\n assignedAgentId?: UserId // routing/ownership\n aiActive?: boolean // staff assigned the AI to answer this room\n state: string // conversation state-machine state\n firstResponseAt?: number // first agent reply ts (SLA)\n csat?: number // satisfaction score 1–5 (set on resolution)\n lastSeq: number // highest seq assigned in this conversation\n tags?: string[] // macro/manual tags (e.g. \"refund\", \"vip\")\n /** Live sentiment of the guest's most recent message (best-effort, async). */\n sentiment?: 'positive' | 'neutral' | 'frustrated'\n /** -1 (very frustrated) .. +1 (very positive); paired with `sentiment`. */\n sentimentScore?: number\n /** Set once an SLA-breach escalation macro has fired, so it only fires once. */\n slaEscalatedAt?: number\n /** If set, the conversation is snoozed until this Unix ms timestamp.\n * Hidden from the inbox until the timestamp passes, then resurfaces. */\n snoozedUntil?: number\n /** Page URL where the widget was open when the conversation started. */\n pageUrl?: string\n /** Browser tab title at conversation start — gives agents context. */\n pageTitle?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Co-browsing / shared annotation ────────────────────────────────────────---\n// A lightweight shared whiteboard layered over a conversation: agent and guest\n// can draw freehand strokes that both sides see live. Strokes are relayed\n// (not stored as messages) and kept per-conversation so late joiners can catch\n// up via the `opened` frame's `annotations` field.\n\nexport interface AnnotationPoint { x: number; y: number }\nexport interface AnnotationStroke {\n id: string\n points: AnnotationPoint[]\n color: string\n width: number\n by: UserId\n}\n\n// ── Subject (the referenced entity — Intercom \"custom object\") ────────────────\n// Carries shared state (available → reserved → sold) and fields (price, vin…)\n// that actions read/write. Many conversations reference one subject. Never a room.\n\nexport interface Subject {\n id: SubjectId\n tenantId: TenantId\n title: string\n state: string\n fields: Record<string, string | number | boolean>\n /** URL of the page where the subject lives (e.g. the listing page URL).\n * Captured automatically by the widget and stored on first open. */\n url?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Conversation lifecycle ────────────────────────────────────────────────────\n/** States in which a conversation is no longer \"open\": it's done with, so inbox\n * / proactive sweeps skip it and load balancing frees the assigned agent. The\n * single source of truth for \"is this conversation finished?\". */\nexport const TERMINAL_STATES: ReadonlySet<string> = new Set([\n 'resolved', 'closed', 'sold', 'issued', 'checked_out',\n])\nexport function isTerminalState(state: string): boolean {\n return TERMINAL_STATES.has(state)\n}\n\nexport type Channel = 'widget' | 'email' | 'sms' | 'whatsapp' | 'instagram' | 'kakao' | 'messenger' | 'line'\n","import type { ActionId, ProfileId, TenantId } from './ids.js'\n\n// ── Actions: the product primitive ────────────────────────────────────────────\n// An action is data an admin authors in the dashboard; the runtime stays generic\n// and only knows how to execute a small, fixed set of EFFECTS. Adding \"make\n// offer\" or \"schedule meeting\" is a config row, not a code deploy.\n\nexport type ActionAudience = 'guest' | 'agent' | 'both'\nexport type ActionSurface = 'toolbar' | 'inline' | 'quick_reply'\n\nexport interface ActionInputField {\n name: string\n label: string\n type: 'text' | 'number' | 'date' | 'select'\n required?: boolean\n options?: string[] // for type: 'select'\n}\n\n// A terminal effect produces a result and ends the action. Actions are\n// single-shot: structured multi-step lives in the conversation state machine,\n// and conversational multi-step is the `bot` effect — not an action workflow.\nexport type TerminalEffect =\n | { type: 'webhook'; url: string } // signed POST to tenant system\n | { type: 'state_transition'; target: 'conversation' | 'subject'; toState: string }\n | { type: 'bot' } // route to the AI resolver\n | { type: 'builtin'; name: string } // e.g. 'handoff'\n\n// The ONLY composition allowed is \"collect a form, then run one terminal\n// effect\" — exactly one level deep. This covers input-gathering (e.g. an offer\n// amount) without becoming a workflow engine.\nexport type ActionEffect =\n | TerminalEffect\n | { type: 'form'; fields: ActionInputField[]; then: TerminalEffect }\n\nexport type ActionResult =\n | { kind: 'system_message'; template?: string } // post a system line into the chat\n | { kind: 'card' } // render the effect's response as a card\n | { kind: 'state_badge' } // reflect a state change\n | { kind: 'none' }\n\nexport interface ActionDef {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[] // conversation/subject states; omit = always available\n effect: ActionEffect\n result: ActionResult\n}\n\n// Client-safe projection of an action: enough for the widget to render it and\n// collect inputs, but NONE of the effect internals (webhook URLs, transition\n// targets) — those stay server-side and execute on `invoke`. The client filters\n// by `availableInStates` locally against the current conversation state, so a\n// state change needs no manifest round-trip; the server re-validates on invoke.\nexport interface ManifestAction {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[]\n input?: ActionInputField[] // present when the action collects input (form effect)\n}\n\n/** Project an internal action to its client-safe manifest form. */\nexport function toManifestAction(a: ActionDef): ManifestAction {\n const input = a.effect.type === 'form' ? a.effect.fields : undefined\n return {\n id: a.id, label: a.label, audience: a.audience, surface: a.surface,\n ...(a.icon ? { icon: a.icon } : {}),\n ...(a.confirm ? { confirm: a.confirm } : {}),\n ...(a.availableInStates ? { availableInStates: a.availableInStates } : {}),\n ...(input ? { input } : {}),\n }\n}\n\n// ── Behavior profile (what \"domain\" becomes) ──────────────────────────────────\n// A reusable, admin-composed bundle of actions + defaults + state machine. Not a\n// built-in taxonomy — the 7 old templates become starter presets of this shape.\n// `version` lets an in-flight invocation validate against a consistent snapshot.\n\n/** Operating hours slot: 0=Sun … 6=Sat, times in \"HH:MM\" 24h local. */\nexport interface OperatingHoursSlot { day: 0|1|2|3|4|5|6; open: string; close: string }\n\nexport interface BehaviorProfile {\n id: ProfileId\n tenantId: TenantId\n name: string\n actions: ActionDef[]\n defaults: {\n greeting?: string\n theme?: { accent: string }\n e2e?: boolean\n persona?: string\n /** Paid-tier flag: when true, hides the \"Powered by Relay\" footer in the widget. */\n whiteLabel?: boolean\n /** White-label: serve/embed the widget from this hostname (e.g.\n * \"chat.acmeco.com\"). Allowed automatically as a CORS origin for the\n * control-plane API so the widget works from the custom domain. */\n customDomain?: string\n }\n states: string[]\n initialState: string\n version: number\n welcomeMessage?: string // first message guests see when opening the widget\n operatingHours?: OperatingHoursSlot[] // empty/absent = always open\n offlineMessage?: string // shown outside operating hours instead of chat\n /** Base64-encoded ECDSA P-256 SPKI public key. When set, guest tokens must be\n * signed JWTs — unsigned opaque tokens are rejected. */\n guestPublicKey?: string\n createdAt: number\n updatedAt: number\n}\n","import type {\n ConnectionId, ConversationId, MessageId, ProfileId, SubjectId, UserId,\n} from './ids.js'\nimport type { Channel, Conversation, Message, MessageContent, Subject, AnnotationStroke } from './entities.js'\nimport type { ManifestAction } from './actions.js'\n\n// ── Wire protocol ─────────────────────────────────────────────────────────────\n// One shared contract, imported by server + widget + dashboard. A change here is\n// a compile error in every consumer — which is the whole reason this lives in a\n// shared package instead of being hand-copied three times.\n\nexport type ErrorCode =\n | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'BAD_REQUEST'\n | 'RATE_LIMITED' | 'PAYLOAD_TOO_LARGE' | 'CONFLICT' | 'INTERNAL'\n\nexport type ClientFrame =\n | { type: 'auth'; token: string }\n // Open an existing conversation, or find-or-create one. Find-or-create keys on\n // (guest, subject) when subjectId is given; otherwise a fresh conversation.\n | { type: 'open'; conversationId?: ConversationId; subjectId?: SubjectId; profileId?: ProfileId; pageUrl?: string; pageTitle?: string; subjectTitle?: string; subjectMeta?: string; linkFrom?: UserId;\n /** Host-supplied display info for the guest — persisted onto the\n * conversation server-side so agents see who they're talking to.\n * Display metadata only, never used for authorization. */\n userInfo?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> } }\n | { type: 'send'; conversationId: ConversationId; clientMsgId: string; content: MessageContent; replyToId?: MessageId; via?: Channel[] }\n | { type: 'sync'; conversationId: ConversationId; sinceSeq: number } // catch-up after cursor\n | { type: 'history'; conversationId: ConversationId; beforeSeq: number; limit?: number } // load older\n | { type: 'read'; conversationId: ConversationId; seq: number } // read up to seq\n | { type: 'typing'; conversationId: ConversationId; isTyping: boolean; preview?: string }\n | { type: 'react'; conversationId: ConversationId; messageId: MessageId; emoji: string; remove?: boolean }\n | { type: 'edit'; conversationId: ConversationId; messageId: MessageId; content: MessageContent }\n | { type: 'delete'; conversationId: ConversationId; messageId: MessageId }\n | { type: 'invoke'; conversationId: ConversationId; actionId: string; clientInvokeId: string; inputs?: Record<string, unknown> }\n | { type: 'assign'; conversationId: ConversationId; agentId: UserId | null } // null = unassign\n | { type: 'tag'; conversationId: ConversationId; tag: string; remove?: boolean }\n | { type: 'note'; conversationId: ConversationId; clientMsgId: string; text: string } // internal note\n | { type: 'agent_status'; status: 'online' | 'away' | 'offline' } // agent sets their availability\n // Co-browsing: a freehand stroke (or \"clear\") on the shared annotation canvas\n // for a subject-anchored conversation. Relayed live to the other participant.\n | { type: 'annotate'; conversationId: ConversationId; stroke: Omit<AnnotationStroke, 'by'> }\n | { type: 'annotate_clear'; conversationId: ConversationId }\n | { type: 'pubkey'; conversationId: ConversationId; key: string }\n // X3DH async E2E: a client uploads a batch of one-time prekeys so peers can\n // encrypt to them while they are offline. The server stores them opaquely and\n // vends one on demand — it never derives or uses the keys.\n | { type: 'uploadPrekeys'; identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekeys: string[] }\n | { type: 'fetchPrekey'; targetUserId: UserId }\n // Inbox stream subscription — used by the agent dashboard, which reuses this\n // ConnectionManager. Typed here so the dashboard doesn't need `as never`.\n | { type: 'subscribe_inbox' }\n | { type: 'unsubscribe_inbox' }\n | { type: 'ping' }\n\nexport type ServerFrame =\n | { type: 'authed'; userId: UserId; connectionId: ConnectionId }\n | { type: 'opened'; conversation: Conversation; subject?: Subject; annotations?: AnnotationStroke[] }\n | { type: 'manifest'; conversationId: ConversationId; actions: ManifestAction[]; version: number; name?: string; theme?: { accent: string }; e2e?: boolean; offline?: boolean; offlineMessage?: string; whiteLabel?: boolean }\n | { type: 'message'; message: Message }\n | { type: 'ack'; clientMsgId: string; messageId: MessageId; seq: number; ts: number }\n | { type: 'delivered'; conversationId: ConversationId; seq: number; to: UserId }\n | { type: 'read'; conversationId: ConversationId; seq: number; by: UserId }\n | { type: 'sync'; conversationId: ConversationId; messages: Message[] }\n | { type: 'history'; conversationId: ConversationId; messages: Message[]; hasMore: boolean }\n | { type: 'typing'; conversationId: ConversationId; userId: UserId; isTyping: boolean; preview?: string }\n | { type: 'reaction'; conversationId: ConversationId; messageId: MessageId; emoji: string; by: UserId; removed: boolean }\n | { type: 'edited'; conversationId: ConversationId; messageId: MessageId; content: MessageContent; editedAt: number }\n | { type: 'deleted'; conversationId: ConversationId; messageId: MessageId; ts: number }\n | { type: 'state'; conversationId: ConversationId; state: string }\n | { type: 'assigned'; conversationId: ConversationId; agentId: UserId | null }\n | { type: 'tagged'; conversationId: ConversationId; tag: string; removed: boolean }\n | { type: 'visitor_count'; count: number } // broadcast to agents: guests currently connected\n | { type: 'agent_status_changed'; agentId: UserId; status: 'online' | 'away' | 'offline' }\n // Live sentiment of a guest's most recent message — relayed to agents only so\n // the inbox can flag frustrated conversations as they happen.\n | { type: 'sentiment'; conversationId: ConversationId; label: 'positive' | 'neutral' | 'frustrated'; score: number }\n // Co-browsing: relay of an annotation stroke / clear to everyone in the room.\n | { type: 'annotation'; conversationId: ConversationId; stroke: AnnotationStroke }\n | { type: 'annotation_clear'; conversationId: ConversationId; by: UserId }\n | { type: 'subjectState'; subjectId: SubjectId; state: string }\n | { type: 'presence'; conversationId: ConversationId; userId: UserId; status: 'online' | 'offline'; lastSeen?: number }\n | { type: 'invoked'; clientInvokeId: string; ok: boolean; error?: string }\n | { type: 'error'; code: ErrorCode; message: string }\n | { type: 'peerkey'; conversationId: ConversationId; userId: UserId; key: string }\n // X3DH bundle vended to a requesting client so they can encrypt to an offline peer.\n // Contains null when the target user has no registered prekeys.\n | { type: 'prekeyBundle'; targetUserId: UserId; bundle: { identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekey?: string } | null }\n | { type: 'pong' }\n\n/** Limits referenced by both ends so validation stays consistent. */\nexport const LIMITS = {\n MAX_TEXT_LEN: 8_000,\n MAX_HISTORY_LIMIT: 100,\n DEFAULT_HISTORY: 50,\n} as const\n","// ── @paramms/chat-widget/core — the headless SDK ─────────────────────────────\n// Everything you need to build your OWN chat UI (an in-app messenger, a\n// marketplace inbox, a full chat app) on the Relay protocol, with zero DOM or\n// React dependencies. This is not a new client: it is the exact transport,\n// store, outbox, and E2E machinery the bundled widget AND the agent dashboard\n// run on — re-exported behind a stable boundary, plus a small convenience\n// client for the common case.\n//\n// import { RelayClient } from '@paramms/chat-widget/core'\n//\n// // ONE url, any scheme — wss/ws/http(s) all work; ws + REST derived from it.\n// const relay = new RelayClient({ url: 'https://api.relay.paramms.com', token, profileId: 'p_x' })\n// const convo = relay.open({ subjectId: 'listing_42' }) // support thread\n// const dm = relay.open({ kind: 'direct', peerId: 'user_bob' }) // user↔user (signed identity required)\n// convo.onChange(() => render(convo.store.messages()))\n// convo.send('hello!')\n//\n// For React, see '@paramms/chat-widget/hooks'.\nexport { ConnectionManager, type SocketLike } from './connection.js'\nexport { ChatStore } from './store.js'\nexport { PersistentOutbox } from './outbox.js'\nexport { E2ESession } from './e2e.js'\nexport { restoreHistory, resolveRelayUrls, httpBaseFromWsUrl } from './history.js'\nexport { mountChatList, type ChatListEntry, type ChatListHandle, type ChatListOptions } from './chatlist.js'\nexport { persistentUid } from './uid.js'\nexport * from './protocol/index.js'\n\nimport { ConnectionManager } from './connection.js'\nimport { ChatStore } from './store.js'\nimport type { ClientFrame, ServerFrame, ConversationId, UserId } from './protocol/index.js'\nimport { asUserId } from './protocol/index.js'\nimport { persistentUid } from './uid.js'\nimport { resolveRelayUrls } from './history.js'\n\nexport interface RelayClientOptions {\n /** Relay URL — ONE url, any scheme. `https://api.relay.paramms.com` is the\n * recommended form; the WebSocket URL (`wss://…/ws`) and REST base are\n * derived from it automatically. `wss://`/`ws://`/`http://` also accepted. */\n url: string\n /** HTTP(S) base for REST calls — only when the REST API lives on a\n * DIFFERENT origin than the socket. Normally omit.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Identity: a signed JWT (secure), a stable userId (host-vouched), or omit\n * for an anonymous per-browser guest (browser environments only). */\n token?: string\n /** Chatroom id (from the dashboard). Required to open conversations. */\n profileId: string\n}\n\nexport interface OpenOptions {\n /** Support thread scoped to a subject (listing/order/…): one thread per\n * (user, subject). Omit for the profile's single support thread. */\n subjectId?: string\n subjectTitle?: string\n /** User↔user conversation (requires the chatroom to have signed identity\n * and `token` to be a valid signed JWT). */\n kind?: 'direct'\n peerId?: string\n /** Display info persisted for agents (support threads only). */\n user?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> }\n}\n\n/** One conversation = one connection + one store. Deliberately thin: the\n * store is the source of truth, `onChange` is the render signal, everything\n * else is the same primitives the first-party UIs use. */\nexport class RelayConversation {\n readonly store: ChatStore\n private readonly conn: ConnectionManager\n private readonly listeners = new Set<() => void>()\n private msgSeq = 0\n private _status = 'connecting'\n private _statusMessage: string | undefined\n\n constructor(opts: RelayClientOptions & OpenOptions & { me: UserId }) {\n this.store = new ChatStore(opts.me)\n const open: Extract<ClientFrame, { type: 'open' }> = {\n type: 'open',\n profileId: opts.profileId as never,\n ...(opts.subjectId ? { subjectId: opts.subjectId as never } : {}),\n ...(opts.subjectTitle ? { subjectTitle: opts.subjectTitle } : {}),\n ...(opts.kind === 'direct' ? { kind: 'direct' as const, peerId: asUserId(opts.peerId ?? '') } : {}),\n ...(opts.user ? { userInfo: opts.user } : {}),\n }\n // Accept any scheme (https/http/wss/ws) — a plain `https://api.…` URL is\n // resolved to the concrete `wss://…/ws` socket endpoint, exactly like the\n // bundled widget's mount(). Before this, RelayClient required a raw\n // WebSocket URL while the React components took `https://` — one URL now\n // works across the entire SDK.\n const { wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n this.conn = new ConnectionManager({\n url: wsUrl,\n token: opts.token ?? opts.me,\n open,\n getCursor: () => this.store.highestSeq(),\n onFrame: (f: ServerFrame) => {\n // The server tells us our CANONICAL id on auth (a signed JWT's sub,\n // not the raw token) — capture it so `mine` checks work under every\n // identity tier.\n if (f.type === 'authed') this._me = f.userId as UserId\n this.store.apply(f); this.emit()\n },\n onStatusChange: (s, msg) => { this._status = s; this._statusMessage = msg; this.emit() },\n })\n this.conn.connect()\n }\n\n /** Subscribe to any change (message, typing, status). Returns unsubscribe. */\n onChange(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => this.listeners.delete(fn)\n }\n private emit(): void { for (const fn of this.listeners) fn() }\n\n private _me: UserId | undefined\n /** Our canonical user id as resolved by the server (JWT sub / userId / anon id). */\n get me(): UserId | undefined { return this._me }\n get conversationId(): ConversationId | undefined { return this.store.conversationId }\n get status(): string { return this._status }\n get statusMessage(): string | undefined { return this._statusMessage }\n\n send(text: string): void {\n const clientMsgId = `c_${Date.now().toString(36)}_${++this.msgSeq}`\n const cid = this.store.conversationId\n if (!cid) return\n this.store.addOptimistic(clientMsgId, { kind: 'text', text })\n this.conn.send({ type: 'send', conversationId: cid, clientMsgId, content: { kind: 'text', text } })\n this.emit()\n }\n\n typing(isTyping: boolean, preview?: string): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'typing', conversationId: cid, isTyping, ...(preview ? { preview } : {}) })\n }\n\n markRead(): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'read', conversationId: cid, seq: this.store.highestSeq() })\n }\n\n close(): void { this.conn.close(); this.listeners.clear() }\n}\n\nexport class RelayClient {\n constructor(private readonly opts: RelayClientOptions) {}\n\n /** The identity this client will act as: the token's subject (resolved\n * server-side), the raw userId, or a persistent anonymous browser id. */\n me(): UserId {\n return asUserId(this.opts.token ?? persistentUid())\n }\n\n open(open: OpenOptions = {}): RelayConversation {\n return new RelayConversation({ ...this.opts, ...open, me: this.me() })\n }\n}\n"],"names":["TERMINAL_STATES","isTerminalState","state","toManifestAction","a","input","LIMITS","RelayConversation","opts","__publicField","ChatStore","open","asUserId","wsUrl","resolveRelayUrls","ConnectionManager","f","s","msg","fn","text","clientMsgId","cid","isTyping","preview","RelayClient","persistentUid"],"mappings":"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"}