@agentup-inksen/web-chat-core 0.0.1

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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @agentup-inksen/web-chat-core
2
+
3
+ Framework-agnostic SDK that the AgentUp web chat clients (`@agentup-inksen/web-chat-app`, the bubble widget, future React Native clients) build on top of.
4
+
5
+ ```ts
6
+ import { AgentUpChatClient, AgentUpChatHub } from '@agentup-inksen/web-chat-core';
7
+
8
+ const api = new AgentUpChatClient({
9
+ apiUrl: 'https://api.example.com',
10
+ visitorToken: visitorJwtFromHost,
11
+ tenantId: 'acme'
12
+ });
13
+
14
+ const agents = await api.listAgents();
15
+ const conversations = await api.listConversations();
16
+
17
+ const hub = new AgentUpChatHub(
18
+ { apiUrl: 'https://api.example.com', accessTokenFactory: () => visitorJwtFromHost },
19
+ {
20
+ onMessage: (msg) => console.log('new', msg),
21
+ onConnectionStateChanged: (s) => console.log('state', s)
22
+ }
23
+ );
24
+ await hub.start();
25
+ await hub.joinConversation(conversations[0].id);
26
+ ```
27
+
28
+ Build:
29
+
30
+ ```bash
31
+ npm install
32
+ npm run build
33
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ AgentUpChatClient: () => AgentUpChatClient,
26
+ AgentUpChatHub: () => AgentUpChatHub,
27
+ WebChatAuthError: () => WebChatAuthError,
28
+ storage: () => storage_exports
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/api-client.ts
33
+ var WebChatAuthError = class extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "WebChatAuthError";
37
+ }
38
+ };
39
+ var AgentUpChatClient = class {
40
+ constructor(config) {
41
+ __publicField(this, "baseUrl");
42
+ __publicField(this, "tenantId");
43
+ __publicField(this, "fetchImpl");
44
+ __publicField(this, "visitorToken");
45
+ if (!config.apiUrl) throw new Error("AgentUpChatClient: apiUrl is required");
46
+ if (!config.visitorToken) throw new Error("AgentUpChatClient: visitorToken is required");
47
+ this.baseUrl = config.apiUrl.replace(/\/$/, "");
48
+ this.visitorToken = config.visitorToken;
49
+ this.tenantId = config.tenantId;
50
+ this.fetchImpl = config.fetch ?? fetch.bind(globalThis);
51
+ }
52
+ /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */
53
+ setVisitorToken(token) {
54
+ if (!token) throw new Error("AgentUpChatClient.setVisitorToken: empty token");
55
+ this.visitorToken = token;
56
+ }
57
+ listAgents(signal) {
58
+ return this.get("/api/webchat/agents", signal);
59
+ }
60
+ listConversations(signal) {
61
+ return this.get("/api/webchat/conversations", signal);
62
+ }
63
+ createConversation(agentCode) {
64
+ return this.post("/api/webchat/conversations", { agentCode });
65
+ }
66
+ getConversation(conversationId, signal) {
67
+ return this.get(`/api/webchat/conversations/${conversationId}`, signal);
68
+ }
69
+ /**
70
+ * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on
71
+ * `/api/webchat/messages/{id}` — that route is POST-only for sending).
72
+ */
73
+ async getMessages(conversationId, signal) {
74
+ const conv = await this.get(`/api/webchat/conversations/${conversationId}`, signal);
75
+ const raw = conv.messages;
76
+ if (!raw?.length) return [];
77
+ return raw.map((m) => ({
78
+ ...m,
79
+ conversationId: m.conversationId || conversationId
80
+ }));
81
+ }
82
+ /**
83
+ * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),
84
+ * not JSON — multipart/form-data is required.
85
+ */
86
+ sendMessage(conversationId, content) {
87
+ const form = new FormData();
88
+ form.append("content", content);
89
+ return this.postForm(`/api/webchat/messages/${conversationId}`, form);
90
+ }
91
+ closeConversation(conversationId) {
92
+ return this.post(`/api/webchat/conversations/${conversationId}/close`, {});
93
+ }
94
+ // ---------------- internals ----------------
95
+ async get(path, signal) {
96
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
97
+ method: "GET",
98
+ headers: this.headers(),
99
+ signal
100
+ });
101
+ return this.unwrap(res);
102
+ }
103
+ async post(path, body) {
104
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
105
+ method: "POST",
106
+ headers: { ...this.headers(), "Content-Type": "application/json" },
107
+ body: JSON.stringify(body ?? {})
108
+ });
109
+ return this.unwrap(res);
110
+ }
111
+ /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */
112
+ async postForm(path, formData) {
113
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
114
+ method: "POST",
115
+ headers: this.headers(),
116
+ body: formData
117
+ });
118
+ return this.unwrap(res);
119
+ }
120
+ headers() {
121
+ const h = {
122
+ Authorization: `Bearer ${this.visitorToken}`,
123
+ Accept: "application/json"
124
+ };
125
+ if (this.tenantId) h["X-Tenant-Id"] = this.tenantId;
126
+ return h;
127
+ }
128
+ async unwrap(res) {
129
+ if (res.status === 401) {
130
+ throw new WebChatAuthError("Visitor token expired or invalid");
131
+ }
132
+ if (!res.ok) {
133
+ let detail = "";
134
+ try {
135
+ const text = await res.text();
136
+ detail = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
137
+ } catch {
138
+ }
139
+ throw new Error(`AgentUp request ${res.url} failed (${res.status}): ${detail}`);
140
+ }
141
+ if (res.status === 204) return void 0;
142
+ const json = await res.json();
143
+ if (json && typeof json === "object" && "success" in json && "result" in json) {
144
+ const env = json;
145
+ if (!env.success) {
146
+ throw new Error(env.message ?? "AgentUp request failed");
147
+ }
148
+ return env.result;
149
+ }
150
+ return json;
151
+ }
152
+ };
153
+
154
+ // src/signalr-client.ts
155
+ var import_signalr = require("@microsoft/signalr");
156
+ var AgentUpChatHub = class {
157
+ constructor(options, handlers = {}) {
158
+ __publicField(this, "options");
159
+ __publicField(this, "handlers");
160
+ __publicField(this, "connection", null);
161
+ __publicField(this, "state", "idle");
162
+ this.options = options;
163
+ this.handlers = handlers;
164
+ }
165
+ get currentState() {
166
+ return this.state;
167
+ }
168
+ async start() {
169
+ if (this.connection && this.connection.state === import_signalr.HubConnectionState.Connected) {
170
+ return;
171
+ }
172
+ this.setState("connecting");
173
+ const base = this.options.apiUrl.replace(/\/$/, "");
174
+ const url = `${base}/api/chatHub`;
175
+ this.connection = new import_signalr.HubConnectionBuilder().withUrl(url, {
176
+ accessTokenFactory: () => Promise.resolve(this.options.accessTokenFactory())
177
+ }).withAutomaticReconnect().configureLogging(import_signalr.LogLevel.Warning).build();
178
+ this.connection.on("ReceiveMessage", (msg) => this.handlers.onMessage?.(msg));
179
+ this.connection.on(
180
+ "ReceiveMessageChunk",
181
+ (chunk) => this.handlers.onMessageChunk?.(chunk)
182
+ );
183
+ this.connection.on(
184
+ "UserTyping",
185
+ (payload) => this.handlers.onUserTyping?.(payload)
186
+ );
187
+ this.connection.on(
188
+ "UserStoppedTyping",
189
+ (userId) => this.handlers.onUserStoppedTyping?.(userId)
190
+ );
191
+ this.connection.on("Error", (msg) => this.handlers.onError?.(new Error(msg)));
192
+ this.connection.onreconnecting(() => this.setState("reconnecting"));
193
+ this.connection.onreconnected(() => this.setState("connected"));
194
+ this.connection.onclose(() => this.setState("disconnected"));
195
+ await this.connection.start();
196
+ this.setState("connected");
197
+ }
198
+ async stop() {
199
+ if (this.connection) {
200
+ await this.connection.stop();
201
+ this.connection = null;
202
+ }
203
+ this.setState("idle");
204
+ }
205
+ async joinConversation(conversationId) {
206
+ await this.ensureConnected();
207
+ await this.connection.invoke("JoinConversation", conversationId);
208
+ }
209
+ async leaveConversation(conversationId) {
210
+ if (this.connection?.state !== import_signalr.HubConnectionState.Connected) return;
211
+ await this.connection.invoke("LeaveConversation", conversationId);
212
+ }
213
+ async sendMessage(conversationId, content) {
214
+ await this.ensureConnected();
215
+ await this.connection.invoke("SendMessage", conversationId, content);
216
+ }
217
+ async sendTyping(conversationId, isTyping) {
218
+ if (this.connection?.state !== import_signalr.HubConnectionState.Connected) return;
219
+ await this.connection.invoke("Typing", conversationId, isTyping);
220
+ }
221
+ async ensureConnected() {
222
+ if (!this.connection) {
223
+ await this.start();
224
+ return;
225
+ }
226
+ if (this.connection.state !== import_signalr.HubConnectionState.Connected) {
227
+ await this.connection.start();
228
+ this.setState("connected");
229
+ }
230
+ }
231
+ setState(next) {
232
+ if (this.state === next) return;
233
+ this.state = next;
234
+ this.handlers.onConnectionStateChanged?.(next);
235
+ }
236
+ };
237
+
238
+ // src/storage.ts
239
+ var storage_exports = {};
240
+ __export(storage_exports, {
241
+ clearToken: () => clearToken,
242
+ getItem: () => getItem,
243
+ loadToken: () => loadToken,
244
+ removeItem: () => removeItem,
245
+ saveToken: () => saveToken,
246
+ setItem: () => setItem
247
+ });
248
+ var PREFIX = "agentup.webchat:";
249
+ function getItem(key) {
250
+ try {
251
+ return localStorage.getItem(PREFIX + key);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function setItem(key, value) {
257
+ try {
258
+ localStorage.setItem(PREFIX + key, value);
259
+ } catch {
260
+ }
261
+ }
262
+ function removeItem(key) {
263
+ try {
264
+ localStorage.removeItem(PREFIX + key);
265
+ } catch {
266
+ }
267
+ }
268
+ function saveToken(token) {
269
+ setItem("visitor_token", token);
270
+ }
271
+ function loadToken() {
272
+ return getItem("visitor_token");
273
+ }
274
+ function clearToken() {
275
+ removeItem("visitor_token");
276
+ }
277
+ // Annotate the CommonJS export names for ESM import in node:
278
+ 0 && (module.exports = {
279
+ AgentUpChatClient,
280
+ AgentUpChatHub,
281
+ WebChatAuthError,
282
+ storage
283
+ });
284
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/api-client.ts","../src/signalr-client.ts","../src/storage.ts"],"sourcesContent":["export * from './types.js';\r\nexport { AgentUpChatClient, WebChatAuthError } from './api-client.js';\r\nexport { AgentUpChatHub } from './signalr-client.js';\r\nexport type {\r\n AgentUpChatHubOptions,\r\n AgentUpChatHubHandlers,\r\n ChatConnectionState\r\n} from './signalr-client.js';\r\nexport * as storage from './storage.js';\r\n","import type {\r\n AgentUpChatClientConfig,\r\n ChatMessage,\r\n ConversationDetails,\r\n PublicAgent,\r\n VisitorConversationSummary\r\n} from './types.js';\r\n\r\n/** Thrown when the visitor token is expired/invalid; UIs should surface a re-auth flow. */\r\nexport class WebChatAuthError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = 'WebChatAuthError';\r\n }\r\n}\r\n\r\n/**\r\n * Minimal REST client for the embedded web chat app endpoints. Wraps fetch with\r\n * the visitor JWT and unwraps the AgentUp `ApiResponse<T>` envelope automatically.\r\n *\r\n * Endpoints used:\r\n * - `GET /api/webchat/agents`\r\n * - `GET /api/webchat/conversations`\r\n * - `POST /api/webchat/conversations`\r\n * - `GET /api/webchat/conversations/{id}` (includes `messages` history)\r\n * - `POST /api/webchat/messages/{conversationId}` (send)\r\n * - `POST /api/webchat/conversations/{id}/close`\r\n */\r\nexport class AgentUpChatClient {\r\n private readonly baseUrl: string;\r\n private readonly tenantId?: string;\r\n private readonly fetchImpl: typeof fetch;\r\n private visitorToken: string;\r\n\r\n constructor(config: AgentUpChatClientConfig) {\r\n if (!config.apiUrl) throw new Error('AgentUpChatClient: apiUrl is required');\r\n if (!config.visitorToken) throw new Error('AgentUpChatClient: visitorToken is required');\r\n this.baseUrl = config.apiUrl.replace(/\\/$/, '');\r\n this.visitorToken = config.visitorToken;\r\n this.tenantId = config.tenantId;\r\n this.fetchImpl = config.fetch ?? fetch.bind(globalThis);\r\n }\r\n\r\n /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */\r\n setVisitorToken(token: string): void {\r\n if (!token) throw new Error('AgentUpChatClient.setVisitorToken: empty token');\r\n this.visitorToken = token;\r\n }\r\n\r\n listAgents(signal?: AbortSignal): Promise<PublicAgent[]> {\r\n return this.get<PublicAgent[]>('/api/webchat/agents', signal);\r\n }\r\n\r\n listConversations(signal?: AbortSignal): Promise<VisitorConversationSummary[]> {\r\n return this.get<VisitorConversationSummary[]>('/api/webchat/conversations', signal);\r\n }\r\n\r\n createConversation(agentCode: string): Promise<VisitorConversationSummary> {\r\n return this.post<VisitorConversationSummary>('/api/webchat/conversations', { agentCode });\r\n }\r\n\r\n getConversation(conversationId: string, signal?: AbortSignal): Promise<ConversationDetails> {\r\n return this.get<ConversationDetails>(`/api/webchat/conversations/${conversationId}`, signal);\r\n }\r\n\r\n /**\r\n * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on\r\n * `/api/webchat/messages/{id}` — that route is POST-only for sending).\r\n */\r\n async getMessages(conversationId: string, signal?: AbortSignal): Promise<ChatMessage[]> {\r\n const conv = await this.get<ConversationDetails>(`/api/webchat/conversations/${conversationId}`, signal);\r\n const raw = conv.messages;\r\n if (!raw?.length) return [];\r\n return raw.map((m) => ({\r\n ...m,\r\n conversationId: m.conversationId || conversationId\r\n }));\r\n }\r\n\r\n /**\r\n * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),\r\n * not JSON — multipart/form-data is required.\r\n */\r\n sendMessage(conversationId: string, content: string): Promise<ChatMessage> {\r\n const form = new FormData();\r\n form.append('content', content);\r\n return this.postForm<ChatMessage>(`/api/webchat/messages/${conversationId}`, form);\r\n }\r\n\r\n closeConversation(conversationId: string): Promise<void> {\r\n return this.post<void>(`/api/webchat/conversations/${conversationId}/close`, {});\r\n }\r\n\r\n // ---------------- internals ----------------\r\n\r\n private async get<T>(path: string, signal?: AbortSignal): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'GET',\r\n headers: this.headers(),\r\n signal\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n private async post<T>(path: string, body: unknown): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'POST',\r\n headers: { ...this.headers(), 'Content-Type': 'application/json' },\r\n body: JSON.stringify(body ?? {})\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */\r\n private async postForm<T>(path: string, formData: FormData): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'POST',\r\n headers: this.headers(),\r\n body: formData\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n private headers(): Record<string, string> {\r\n const h: Record<string, string> = {\r\n Authorization: `Bearer ${this.visitorToken}`,\r\n Accept: 'application/json'\r\n };\r\n if (this.tenantId) h['X-Tenant-Id'] = this.tenantId;\r\n return h;\r\n }\r\n\r\n private async unwrap<T>(res: Response): Promise<T> {\r\n if (res.status === 401) {\r\n throw new WebChatAuthError('Visitor token expired or invalid');\r\n }\r\n if (!res.ok) {\r\n let detail = '';\r\n try {\r\n const text = await res.text();\r\n detail = text.length > 200 ? text.slice(0, 200) + '…' : text;\r\n } catch {\r\n // ignore body parse errors\r\n }\r\n throw new Error(`AgentUp request ${res.url} failed (${res.status}): ${detail}`);\r\n }\r\n if (res.status === 204) return undefined as T;\r\n\r\n const json = (await res.json()) as unknown;\r\n // Backend wraps responses as { success, result } except for a few skipped paths.\r\n if (\r\n json &&\r\n typeof json === 'object' &&\r\n 'success' in (json as Record<string, unknown>) &&\r\n 'result' in (json as Record<string, unknown>)\r\n ) {\r\n const env = json as { success: boolean; result: T; message?: string };\r\n if (!env.success) {\r\n throw new Error(env.message ?? 'AgentUp request failed');\r\n }\r\n return env.result;\r\n }\r\n return json as T;\r\n }\r\n}\r\n","import {\r\n HubConnection,\r\n HubConnectionBuilder,\r\n HubConnectionState,\r\n LogLevel\r\n} from '@microsoft/signalr';\r\n\r\nimport type { ChatMessage, ChatMessageChunk, UserTypingPayload } from './types.js';\r\n\r\n/** Connection lifecycle states surfaced to UI layers. */\r\nexport type ChatConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';\r\n\r\nexport interface AgentUpChatHubOptions {\r\n /** Base URL of the AgentUp API. */\r\n apiUrl: string;\r\n /** Provides the latest visitor JWT (called on initial connect AND on reconnect). */\r\n accessTokenFactory: () => string | Promise<string>;\r\n}\r\n\r\nexport interface AgentUpChatHubHandlers {\r\n onMessage?: (message: ChatMessage) => void;\r\n /** AI/agent streaming tokens before the final `ReceiveMessage`. */\r\n onMessageChunk?: (chunk: ChatMessageChunk) => void;\r\n /** Hub `UserTyping` — includes AI (`userId` `\"ai\"`) and human agents. */\r\n onUserTyping?: (payload: UserTypingPayload) => void;\r\n /** Hub `UserStoppedTyping` — argument is `userId` (e.g. `\"ai\"`). */\r\n onUserStoppedTyping?: (userId: string) => void;\r\n onConnectionStateChanged?: (state: ChatConnectionState) => void;\r\n onError?: (error: unknown) => void;\r\n}\r\n\r\n/**\r\n * Thin SignalR client for the AgentUp ChatHub. Intentionally narrow surface — the\r\n * UI layer owns active-conversation tracking and group join/leave logic.\r\n *\r\n * Server methods used:\r\n * - `JoinConversation(conversationId)` / `LeaveConversation(conversationId)`\r\n * - `SendMessage(conversationId, content)`\r\n * - `Typing(conversationId, isTyping)`\r\n *\r\n * Server callbacks consumed:\r\n * - `ReceiveMessage`, `ReceiveMessageChunk` (streaming), `UserTyping`, `UserStoppedTyping`, `Error`.\r\n */\r\nexport class AgentUpChatHub {\r\n private readonly options: AgentUpChatHubOptions;\r\n private readonly handlers: AgentUpChatHubHandlers;\r\n private connection: HubConnection | null = null;\r\n private state: ChatConnectionState = 'idle';\r\n\r\n constructor(options: AgentUpChatHubOptions, handlers: AgentUpChatHubHandlers = {}) {\r\n this.options = options;\r\n this.handlers = handlers;\r\n }\r\n\r\n get currentState(): ChatConnectionState {\r\n return this.state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n if (this.connection && this.connection.state === HubConnectionState.Connected) {\r\n return;\r\n }\r\n this.setState('connecting');\r\n\r\n const base = this.options.apiUrl.replace(/\\/$/, '');\r\n /** Must match ASP.NET `MapHub<ChatHub>(\"/api/chatHub\")` (same path as legacy widget). */\r\n const url = `${base}/api/chatHub`;\r\n this.connection = new HubConnectionBuilder()\r\n .withUrl(url, {\r\n accessTokenFactory: () => Promise.resolve(this.options.accessTokenFactory())\r\n })\r\n .withAutomaticReconnect()\r\n .configureLogging(LogLevel.Warning)\r\n .build();\r\n\r\n this.connection.on('ReceiveMessage', (msg: ChatMessage) => this.handlers.onMessage?.(msg));\r\n this.connection.on('ReceiveMessageChunk', (chunk: ChatMessageChunk) =>\r\n this.handlers.onMessageChunk?.(chunk)\r\n );\r\n this.connection.on('UserTyping', (payload: UserTypingPayload) =>\r\n this.handlers.onUserTyping?.(payload)\r\n );\r\n this.connection.on('UserStoppedTyping', (userId: string) =>\r\n this.handlers.onUserStoppedTyping?.(userId)\r\n );\r\n this.connection.on('Error', (msg: string) => this.handlers.onError?.(new Error(msg)));\r\n\r\n this.connection.onreconnecting(() => this.setState('reconnecting'));\r\n this.connection.onreconnected(() => this.setState('connected'));\r\n this.connection.onclose(() => this.setState('disconnected'));\r\n\r\n await this.connection.start();\r\n this.setState('connected');\r\n }\r\n\r\n async stop(): Promise<void> {\r\n if (this.connection) {\r\n await this.connection.stop();\r\n this.connection = null;\r\n }\r\n this.setState('idle');\r\n }\r\n\r\n async joinConversation(conversationId: string): Promise<void> {\r\n await this.ensureConnected();\r\n await this.connection!.invoke('JoinConversation', conversationId);\r\n }\r\n\r\n async leaveConversation(conversationId: string): Promise<void> {\r\n if (this.connection?.state !== HubConnectionState.Connected) return;\r\n await this.connection.invoke('LeaveConversation', conversationId);\r\n }\r\n\r\n async sendMessage(conversationId: string, content: string): Promise<void> {\r\n await this.ensureConnected();\r\n await this.connection!.invoke('SendMessage', conversationId, content);\r\n }\r\n\r\n async sendTyping(conversationId: string, isTyping: boolean): Promise<void> {\r\n if (this.connection?.state !== HubConnectionState.Connected) return;\r\n await this.connection.invoke('Typing', conversationId, isTyping);\r\n }\r\n\r\n private async ensureConnected(): Promise<void> {\r\n if (!this.connection) {\r\n await this.start();\r\n return;\r\n }\r\n if (this.connection.state !== HubConnectionState.Connected) {\r\n await this.connection.start();\r\n this.setState('connected');\r\n }\r\n }\r\n\r\n private setState(next: ChatConnectionState): void {\r\n if (this.state === next) return;\r\n this.state = next;\r\n this.handlers.onConnectionStateChanged?.(next);\r\n }\r\n}\r\n","/**\r\n * Local storage helper for the embedded chat app. Keeps ephemeral UI state\r\n * (selected conversation id, sidebar collapsed, etc.) outside React-style\r\n * components so the choice of UI framework stays flexible.\r\n *\r\n * Storage is **not** used for the visitor token by default — tokens are short\r\n * lived and should be re-minted from the host backend on demand. Hosts can\r\n * still call {@link saveToken} if they want browser refresh persistence.\r\n */\r\nconst PREFIX = 'agentup.webchat:';\r\n\r\nexport function getItem(key: string): string | null {\r\n try {\r\n return localStorage.getItem(PREFIX + key);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function setItem(key: string, value: string): void {\r\n try {\r\n localStorage.setItem(PREFIX + key, value);\r\n } catch {\r\n // Storage may be disabled (private mode); silently degrade.\r\n }\r\n}\r\n\r\nexport function removeItem(key: string): void {\r\n try {\r\n localStorage.removeItem(PREFIX + key);\r\n } catch {\r\n // ignore\r\n }\r\n}\r\n\r\n/** Convenience wrapper for the visitor token (host-controlled persistence). */\r\nexport function saveToken(token: string): void {\r\n setItem('visitor_token', token);\r\n}\r\n\r\nexport function loadToken(): string | null {\r\n return getItem('visitor_token');\r\n}\r\n\r\nexport function clearToken(): void {\r\n removeItem('visitor_token');\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Shared types for the AgentUp web chat clients (mirrors the .NET DTOs).
3
+ *
4
+ * Keep this file in sync with `src/Inksen.AI.AgentUp.Api.Models/DTOs/WebChatDtos.cs`.
5
+ */
6
+ interface PublicAgent {
7
+ /** AiAgent.Id (UUID v7). */
8
+ id: string;
9
+ /** Stable code referenced by `WebChatSession.Metadata.agentCode`. */
10
+ code: string;
11
+ name: string;
12
+ description?: string | null;
13
+ avatarUrl?: string | null;
14
+ welcomeMessage?: string | null;
15
+ displayOrder: number;
16
+ }
17
+ /** Mirrors `Domain.Entities.ConversationStatus`. */
18
+ type ConversationStatus = 0 | 1 | 2 | 3;
19
+ interface VisitorConversationSummary {
20
+ id: string;
21
+ agentCode?: string | null;
22
+ agentName?: string | null;
23
+ /** AI-provided short label when available (OpenAI Responses metadata, etc.). */
24
+ title?: string | null;
25
+ lastMessageSnippet?: string | null;
26
+ lastMessageAt?: string | null;
27
+ status: ConversationStatus;
28
+ createdAt: string;
29
+ }
30
+ interface VisitorTokenResponse {
31
+ visitorToken: string;
32
+ expiresAt: string;
33
+ visitorId: string;
34
+ }
35
+ /** Mirrors `Domain.Entities.SenderType`. */
36
+ type SenderType = 'Customer' | 'Agent' | 'AI' | 'System';
37
+ interface ChatMessage {
38
+ id: string;
39
+ conversationId: string;
40
+ content: string;
41
+ senderType: SenderType;
42
+ senderName?: string | null;
43
+ createdAt: string;
44
+ /** Optional: AI tool calls / structured payload (passthrough). */
45
+ metadata?: Record<string, unknown> | null;
46
+ }
47
+ /** Matches `TypingIndicatorDto` on hub event `UserTyping` (camelCase JSON). */
48
+ interface UserTypingPayload {
49
+ conversationId: string;
50
+ userId: string;
51
+ userName?: string | null;
52
+ isTyping?: boolean;
53
+ }
54
+ /** Matches ChatHub / WebChatAiResponseService `ReceiveMessageChunk` payload (camelCase JSON). */
55
+ interface ChatMessageChunk {
56
+ id: string;
57
+ conversationId: string;
58
+ delta: string;
59
+ /** API may send numeric enum (Customer=0, AI=1, Agent=2). */
60
+ senderType?: SenderType | number;
61
+ senderName?: string | null;
62
+ createdAt?: string;
63
+ }
64
+ interface ConversationDetails {
65
+ id: string;
66
+ status: ConversationStatus;
67
+ customerName?: string | null;
68
+ customerEmail?: string | null;
69
+ createdAt: string;
70
+ lastMessageAt?: string | null;
71
+ isHumanTakeover: boolean;
72
+ /** Present on `GET /api/webchat/conversations/{id}` (embedded app history). */
73
+ messages?: ChatMessage[];
74
+ }
75
+ /** Configuration accepted by `AgentUpChatClient`. */
76
+ interface AgentUpChatClientConfig {
77
+ /** Base URL of the AgentUp API (no trailing slash, e.g. `https://api.example.com`). */
78
+ apiUrl: string;
79
+ /** Visitor JWT minted by the host backend via `POST /api/webchat/visitor-tokens`. */
80
+ visitorToken: string;
81
+ /** Optional `X-Tenant-Id` header. Required when the API runs without subdomain-based routing. */
82
+ tenantId?: string;
83
+ /** Optional fetch implementation (defaults to global `fetch`). */
84
+ fetch?: typeof fetch;
85
+ }
86
+
87
+ /** Thrown when the visitor token is expired/invalid; UIs should surface a re-auth flow. */
88
+ declare class WebChatAuthError extends Error {
89
+ constructor(message: string);
90
+ }
91
+ /**
92
+ * Minimal REST client for the embedded web chat app endpoints. Wraps fetch with
93
+ * the visitor JWT and unwraps the AgentUp `ApiResponse<T>` envelope automatically.
94
+ *
95
+ * Endpoints used:
96
+ * - `GET /api/webchat/agents`
97
+ * - `GET /api/webchat/conversations`
98
+ * - `POST /api/webchat/conversations`
99
+ * - `GET /api/webchat/conversations/{id}` (includes `messages` history)
100
+ * - `POST /api/webchat/messages/{conversationId}` (send)
101
+ * - `POST /api/webchat/conversations/{id}/close`
102
+ */
103
+ declare class AgentUpChatClient {
104
+ private readonly baseUrl;
105
+ private readonly tenantId?;
106
+ private readonly fetchImpl;
107
+ private visitorToken;
108
+ constructor(config: AgentUpChatClientConfig);
109
+ /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */
110
+ setVisitorToken(token: string): void;
111
+ listAgents(signal?: AbortSignal): Promise<PublicAgent[]>;
112
+ listConversations(signal?: AbortSignal): Promise<VisitorConversationSummary[]>;
113
+ createConversation(agentCode: string): Promise<VisitorConversationSummary>;
114
+ getConversation(conversationId: string, signal?: AbortSignal): Promise<ConversationDetails>;
115
+ /**
116
+ * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on
117
+ * `/api/webchat/messages/{id}` — that route is POST-only for sending).
118
+ */
119
+ getMessages(conversationId: string, signal?: AbortSignal): Promise<ChatMessage[]>;
120
+ /**
121
+ * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),
122
+ * not JSON — multipart/form-data is required.
123
+ */
124
+ sendMessage(conversationId: string, content: string): Promise<ChatMessage>;
125
+ closeConversation(conversationId: string): Promise<void>;
126
+ private get;
127
+ private post;
128
+ /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */
129
+ private postForm;
130
+ private headers;
131
+ private unwrap;
132
+ }
133
+
134
+ /** Connection lifecycle states surfaced to UI layers. */
135
+ type ChatConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
136
+ interface AgentUpChatHubOptions {
137
+ /** Base URL of the AgentUp API. */
138
+ apiUrl: string;
139
+ /** Provides the latest visitor JWT (called on initial connect AND on reconnect). */
140
+ accessTokenFactory: () => string | Promise<string>;
141
+ }
142
+ interface AgentUpChatHubHandlers {
143
+ onMessage?: (message: ChatMessage) => void;
144
+ /** AI/agent streaming tokens before the final `ReceiveMessage`. */
145
+ onMessageChunk?: (chunk: ChatMessageChunk) => void;
146
+ /** Hub `UserTyping` — includes AI (`userId` `"ai"`) and human agents. */
147
+ onUserTyping?: (payload: UserTypingPayload) => void;
148
+ /** Hub `UserStoppedTyping` — argument is `userId` (e.g. `"ai"`). */
149
+ onUserStoppedTyping?: (userId: string) => void;
150
+ onConnectionStateChanged?: (state: ChatConnectionState) => void;
151
+ onError?: (error: unknown) => void;
152
+ }
153
+ /**
154
+ * Thin SignalR client for the AgentUp ChatHub. Intentionally narrow surface — the
155
+ * UI layer owns active-conversation tracking and group join/leave logic.
156
+ *
157
+ * Server methods used:
158
+ * - `JoinConversation(conversationId)` / `LeaveConversation(conversationId)`
159
+ * - `SendMessage(conversationId, content)`
160
+ * - `Typing(conversationId, isTyping)`
161
+ *
162
+ * Server callbacks consumed:
163
+ * - `ReceiveMessage`, `ReceiveMessageChunk` (streaming), `UserTyping`, `UserStoppedTyping`, `Error`.
164
+ */
165
+ declare class AgentUpChatHub {
166
+ private readonly options;
167
+ private readonly handlers;
168
+ private connection;
169
+ private state;
170
+ constructor(options: AgentUpChatHubOptions, handlers?: AgentUpChatHubHandlers);
171
+ get currentState(): ChatConnectionState;
172
+ start(): Promise<void>;
173
+ stop(): Promise<void>;
174
+ joinConversation(conversationId: string): Promise<void>;
175
+ leaveConversation(conversationId: string): Promise<void>;
176
+ sendMessage(conversationId: string, content: string): Promise<void>;
177
+ sendTyping(conversationId: string, isTyping: boolean): Promise<void>;
178
+ private ensureConnected;
179
+ private setState;
180
+ }
181
+
182
+ declare function getItem(key: string): string | null;
183
+ declare function setItem(key: string, value: string): void;
184
+ declare function removeItem(key: string): void;
185
+ /** Convenience wrapper for the visitor token (host-controlled persistence). */
186
+ declare function saveToken(token: string): void;
187
+ declare function loadToken(): string | null;
188
+ declare function clearToken(): void;
189
+
190
+ declare const storage_clearToken: typeof clearToken;
191
+ declare const storage_getItem: typeof getItem;
192
+ declare const storage_loadToken: typeof loadToken;
193
+ declare const storage_removeItem: typeof removeItem;
194
+ declare const storage_saveToken: typeof saveToken;
195
+ declare const storage_setItem: typeof setItem;
196
+ declare namespace storage {
197
+ export { storage_clearToken as clearToken, storage_getItem as getItem, storage_loadToken as loadToken, storage_removeItem as removeItem, storage_saveToken as saveToken, storage_setItem as setItem };
198
+ }
199
+
200
+ export { AgentUpChatClient, type AgentUpChatClientConfig, AgentUpChatHub, type AgentUpChatHubHandlers, type AgentUpChatHubOptions, type ChatConnectionState, type ChatMessage, type ChatMessageChunk, type ConversationDetails, type ConversationStatus, type PublicAgent, type SenderType, type UserTypingPayload, type VisitorConversationSummary, type VisitorTokenResponse, WebChatAuthError, storage };
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Shared types for the AgentUp web chat clients (mirrors the .NET DTOs).
3
+ *
4
+ * Keep this file in sync with `src/Inksen.AI.AgentUp.Api.Models/DTOs/WebChatDtos.cs`.
5
+ */
6
+ interface PublicAgent {
7
+ /** AiAgent.Id (UUID v7). */
8
+ id: string;
9
+ /** Stable code referenced by `WebChatSession.Metadata.agentCode`. */
10
+ code: string;
11
+ name: string;
12
+ description?: string | null;
13
+ avatarUrl?: string | null;
14
+ welcomeMessage?: string | null;
15
+ displayOrder: number;
16
+ }
17
+ /** Mirrors `Domain.Entities.ConversationStatus`. */
18
+ type ConversationStatus = 0 | 1 | 2 | 3;
19
+ interface VisitorConversationSummary {
20
+ id: string;
21
+ agentCode?: string | null;
22
+ agentName?: string | null;
23
+ /** AI-provided short label when available (OpenAI Responses metadata, etc.). */
24
+ title?: string | null;
25
+ lastMessageSnippet?: string | null;
26
+ lastMessageAt?: string | null;
27
+ status: ConversationStatus;
28
+ createdAt: string;
29
+ }
30
+ interface VisitorTokenResponse {
31
+ visitorToken: string;
32
+ expiresAt: string;
33
+ visitorId: string;
34
+ }
35
+ /** Mirrors `Domain.Entities.SenderType`. */
36
+ type SenderType = 'Customer' | 'Agent' | 'AI' | 'System';
37
+ interface ChatMessage {
38
+ id: string;
39
+ conversationId: string;
40
+ content: string;
41
+ senderType: SenderType;
42
+ senderName?: string | null;
43
+ createdAt: string;
44
+ /** Optional: AI tool calls / structured payload (passthrough). */
45
+ metadata?: Record<string, unknown> | null;
46
+ }
47
+ /** Matches `TypingIndicatorDto` on hub event `UserTyping` (camelCase JSON). */
48
+ interface UserTypingPayload {
49
+ conversationId: string;
50
+ userId: string;
51
+ userName?: string | null;
52
+ isTyping?: boolean;
53
+ }
54
+ /** Matches ChatHub / WebChatAiResponseService `ReceiveMessageChunk` payload (camelCase JSON). */
55
+ interface ChatMessageChunk {
56
+ id: string;
57
+ conversationId: string;
58
+ delta: string;
59
+ /** API may send numeric enum (Customer=0, AI=1, Agent=2). */
60
+ senderType?: SenderType | number;
61
+ senderName?: string | null;
62
+ createdAt?: string;
63
+ }
64
+ interface ConversationDetails {
65
+ id: string;
66
+ status: ConversationStatus;
67
+ customerName?: string | null;
68
+ customerEmail?: string | null;
69
+ createdAt: string;
70
+ lastMessageAt?: string | null;
71
+ isHumanTakeover: boolean;
72
+ /** Present on `GET /api/webchat/conversations/{id}` (embedded app history). */
73
+ messages?: ChatMessage[];
74
+ }
75
+ /** Configuration accepted by `AgentUpChatClient`. */
76
+ interface AgentUpChatClientConfig {
77
+ /** Base URL of the AgentUp API (no trailing slash, e.g. `https://api.example.com`). */
78
+ apiUrl: string;
79
+ /** Visitor JWT minted by the host backend via `POST /api/webchat/visitor-tokens`. */
80
+ visitorToken: string;
81
+ /** Optional `X-Tenant-Id` header. Required when the API runs without subdomain-based routing. */
82
+ tenantId?: string;
83
+ /** Optional fetch implementation (defaults to global `fetch`). */
84
+ fetch?: typeof fetch;
85
+ }
86
+
87
+ /** Thrown when the visitor token is expired/invalid; UIs should surface a re-auth flow. */
88
+ declare class WebChatAuthError extends Error {
89
+ constructor(message: string);
90
+ }
91
+ /**
92
+ * Minimal REST client for the embedded web chat app endpoints. Wraps fetch with
93
+ * the visitor JWT and unwraps the AgentUp `ApiResponse<T>` envelope automatically.
94
+ *
95
+ * Endpoints used:
96
+ * - `GET /api/webchat/agents`
97
+ * - `GET /api/webchat/conversations`
98
+ * - `POST /api/webchat/conversations`
99
+ * - `GET /api/webchat/conversations/{id}` (includes `messages` history)
100
+ * - `POST /api/webchat/messages/{conversationId}` (send)
101
+ * - `POST /api/webchat/conversations/{id}/close`
102
+ */
103
+ declare class AgentUpChatClient {
104
+ private readonly baseUrl;
105
+ private readonly tenantId?;
106
+ private readonly fetchImpl;
107
+ private visitorToken;
108
+ constructor(config: AgentUpChatClientConfig);
109
+ /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */
110
+ setVisitorToken(token: string): void;
111
+ listAgents(signal?: AbortSignal): Promise<PublicAgent[]>;
112
+ listConversations(signal?: AbortSignal): Promise<VisitorConversationSummary[]>;
113
+ createConversation(agentCode: string): Promise<VisitorConversationSummary>;
114
+ getConversation(conversationId: string, signal?: AbortSignal): Promise<ConversationDetails>;
115
+ /**
116
+ * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on
117
+ * `/api/webchat/messages/{id}` — that route is POST-only for sending).
118
+ */
119
+ getMessages(conversationId: string, signal?: AbortSignal): Promise<ChatMessage[]>;
120
+ /**
121
+ * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),
122
+ * not JSON — multipart/form-data is required.
123
+ */
124
+ sendMessage(conversationId: string, content: string): Promise<ChatMessage>;
125
+ closeConversation(conversationId: string): Promise<void>;
126
+ private get;
127
+ private post;
128
+ /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */
129
+ private postForm;
130
+ private headers;
131
+ private unwrap;
132
+ }
133
+
134
+ /** Connection lifecycle states surfaced to UI layers. */
135
+ type ChatConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
136
+ interface AgentUpChatHubOptions {
137
+ /** Base URL of the AgentUp API. */
138
+ apiUrl: string;
139
+ /** Provides the latest visitor JWT (called on initial connect AND on reconnect). */
140
+ accessTokenFactory: () => string | Promise<string>;
141
+ }
142
+ interface AgentUpChatHubHandlers {
143
+ onMessage?: (message: ChatMessage) => void;
144
+ /** AI/agent streaming tokens before the final `ReceiveMessage`. */
145
+ onMessageChunk?: (chunk: ChatMessageChunk) => void;
146
+ /** Hub `UserTyping` — includes AI (`userId` `"ai"`) and human agents. */
147
+ onUserTyping?: (payload: UserTypingPayload) => void;
148
+ /** Hub `UserStoppedTyping` — argument is `userId` (e.g. `"ai"`). */
149
+ onUserStoppedTyping?: (userId: string) => void;
150
+ onConnectionStateChanged?: (state: ChatConnectionState) => void;
151
+ onError?: (error: unknown) => void;
152
+ }
153
+ /**
154
+ * Thin SignalR client for the AgentUp ChatHub. Intentionally narrow surface — the
155
+ * UI layer owns active-conversation tracking and group join/leave logic.
156
+ *
157
+ * Server methods used:
158
+ * - `JoinConversation(conversationId)` / `LeaveConversation(conversationId)`
159
+ * - `SendMessage(conversationId, content)`
160
+ * - `Typing(conversationId, isTyping)`
161
+ *
162
+ * Server callbacks consumed:
163
+ * - `ReceiveMessage`, `ReceiveMessageChunk` (streaming), `UserTyping`, `UserStoppedTyping`, `Error`.
164
+ */
165
+ declare class AgentUpChatHub {
166
+ private readonly options;
167
+ private readonly handlers;
168
+ private connection;
169
+ private state;
170
+ constructor(options: AgentUpChatHubOptions, handlers?: AgentUpChatHubHandlers);
171
+ get currentState(): ChatConnectionState;
172
+ start(): Promise<void>;
173
+ stop(): Promise<void>;
174
+ joinConversation(conversationId: string): Promise<void>;
175
+ leaveConversation(conversationId: string): Promise<void>;
176
+ sendMessage(conversationId: string, content: string): Promise<void>;
177
+ sendTyping(conversationId: string, isTyping: boolean): Promise<void>;
178
+ private ensureConnected;
179
+ private setState;
180
+ }
181
+
182
+ declare function getItem(key: string): string | null;
183
+ declare function setItem(key: string, value: string): void;
184
+ declare function removeItem(key: string): void;
185
+ /** Convenience wrapper for the visitor token (host-controlled persistence). */
186
+ declare function saveToken(token: string): void;
187
+ declare function loadToken(): string | null;
188
+ declare function clearToken(): void;
189
+
190
+ declare const storage_clearToken: typeof clearToken;
191
+ declare const storage_getItem: typeof getItem;
192
+ declare const storage_loadToken: typeof loadToken;
193
+ declare const storage_removeItem: typeof removeItem;
194
+ declare const storage_saveToken: typeof saveToken;
195
+ declare const storage_setItem: typeof setItem;
196
+ declare namespace storage {
197
+ export { storage_clearToken as clearToken, storage_getItem as getItem, storage_loadToken as loadToken, storage_removeItem as removeItem, storage_saveToken as saveToken, storage_setItem as setItem };
198
+ }
199
+
200
+ export { AgentUpChatClient, type AgentUpChatClientConfig, AgentUpChatHub, type AgentUpChatHubHandlers, type AgentUpChatHubOptions, type ChatConnectionState, type ChatMessage, type ChatMessageChunk, type ConversationDetails, type ConversationStatus, type PublicAgent, type SenderType, type UserTypingPayload, type VisitorConversationSummary, type VisitorTokenResponse, WebChatAuthError, storage };
package/dist/index.js ADDED
@@ -0,0 +1,264 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
8
+
9
+ // src/api-client.ts
10
+ var WebChatAuthError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "WebChatAuthError";
14
+ }
15
+ };
16
+ var AgentUpChatClient = class {
17
+ constructor(config) {
18
+ __publicField(this, "baseUrl");
19
+ __publicField(this, "tenantId");
20
+ __publicField(this, "fetchImpl");
21
+ __publicField(this, "visitorToken");
22
+ if (!config.apiUrl) throw new Error("AgentUpChatClient: apiUrl is required");
23
+ if (!config.visitorToken) throw new Error("AgentUpChatClient: visitorToken is required");
24
+ this.baseUrl = config.apiUrl.replace(/\/$/, "");
25
+ this.visitorToken = config.visitorToken;
26
+ this.tenantId = config.tenantId;
27
+ this.fetchImpl = config.fetch ?? fetch.bind(globalThis);
28
+ }
29
+ /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */
30
+ setVisitorToken(token) {
31
+ if (!token) throw new Error("AgentUpChatClient.setVisitorToken: empty token");
32
+ this.visitorToken = token;
33
+ }
34
+ listAgents(signal) {
35
+ return this.get("/api/webchat/agents", signal);
36
+ }
37
+ listConversations(signal) {
38
+ return this.get("/api/webchat/conversations", signal);
39
+ }
40
+ createConversation(agentCode) {
41
+ return this.post("/api/webchat/conversations", { agentCode });
42
+ }
43
+ getConversation(conversationId, signal) {
44
+ return this.get(`/api/webchat/conversations/${conversationId}`, signal);
45
+ }
46
+ /**
47
+ * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on
48
+ * `/api/webchat/messages/{id}` — that route is POST-only for sending).
49
+ */
50
+ async getMessages(conversationId, signal) {
51
+ const conv = await this.get(`/api/webchat/conversations/${conversationId}`, signal);
52
+ const raw = conv.messages;
53
+ if (!raw?.length) return [];
54
+ return raw.map((m) => ({
55
+ ...m,
56
+ conversationId: m.conversationId || conversationId
57
+ }));
58
+ }
59
+ /**
60
+ * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),
61
+ * not JSON — multipart/form-data is required.
62
+ */
63
+ sendMessage(conversationId, content) {
64
+ const form = new FormData();
65
+ form.append("content", content);
66
+ return this.postForm(`/api/webchat/messages/${conversationId}`, form);
67
+ }
68
+ closeConversation(conversationId) {
69
+ return this.post(`/api/webchat/conversations/${conversationId}/close`, {});
70
+ }
71
+ // ---------------- internals ----------------
72
+ async get(path, signal) {
73
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
74
+ method: "GET",
75
+ headers: this.headers(),
76
+ signal
77
+ });
78
+ return this.unwrap(res);
79
+ }
80
+ async post(path, body) {
81
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
82
+ method: "POST",
83
+ headers: { ...this.headers(), "Content-Type": "application/json" },
84
+ body: JSON.stringify(body ?? {})
85
+ });
86
+ return this.unwrap(res);
87
+ }
88
+ /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */
89
+ async postForm(path, formData) {
90
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
91
+ method: "POST",
92
+ headers: this.headers(),
93
+ body: formData
94
+ });
95
+ return this.unwrap(res);
96
+ }
97
+ headers() {
98
+ const h = {
99
+ Authorization: `Bearer ${this.visitorToken}`,
100
+ Accept: "application/json"
101
+ };
102
+ if (this.tenantId) h["X-Tenant-Id"] = this.tenantId;
103
+ return h;
104
+ }
105
+ async unwrap(res) {
106
+ if (res.status === 401) {
107
+ throw new WebChatAuthError("Visitor token expired or invalid");
108
+ }
109
+ if (!res.ok) {
110
+ let detail = "";
111
+ try {
112
+ const text = await res.text();
113
+ detail = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
114
+ } catch {
115
+ }
116
+ throw new Error(`AgentUp request ${res.url} failed (${res.status}): ${detail}`);
117
+ }
118
+ if (res.status === 204) return void 0;
119
+ const json = await res.json();
120
+ if (json && typeof json === "object" && "success" in json && "result" in json) {
121
+ const env = json;
122
+ if (!env.success) {
123
+ throw new Error(env.message ?? "AgentUp request failed");
124
+ }
125
+ return env.result;
126
+ }
127
+ return json;
128
+ }
129
+ };
130
+
131
+ // src/signalr-client.ts
132
+ import {
133
+ HubConnectionBuilder,
134
+ HubConnectionState,
135
+ LogLevel
136
+ } from "@microsoft/signalr";
137
+ var AgentUpChatHub = class {
138
+ constructor(options, handlers = {}) {
139
+ __publicField(this, "options");
140
+ __publicField(this, "handlers");
141
+ __publicField(this, "connection", null);
142
+ __publicField(this, "state", "idle");
143
+ this.options = options;
144
+ this.handlers = handlers;
145
+ }
146
+ get currentState() {
147
+ return this.state;
148
+ }
149
+ async start() {
150
+ if (this.connection && this.connection.state === HubConnectionState.Connected) {
151
+ return;
152
+ }
153
+ this.setState("connecting");
154
+ const base = this.options.apiUrl.replace(/\/$/, "");
155
+ const url = `${base}/api/chatHub`;
156
+ this.connection = new HubConnectionBuilder().withUrl(url, {
157
+ accessTokenFactory: () => Promise.resolve(this.options.accessTokenFactory())
158
+ }).withAutomaticReconnect().configureLogging(LogLevel.Warning).build();
159
+ this.connection.on("ReceiveMessage", (msg) => this.handlers.onMessage?.(msg));
160
+ this.connection.on(
161
+ "ReceiveMessageChunk",
162
+ (chunk) => this.handlers.onMessageChunk?.(chunk)
163
+ );
164
+ this.connection.on(
165
+ "UserTyping",
166
+ (payload) => this.handlers.onUserTyping?.(payload)
167
+ );
168
+ this.connection.on(
169
+ "UserStoppedTyping",
170
+ (userId) => this.handlers.onUserStoppedTyping?.(userId)
171
+ );
172
+ this.connection.on("Error", (msg) => this.handlers.onError?.(new Error(msg)));
173
+ this.connection.onreconnecting(() => this.setState("reconnecting"));
174
+ this.connection.onreconnected(() => this.setState("connected"));
175
+ this.connection.onclose(() => this.setState("disconnected"));
176
+ await this.connection.start();
177
+ this.setState("connected");
178
+ }
179
+ async stop() {
180
+ if (this.connection) {
181
+ await this.connection.stop();
182
+ this.connection = null;
183
+ }
184
+ this.setState("idle");
185
+ }
186
+ async joinConversation(conversationId) {
187
+ await this.ensureConnected();
188
+ await this.connection.invoke("JoinConversation", conversationId);
189
+ }
190
+ async leaveConversation(conversationId) {
191
+ if (this.connection?.state !== HubConnectionState.Connected) return;
192
+ await this.connection.invoke("LeaveConversation", conversationId);
193
+ }
194
+ async sendMessage(conversationId, content) {
195
+ await this.ensureConnected();
196
+ await this.connection.invoke("SendMessage", conversationId, content);
197
+ }
198
+ async sendTyping(conversationId, isTyping) {
199
+ if (this.connection?.state !== HubConnectionState.Connected) return;
200
+ await this.connection.invoke("Typing", conversationId, isTyping);
201
+ }
202
+ async ensureConnected() {
203
+ if (!this.connection) {
204
+ await this.start();
205
+ return;
206
+ }
207
+ if (this.connection.state !== HubConnectionState.Connected) {
208
+ await this.connection.start();
209
+ this.setState("connected");
210
+ }
211
+ }
212
+ setState(next) {
213
+ if (this.state === next) return;
214
+ this.state = next;
215
+ this.handlers.onConnectionStateChanged?.(next);
216
+ }
217
+ };
218
+
219
+ // src/storage.ts
220
+ var storage_exports = {};
221
+ __export(storage_exports, {
222
+ clearToken: () => clearToken,
223
+ getItem: () => getItem,
224
+ loadToken: () => loadToken,
225
+ removeItem: () => removeItem,
226
+ saveToken: () => saveToken,
227
+ setItem: () => setItem
228
+ });
229
+ var PREFIX = "agentup.webchat:";
230
+ function getItem(key) {
231
+ try {
232
+ return localStorage.getItem(PREFIX + key);
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ function setItem(key, value) {
238
+ try {
239
+ localStorage.setItem(PREFIX + key, value);
240
+ } catch {
241
+ }
242
+ }
243
+ function removeItem(key) {
244
+ try {
245
+ localStorage.removeItem(PREFIX + key);
246
+ } catch {
247
+ }
248
+ }
249
+ function saveToken(token) {
250
+ setItem("visitor_token", token);
251
+ }
252
+ function loadToken() {
253
+ return getItem("visitor_token");
254
+ }
255
+ function clearToken() {
256
+ removeItem("visitor_token");
257
+ }
258
+ export {
259
+ AgentUpChatClient,
260
+ AgentUpChatHub,
261
+ WebChatAuthError,
262
+ storage_exports as storage
263
+ };
264
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api-client.ts","../src/signalr-client.ts","../src/storage.ts"],"sourcesContent":["import type {\r\n AgentUpChatClientConfig,\r\n ChatMessage,\r\n ConversationDetails,\r\n PublicAgent,\r\n VisitorConversationSummary\r\n} from './types.js';\r\n\r\n/** Thrown when the visitor token is expired/invalid; UIs should surface a re-auth flow. */\r\nexport class WebChatAuthError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = 'WebChatAuthError';\r\n }\r\n}\r\n\r\n/**\r\n * Minimal REST client for the embedded web chat app endpoints. Wraps fetch with\r\n * the visitor JWT and unwraps the AgentUp `ApiResponse<T>` envelope automatically.\r\n *\r\n * Endpoints used:\r\n * - `GET /api/webchat/agents`\r\n * - `GET /api/webchat/conversations`\r\n * - `POST /api/webchat/conversations`\r\n * - `GET /api/webchat/conversations/{id}` (includes `messages` history)\r\n * - `POST /api/webchat/messages/{conversationId}` (send)\r\n * - `POST /api/webchat/conversations/{id}/close`\r\n */\r\nexport class AgentUpChatClient {\r\n private readonly baseUrl: string;\r\n private readonly tenantId?: string;\r\n private readonly fetchImpl: typeof fetch;\r\n private visitorToken: string;\r\n\r\n constructor(config: AgentUpChatClientConfig) {\r\n if (!config.apiUrl) throw new Error('AgentUpChatClient: apiUrl is required');\r\n if (!config.visitorToken) throw new Error('AgentUpChatClient: visitorToken is required');\r\n this.baseUrl = config.apiUrl.replace(/\\/$/, '');\r\n this.visitorToken = config.visitorToken;\r\n this.tenantId = config.tenantId;\r\n this.fetchImpl = config.fetch ?? fetch.bind(globalThis);\r\n }\r\n\r\n /** Refresh the visitor token in place (e.g. after a silent re-auth round-trip). */\r\n setVisitorToken(token: string): void {\r\n if (!token) throw new Error('AgentUpChatClient.setVisitorToken: empty token');\r\n this.visitorToken = token;\r\n }\r\n\r\n listAgents(signal?: AbortSignal): Promise<PublicAgent[]> {\r\n return this.get<PublicAgent[]>('/api/webchat/agents', signal);\r\n }\r\n\r\n listConversations(signal?: AbortSignal): Promise<VisitorConversationSummary[]> {\r\n return this.get<VisitorConversationSummary[]>('/api/webchat/conversations', signal);\r\n }\r\n\r\n createConversation(agentCode: string): Promise<VisitorConversationSummary> {\r\n return this.post<VisitorConversationSummary>('/api/webchat/conversations', { agentCode });\r\n }\r\n\r\n getConversation(conversationId: string, signal?: AbortSignal): Promise<ConversationDetails> {\r\n return this.get<ConversationDetails>(`/api/webchat/conversations/${conversationId}`, signal);\r\n }\r\n\r\n /**\r\n * Message history is returned on `GET /api/webchat/conversations/{id}` (there is no GET on\r\n * `/api/webchat/messages/{id}` — that route is POST-only for sending).\r\n */\r\n async getMessages(conversationId: string, signal?: AbortSignal): Promise<ChatMessage[]> {\r\n const conv = await this.get<ConversationDetails>(`/api/webchat/conversations/${conversationId}`, signal);\r\n const raw = conv.messages;\r\n if (!raw?.length) return [];\r\n return raw.map((m) => ({\r\n ...m,\r\n conversationId: m.conversationId || conversationId\r\n }));\r\n }\r\n\r\n /**\r\n * REST fallback for sending. The API binds `[FromForm] content` (see `WebChatController`),\r\n * not JSON — multipart/form-data is required.\r\n */\r\n sendMessage(conversationId: string, content: string): Promise<ChatMessage> {\r\n const form = new FormData();\r\n form.append('content', content);\r\n return this.postForm<ChatMessage>(`/api/webchat/messages/${conversationId}`, form);\r\n }\r\n\r\n closeConversation(conversationId: string): Promise<void> {\r\n return this.post<void>(`/api/webchat/conversations/${conversationId}/close`, {});\r\n }\r\n\r\n // ---------------- internals ----------------\r\n\r\n private async get<T>(path: string, signal?: AbortSignal): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'GET',\r\n headers: this.headers(),\r\n signal\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n private async post<T>(path: string, body: unknown): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'POST',\r\n headers: { ...this.headers(), 'Content-Type': 'application/json' },\r\n body: JSON.stringify(body ?? {})\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n /** POST multipart/form-data (do not set Content-Type — boundary is set by the runtime). */\r\n private async postForm<T>(path: string, formData: FormData): Promise<T> {\r\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\r\n method: 'POST',\r\n headers: this.headers(),\r\n body: formData\r\n });\r\n return this.unwrap<T>(res);\r\n }\r\n\r\n private headers(): Record<string, string> {\r\n const h: Record<string, string> = {\r\n Authorization: `Bearer ${this.visitorToken}`,\r\n Accept: 'application/json'\r\n };\r\n if (this.tenantId) h['X-Tenant-Id'] = this.tenantId;\r\n return h;\r\n }\r\n\r\n private async unwrap<T>(res: Response): Promise<T> {\r\n if (res.status === 401) {\r\n throw new WebChatAuthError('Visitor token expired or invalid');\r\n }\r\n if (!res.ok) {\r\n let detail = '';\r\n try {\r\n const text = await res.text();\r\n detail = text.length > 200 ? text.slice(0, 200) + '…' : text;\r\n } catch {\r\n // ignore body parse errors\r\n }\r\n throw new Error(`AgentUp request ${res.url} failed (${res.status}): ${detail}`);\r\n }\r\n if (res.status === 204) return undefined as T;\r\n\r\n const json = (await res.json()) as unknown;\r\n // Backend wraps responses as { success, result } except for a few skipped paths.\r\n if (\r\n json &&\r\n typeof json === 'object' &&\r\n 'success' in (json as Record<string, unknown>) &&\r\n 'result' in (json as Record<string, unknown>)\r\n ) {\r\n const env = json as { success: boolean; result: T; message?: string };\r\n if (!env.success) {\r\n throw new Error(env.message ?? 'AgentUp request failed');\r\n }\r\n return env.result;\r\n }\r\n return json as T;\r\n }\r\n}\r\n","import {\r\n HubConnection,\r\n HubConnectionBuilder,\r\n HubConnectionState,\r\n LogLevel\r\n} from '@microsoft/signalr';\r\n\r\nimport type { ChatMessage, ChatMessageChunk, UserTypingPayload } from './types.js';\r\n\r\n/** Connection lifecycle states surfaced to UI layers. */\r\nexport type ChatConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';\r\n\r\nexport interface AgentUpChatHubOptions {\r\n /** Base URL of the AgentUp API. */\r\n apiUrl: string;\r\n /** Provides the latest visitor JWT (called on initial connect AND on reconnect). */\r\n accessTokenFactory: () => string | Promise<string>;\r\n}\r\n\r\nexport interface AgentUpChatHubHandlers {\r\n onMessage?: (message: ChatMessage) => void;\r\n /** AI/agent streaming tokens before the final `ReceiveMessage`. */\r\n onMessageChunk?: (chunk: ChatMessageChunk) => void;\r\n /** Hub `UserTyping` — includes AI (`userId` `\"ai\"`) and human agents. */\r\n onUserTyping?: (payload: UserTypingPayload) => void;\r\n /** Hub `UserStoppedTyping` — argument is `userId` (e.g. `\"ai\"`). */\r\n onUserStoppedTyping?: (userId: string) => void;\r\n onConnectionStateChanged?: (state: ChatConnectionState) => void;\r\n onError?: (error: unknown) => void;\r\n}\r\n\r\n/**\r\n * Thin SignalR client for the AgentUp ChatHub. Intentionally narrow surface — the\r\n * UI layer owns active-conversation tracking and group join/leave logic.\r\n *\r\n * Server methods used:\r\n * - `JoinConversation(conversationId)` / `LeaveConversation(conversationId)`\r\n * - `SendMessage(conversationId, content)`\r\n * - `Typing(conversationId, isTyping)`\r\n *\r\n * Server callbacks consumed:\r\n * - `ReceiveMessage`, `ReceiveMessageChunk` (streaming), `UserTyping`, `UserStoppedTyping`, `Error`.\r\n */\r\nexport class AgentUpChatHub {\r\n private readonly options: AgentUpChatHubOptions;\r\n private readonly handlers: AgentUpChatHubHandlers;\r\n private connection: HubConnection | null = null;\r\n private state: ChatConnectionState = 'idle';\r\n\r\n constructor(options: AgentUpChatHubOptions, handlers: AgentUpChatHubHandlers = {}) {\r\n this.options = options;\r\n this.handlers = handlers;\r\n }\r\n\r\n get currentState(): ChatConnectionState {\r\n return this.state;\r\n }\r\n\r\n async start(): Promise<void> {\r\n if (this.connection && this.connection.state === HubConnectionState.Connected) {\r\n return;\r\n }\r\n this.setState('connecting');\r\n\r\n const base = this.options.apiUrl.replace(/\\/$/, '');\r\n /** Must match ASP.NET `MapHub<ChatHub>(\"/api/chatHub\")` (same path as legacy widget). */\r\n const url = `${base}/api/chatHub`;\r\n this.connection = new HubConnectionBuilder()\r\n .withUrl(url, {\r\n accessTokenFactory: () => Promise.resolve(this.options.accessTokenFactory())\r\n })\r\n .withAutomaticReconnect()\r\n .configureLogging(LogLevel.Warning)\r\n .build();\r\n\r\n this.connection.on('ReceiveMessage', (msg: ChatMessage) => this.handlers.onMessage?.(msg));\r\n this.connection.on('ReceiveMessageChunk', (chunk: ChatMessageChunk) =>\r\n this.handlers.onMessageChunk?.(chunk)\r\n );\r\n this.connection.on('UserTyping', (payload: UserTypingPayload) =>\r\n this.handlers.onUserTyping?.(payload)\r\n );\r\n this.connection.on('UserStoppedTyping', (userId: string) =>\r\n this.handlers.onUserStoppedTyping?.(userId)\r\n );\r\n this.connection.on('Error', (msg: string) => this.handlers.onError?.(new Error(msg)));\r\n\r\n this.connection.onreconnecting(() => this.setState('reconnecting'));\r\n this.connection.onreconnected(() => this.setState('connected'));\r\n this.connection.onclose(() => this.setState('disconnected'));\r\n\r\n await this.connection.start();\r\n this.setState('connected');\r\n }\r\n\r\n async stop(): Promise<void> {\r\n if (this.connection) {\r\n await this.connection.stop();\r\n this.connection = null;\r\n }\r\n this.setState('idle');\r\n }\r\n\r\n async joinConversation(conversationId: string): Promise<void> {\r\n await this.ensureConnected();\r\n await this.connection!.invoke('JoinConversation', conversationId);\r\n }\r\n\r\n async leaveConversation(conversationId: string): Promise<void> {\r\n if (this.connection?.state !== HubConnectionState.Connected) return;\r\n await this.connection.invoke('LeaveConversation', conversationId);\r\n }\r\n\r\n async sendMessage(conversationId: string, content: string): Promise<void> {\r\n await this.ensureConnected();\r\n await this.connection!.invoke('SendMessage', conversationId, content);\r\n }\r\n\r\n async sendTyping(conversationId: string, isTyping: boolean): Promise<void> {\r\n if (this.connection?.state !== HubConnectionState.Connected) return;\r\n await this.connection.invoke('Typing', conversationId, isTyping);\r\n }\r\n\r\n private async ensureConnected(): Promise<void> {\r\n if (!this.connection) {\r\n await this.start();\r\n return;\r\n }\r\n if (this.connection.state !== HubConnectionState.Connected) {\r\n await this.connection.start();\r\n this.setState('connected');\r\n }\r\n }\r\n\r\n private setState(next: ChatConnectionState): void {\r\n if (this.state === next) return;\r\n this.state = next;\r\n this.handlers.onConnectionStateChanged?.(next);\r\n }\r\n}\r\n","/**\r\n * Local storage helper for the embedded chat app. Keeps ephemeral UI state\r\n * (selected conversation id, sidebar collapsed, etc.) outside React-style\r\n * components so the choice of UI framework stays flexible.\r\n *\r\n * Storage is **not** used for the visitor token by default — tokens are short\r\n * lived and should be re-minted from the host backend on demand. Hosts can\r\n * still call {@link saveToken} if they want browser refresh persistence.\r\n */\r\nconst PREFIX = 'agentup.webchat:';\r\n\r\nexport function getItem(key: string): string | null {\r\n try {\r\n return localStorage.getItem(PREFIX + key);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function setItem(key: string, value: string): void {\r\n try {\r\n localStorage.setItem(PREFIX + key, value);\r\n } catch {\r\n // Storage may be disabled (private mode); silently degrade.\r\n }\r\n}\r\n\r\nexport function removeItem(key: string): void {\r\n try {\r\n localStorage.removeItem(PREFIX + key);\r\n } catch {\r\n // ignore\r\n }\r\n}\r\n\r\n/** Convenience wrapper for the visitor token (host-controlled persistence). */\r\nexport function saveToken(token: string): void {\r\n setItem('visitor_token', token);\r\n}\r\n\r\nexport function loadToken(): string | null {\r\n return getItem('visitor_token');\r\n}\r\n\r\nexport function clearToken(): void {\r\n removeItem('visitor_token');\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@agentup-inksen/web-chat-core",
3
+ "version": "0.0.1",
4
+ "description": "Shared SDK for AgentUp web chat clients (REST + SignalR + types)",
5
+ "license": "PROPRIETARY",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap",
23
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --sourcemap",
24
+ "clean": "rimraf dist",
25
+ "lint": "tsc --noEmit"
26
+ },
27
+ "dependencies": {
28
+ "@microsoft/signalr": "^8.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "rimraf": "^5.0.5",
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "restricted"
37
+ }
38
+ }