@cstar.help/js 0.10.3 → 0.12.0

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 CHANGED
@@ -30,7 +30,7 @@ cStar has one package with **six entry points**, each designed for a different c
30
30
 
31
31
  ### The Security Model
32
32
 
33
- cStar uses **two key types** to control access — the same pattern as Stripe and Supabase:
33
+ cStar uses **two key types** to control access — the same pattern as Stripe:
34
34
 
35
35
  ```
36
36
  Secret keys (sk_live_*, sk_test_*)
@@ -200,32 +200,84 @@ const { data: searchResults } = await cstar.search.query({
200
200
 
201
201
  Build custom chat experiences in the browser. Uses a publishable key — safe for frontend code.
202
202
 
203
+ The chat client supports two authentication modes:
204
+
205
+ - **HMAC** — your server signs the customer payload with the team's signing secret. Best for sites where users are already authenticated server-side.
206
+ - **Session** — the customer signs in directly with email/password (or magic link). Best for standalone widgets and customer portals where there's no host-app session to bridge.
207
+
208
+ You don't have to pick the mode up-front. The first response from any cstar endpoint includes an `X-Cstar-Auth-Mode` header, which the SDK caches; `chat.getAuthMode()` returns `'hmac'`, `'session'`, or `null` after the first call.
209
+
210
+ ### HMAC
211
+
203
212
  ```typescript
204
213
  import { ChatClient } from '@cstar.help/js/chat';
205
214
 
206
215
  const chat = new ChatClient({ teamSlug: 'acme' });
207
216
 
208
- // Identify the customer (HMAC signature from your server)
209
217
  await chat.identify(
210
218
  { externalId: 'usr_123', email: 'jane@example.com', name: 'Jane', timestamp: Date.now() / 1000 },
211
219
  hmacSignature
212
220
  );
221
+ ```
222
+
223
+ ### Session (email/password)
224
+
225
+ ```typescript
226
+ const chat = new ChatClient({ teamSlug: 'acme' });
227
+
228
+ // Login with credentials — returns the customer + tokens
229
+ const { customer, accessToken, refreshToken } = await chat.loginWithCredentials(
230
+ 'jane@example.com',
231
+ 'hunter2'
232
+ );
233
+
234
+ // Or rehydrate from previously stored tokens
235
+ await chat.loginWithSession(accessToken, refreshToken);
236
+ ```
213
237
 
214
- // List conversations
215
- const { conversations } = await chat.conversations.list();
238
+ ### Once authenticated (either mode)
216
239
 
217
- // Send a message
240
+ ```typescript
241
+ const { tickets } = await chat.tickets.list();
218
242
  await chat.messages.send('tkt_abc123', 'Thanks for the help!');
219
243
 
220
- // Subscribe to new messages
221
244
  const unsubscribe = chat.onMessage('tkt_abc123', (message) => {
222
245
  console.log(`${message.sender_name}: ${message.content}`);
223
246
  });
224
247
 
225
- // Clean up
226
248
  chat.disconnect();
227
249
  ```
228
250
 
251
+ ### Typed errors
252
+
253
+ Every `ChatClient` request throws a typed subclass of `ChatClientError` on failure. Each carries a `requestId` (from the `X-Request-Id` response header) so customer support can correlate against server logs.
254
+
255
+ ```typescript
256
+ import {
257
+ ChatClientError,
258
+ ChatClientAuthError,
259
+ ChatClientRateLimitError,
260
+ ChatClientServerError,
261
+ ChatClientNetworkError
262
+ } from '@cstar.help/js/chat';
263
+
264
+ try {
265
+ await chat.identify(customer, signature);
266
+ } catch (err) {
267
+ if (err instanceof ChatClientAuthError) {
268
+ // 401 / 403 — bad signature, expired token, wrong team
269
+ } else if (err instanceof ChatClientRateLimitError) {
270
+ // 429 — back off for err.retryAfter seconds
271
+ } else if (err instanceof ChatClientServerError) {
272
+ // 5xx — usually transient, retry with backoff
273
+ } else if (err instanceof ChatClientNetworkError) {
274
+ // fetch() itself failed — DNS, offline, CORS — see err.originalError
275
+ } else if (err instanceof ChatClientError) {
276
+ console.log('Error', err.code, err.statusCode, 'requestId:', err.requestId);
277
+ }
278
+ }
279
+ ```
280
+
229
281
  ---
230
282
 
231
283
  ## Client-Side: Public Knowledge Base
@@ -30,7 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/chat/index.ts
31
31
  var chat_exports = {};
32
32
  __export(chat_exports, {
33
- ChatClient: () => ChatClient
33
+ ChatClient: () => ChatClient,
34
+ ChatClientAuthError: () => ChatClientAuthError,
35
+ ChatClientError: () => ChatClientError,
36
+ ChatClientNetworkError: () => ChatClientNetworkError,
37
+ ChatClientRateLimitError: () => ChatClientRateLimitError,
38
+ ChatClientServerError: () => ChatClientServerError
34
39
  });
35
40
  module.exports = __toCommonJS(chat_exports);
36
41
 
@@ -45,8 +50,8 @@ var RealtimeManager = class {
45
50
  this.options = options;
46
51
  }
47
52
  /**
48
- * Initialize the Supabase client and authenticate with a realtime token.
49
- * Returns true if successful, false if @supabase/supabase-js is unavailable.
53
+ * Initialize the realtime client and authenticate with a token.
54
+ * Returns true if successful, false if the realtime runtime is unavailable.
50
55
  */
51
56
  init() {
52
57
  if (this.initPromise) return this.initPromise;
@@ -55,15 +60,15 @@ var RealtimeManager = class {
55
60
  }
56
61
  async doInit() {
57
62
  try {
63
+ const tokenData = await this.options.fetchToken();
58
64
  const { createClient } = await import(
59
65
  /* webpackIgnore: true */
60
66
  "@supabase/supabase-js"
61
67
  );
62
- this.client = createClient(this.options.supabaseUrl, this.options.supabaseAnonKey, {
68
+ this.client = createClient(tokenData.realtimeUrl, tokenData.realtimeKey, {
63
69
  auth: { persistSession: false, autoRefreshToken: false },
64
70
  realtime: { params: { eventsPerSecond: 10 } }
65
71
  });
66
- const tokenData = await this.options.fetchToken();
67
72
  this.setToken(tokenData);
68
73
  return true;
69
74
  } catch {
@@ -121,7 +126,8 @@ var RealtimeManager = class {
121
126
  sender_name: row.sender_name ?? "",
122
127
  created_at: row.created_at,
123
128
  read_at: row.read_at ?? null,
124
- attachments: row.attachments ?? []
129
+ attachments: row.attachments ?? [],
130
+ payload: row.payload ?? null
125
131
  };
126
132
  callback(message);
127
133
  }
@@ -129,10 +135,18 @@ var RealtimeManager = class {
129
135
  this.channels.set(channelName, channel);
130
136
  return () => this.removeChannel(channelName);
131
137
  }
132
- /** Get the underlying Supabase client (for sharing with PresenceManager). */
138
+ /** Get the underlying realtime client (for sharing with PresenceManager). */
133
139
  getClient() {
134
140
  return this.client;
135
141
  }
142
+ /**
143
+ * The current Supabase JWT (set by init/refresh). Returned so callers can
144
+ * decode the `sub` claim to learn the customerId — required after a
145
+ * session-mode login where the SDK doesn't have customerId at hand.
146
+ */
147
+ getCurrentToken() {
148
+ return this.currentToken;
149
+ }
136
150
  /** Whether the realtime manager is initialized and has a token. */
137
151
  get isReady() {
138
152
  return this.client !== null && this.currentToken !== null;
@@ -171,7 +185,7 @@ var PresenceManager = class {
171
185
  this.typingClearTimers = /* @__PURE__ */ new Map();
172
186
  this.options = options;
173
187
  }
174
- /** Set the Supabase client (shared from RealtimeManager). */
188
+ /** Set the realtime client (shared from RealtimeManager). */
175
189
  setClient(client) {
176
190
  this.client = client;
177
191
  }
@@ -274,6 +288,43 @@ var PresenceManager = class {
274
288
  }
275
289
  };
276
290
 
291
+ // src/chat/errors.ts
292
+ var ChatClientError = class extends Error {
293
+ constructor(message, opts) {
294
+ super(message);
295
+ this.name = "ChatClientError";
296
+ this.code = opts.code ?? "UNKNOWN";
297
+ this.statusCode = opts.statusCode;
298
+ this.requestId = opts.requestId ?? null;
299
+ }
300
+ };
301
+ var ChatClientAuthError = class extends ChatClientError {
302
+ constructor(opts) {
303
+ super(opts.message, opts);
304
+ this.name = "ChatClientAuthError";
305
+ }
306
+ };
307
+ var ChatClientRateLimitError = class extends ChatClientError {
308
+ constructor(opts) {
309
+ super(opts.message, opts);
310
+ this.name = "ChatClientRateLimitError";
311
+ this.retryAfter = opts.retryAfter ?? null;
312
+ }
313
+ };
314
+ var ChatClientServerError = class extends ChatClientError {
315
+ constructor(opts) {
316
+ super(opts.message, opts);
317
+ this.name = "ChatClientServerError";
318
+ }
319
+ };
320
+ var ChatClientNetworkError = class extends ChatClientError {
321
+ constructor(opts) {
322
+ super(opts.message, { code: "NETWORK_ERROR", statusCode: 0, requestId: null });
323
+ this.name = "ChatClientNetworkError";
324
+ this.originalError = opts.originalError;
325
+ }
326
+ };
327
+
277
328
  // src/chat/index.ts
278
329
  var DEFAULT_BASE_URL = "https://www.cstar.help";
279
330
  var DEFAULT_POLLING_INTERVAL = 3e3;
@@ -282,6 +333,7 @@ var ChatClient = class {
282
333
  this.sessionToken = null;
283
334
  this.teamId = null;
284
335
  this.customerId = null;
336
+ this.authMode = null;
285
337
  this.pollingTimers = /* @__PURE__ */ new Map();
286
338
  this.realtimeManager = null;
287
339
  this.presenceManager = null;
@@ -290,18 +342,17 @@ var ChatClient = class {
290
342
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
291
343
  this.teamSlug = config.teamSlug;
292
344
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
293
- this.supabaseUrl = config.supabaseUrl;
294
- this.supabaseAnonKey = config.supabaseAnonKey;
295
- this.conversations = new ConversationsApi(this);
345
+ this.realtimeEnabled = config.realtime !== false;
346
+ this.tickets = new TicketsApi(this);
296
347
  this.messages = new MessagesApi(this);
297
348
  this.community = new ChatCommunityApi(this);
298
349
  }
299
350
  /**
300
351
  * Identify a customer using HMAC signature verification.
301
- * Must be called before accessing conversations or messages.
352
+ * Must be called before accessing tickets or messages.
302
353
  *
303
- * If `supabaseUrl` and `supabaseAnonKey` were provided in the config,
304
- * this will also initialize real-time subscriptions (non-blocking).
354
+ * Real-time subscriptions are initialized automatically (non-blocking)
355
+ * unless `realtime: false` was passed in config.
305
356
  *
306
357
  * @param customer - Customer identity fields
307
358
  * @param signature - HMAC-SHA256 hex signature of the signed fields
@@ -323,7 +374,7 @@ var ChatClient = class {
323
374
  this.sessionToken = response.data.sessionToken;
324
375
  this.teamId = response.data.teamId;
325
376
  this.customerId = response.data.customer.id;
326
- if (this.supabaseUrl && this.supabaseAnonKey) {
377
+ if (this.realtimeEnabled) {
327
378
  this.realtimeInitPromise = this.initRealtime(
328
379
  response.data.customer.id,
329
380
  response.data.customer.name || customer.name || customer.email
@@ -332,8 +383,72 @@ var ChatClient = class {
332
383
  return response.data;
333
384
  }
334
385
  /**
335
- * Subscribe to new messages on a conversation.
336
- * Uses Supabase Realtime (Postgres Changes) when available, falls back to polling.
386
+ * Sign in with cStar's hosted password auth. The SDK keeps the resulting
387
+ * Supabase access token and uses it as Bearer for every subsequent call.
388
+ * Real-time bootstraps automatically (same flow as `identify()`).
389
+ *
390
+ * Use this when your customers don't have a session in your app and you
391
+ * want cStar to handle login. For the case where your app already has a
392
+ * Supabase session, use `loginWithSession({ accessToken })` instead.
393
+ */
394
+ async loginWithCredentials(params) {
395
+ const teamId = await this.resolveTeamIdFromSlug();
396
+ const response = await this.fetch("/api/widget/auth/login", {
397
+ method: "POST",
398
+ body: { email: params.email, password: params.password, teamId }
399
+ });
400
+ if (!response.accessToken) {
401
+ throw new ChatClientAuthError({
402
+ message: "Login succeeded but no session was issued",
403
+ code: "NO_SESSION",
404
+ statusCode: 200,
405
+ requestId: null
406
+ });
407
+ }
408
+ await this.loginWithSession({
409
+ accessToken: response.accessToken,
410
+ teamId,
411
+ customerName: response.customer.name ?? response.customer.email
412
+ });
413
+ return {
414
+ customer: {
415
+ id: this.customerId ?? response.customer.id,
416
+ email: response.customer.email,
417
+ name: response.customer.name
418
+ },
419
+ teamId
420
+ };
421
+ }
422
+ /**
423
+ * Use an existing Supabase access token for widget calls. Skip the cStar
424
+ * login flow entirely; the SDK uses the supplied token as Bearer and
425
+ * bootstraps real-time the same way `identify()` does.
426
+ *
427
+ * The widget API verifies the token via the `widget-session` strategy on
428
+ * every request — there is no client-side validation here.
429
+ */
430
+ async loginWithSession(params) {
431
+ this.sessionToken = params.accessToken;
432
+ if (params.teamId) {
433
+ this.teamId = params.teamId;
434
+ }
435
+ this.authMode = "session";
436
+ if (this.realtimeEnabled) {
437
+ const customerName = params.customerName ?? "";
438
+ this.realtimeInitPromise = this.initRealtimeFromSession(customerName);
439
+ }
440
+ }
441
+ /**
442
+ * Returns the auth mode the client is currently using, or `null` before
443
+ * any authenticated call has resolved one. Useful for branching UI
444
+ * (e.g. to show a "Sign out" button only in session mode).
445
+ */
446
+ getAuthMode() {
447
+ return this.authMode;
448
+ }
449
+ /**
450
+ * Subscribe to new messages on a ticket.
451
+ * Uses real-time delivery when available, falls back to polling.
337
452
  *
338
453
  * @returns Unsubscribe function
339
454
  */
@@ -345,7 +460,7 @@ var ChatClient = class {
345
460
  }
346
461
  /**
347
462
  * Subscribe to typing indicator events.
348
- * Requires Supabase Realtime — returns a no-op if unavailable.
463
+ * Requires real-time — returns a no-op when unavailable.
349
464
  *
350
465
  * @returns Unsubscribe function
351
466
  */
@@ -357,8 +472,8 @@ var ChatClient = class {
357
472
  };
358
473
  }
359
474
  /**
360
- * Send a customer typing indicator (requires realtime).
361
- * No-op if realtime is not available.
475
+ * Send a customer typing indicator (requires real-time).
476
+ * No-op when real-time is unavailable.
362
477
  */
363
478
  async sendTyping(ticketId, isTyping) {
364
479
  if (this.presenceManager) {
@@ -384,8 +499,20 @@ var ChatClient = class {
384
499
  this.sessionToken = null;
385
500
  this.teamId = null;
386
501
  this.customerId = null;
502
+ this.authMode = null;
387
503
  }
388
- /** @internal Make an authenticated request to the widget API. */
504
+ /**
505
+ * @internal Make an authenticated request to the widget API.
506
+ *
507
+ * On any non-2xx response, throws a typed subclass of `ChatClientError`
508
+ * (`ChatClientAuthError`, `ChatClientRateLimitError`, etc.) so consumers
509
+ * can branch on error class instead of parsing message strings.
510
+ *
511
+ * On the way out, reads the `X-Cstar-Auth-Mode` response header (set by
512
+ * widgetHandler on every response) and caches it on the client. After
513
+ * the first authenticated round-trip, `getAuthMode()` returns the
514
+ * server-confirmed mode.
515
+ */
389
516
  async fetch(path, options) {
390
517
  const url = this.buildUrl(path, options.query);
391
518
  const headers = {
@@ -394,17 +521,104 @@ var ChatClient = class {
394
521
  if (this.sessionToken) {
395
522
  headers["Authorization"] = `Bearer ${this.sessionToken}`;
396
523
  }
397
- const response = await globalThis.fetch(url, {
398
- method: options.method,
399
- headers,
400
- body: options.body ? JSON.stringify(options.body) : void 0
401
- });
524
+ let response;
525
+ try {
526
+ response = await globalThis.fetch(url, {
527
+ method: options.method,
528
+ headers,
529
+ body: options.body ? JSON.stringify(options.body) : void 0
530
+ });
531
+ } catch (networkErr) {
532
+ throw new ChatClientNetworkError({
533
+ message: networkErr instanceof Error ? networkErr.message : "Network request failed",
534
+ originalError: networkErr
535
+ });
536
+ }
537
+ const responseHeaders = response.headers && typeof response.headers.get === "function" ? response.headers : null;
538
+ const headerMode = responseHeaders?.get("X-Cstar-Auth-Mode") ?? null;
539
+ if (headerMode === "hmac" || headerMode === "session") {
540
+ this.authMode = headerMode;
541
+ }
542
+ const requestId = responseHeaders?.get("X-Request-Id") ?? null;
402
543
  if (!response.ok) {
403
- const errorBody = await response.json().catch(() => ({ error: { message: "Request failed" } }));
404
- throw new Error(errorBody.error?.message ?? `Widget API error: ${response.status}`);
544
+ const errorBody = await response.json().catch(() => ({ error: { code: "UNKNOWN", message: "Request failed" } }));
545
+ const code = errorBody.error?.code ?? "UNKNOWN";
546
+ const message = errorBody.error?.message ?? `Widget API error: ${response.status}`;
547
+ if (response.status === 401 || response.status === 403) {
548
+ throw new ChatClientAuthError({
549
+ message,
550
+ code,
551
+ statusCode: response.status,
552
+ requestId
553
+ });
554
+ }
555
+ if (response.status === 429) {
556
+ const retryAfterHeader = responseHeaders?.get("Retry-After") ?? null;
557
+ const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : null;
558
+ throw new ChatClientRateLimitError({
559
+ message,
560
+ code,
561
+ statusCode: response.status,
562
+ requestId,
563
+ retryAfter: Number.isFinite(retryAfter ?? NaN) ? retryAfter : null
564
+ });
565
+ }
566
+ if (response.status >= 500) {
567
+ throw new ChatClientServerError({
568
+ message,
569
+ code,
570
+ statusCode: response.status,
571
+ requestId
572
+ });
573
+ }
574
+ throw new ChatClientError(message, {
575
+ code,
576
+ statusCode: response.status,
577
+ requestId
578
+ });
405
579
  }
406
580
  return response.json();
407
581
  }
582
+ /**
583
+ * Look up the team UUID from the configured `teamSlug`. Used by
584
+ * `loginWithCredentials`. HMAC integrators (`identify()`) never hit
585
+ * this path — verify-identity accepts the slug directly.
586
+ *
587
+ * @internal
588
+ */
589
+ async resolveTeamIdFromSlug() {
590
+ const response = await this.fetch(
591
+ "/api/widget/team-by-slug",
592
+ {
593
+ method: "GET",
594
+ query: { teamSlug: this.teamSlug }
595
+ }
596
+ );
597
+ return response.data.teamId;
598
+ }
599
+ /** @internal Bootstrap realtime from a session login (no precomputed customerId). */
600
+ async initRealtimeFromSession(customerName) {
601
+ try {
602
+ this.realtimeManager = new RealtimeManager({
603
+ fetchToken: () => this.fetchRealtimeToken()
604
+ });
605
+ const ready = await this.realtimeManager.init();
606
+ if (!ready) return;
607
+ this.realtimeReady = true;
608
+ const token = this.realtimeManager.getCurrentToken();
609
+ const customerId = token ? decodeJwtSub(token) : null;
610
+ if (customerId) {
611
+ this.customerId = customerId;
612
+ this.presenceManager = new PresenceManager({ customerId, customerName });
613
+ const client = this.realtimeManager.getClient();
614
+ if (client) {
615
+ this.presenceManager.setClient(client);
616
+ }
617
+ }
618
+ } catch {
619
+ this.realtimeReady = false;
620
+ }
621
+ }
408
622
  /** @internal Get the stored team ID for requests that need it. */
409
623
  getTeamId() {
410
624
  return this.teamId;
@@ -422,8 +636,6 @@ var ChatClient = class {
422
636
  async initRealtime(customerId, customerName) {
423
637
  try {
424
638
  this.realtimeManager = new RealtimeManager({
425
- supabaseUrl: this.supabaseUrl,
426
- supabaseAnonKey: this.supabaseAnonKey,
427
639
  fetchToken: () => this.fetchRealtimeToken()
428
640
  });
429
641
  const ready = await this.realtimeManager.init();
@@ -490,28 +702,29 @@ var ChatClient = class {
490
702
  return qs ? `${base}?${qs}` : base;
491
703
  }
492
704
  };
493
- var ConversationsApi = class {
705
+ var TicketsApi = class {
494
706
  constructor(chat) {
495
707
  this.chat = chat;
496
708
  }
497
- /** List conversations for the identified customer. */
709
+ /** List tickets for the identified customer. */
498
710
  async list(params) {
499
711
  const teamId = this.chat.getTeamId();
500
712
  if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
501
- const response = await this.chat.fetch(
502
- "/api/widget/conversations",
503
- {
504
- method: "GET",
505
- query: {
506
- teamId,
507
- limit: params?.limit?.toString(),
508
- cursor: params?.cursor
509
- }
713
+ const response = await this.chat.fetch("/api/widget/conversations", {
714
+ method: "GET",
715
+ query: {
716
+ teamId,
717
+ limit: params?.limit?.toString(),
718
+ cursor: params?.cursor
510
719
  }
511
- );
512
- return response.data;
720
+ });
721
+ return {
722
+ tickets: response.data.tickets ?? response.data.conversations ?? [],
723
+ cursor: response.data.cursor,
724
+ hasMore: response.data.hasMore
725
+ };
513
726
  }
514
- /** Create a new conversation (ticket). */
727
+ /** Create a new ticket. */
515
728
  async create(params) {
516
729
  const teamId = this.chat.getTeamId();
517
730
  if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
@@ -669,7 +882,7 @@ var MessagesApi = class {
669
882
  constructor(chat) {
670
883
  this.chat = chat;
671
884
  }
672
- /** List messages for a conversation. */
885
+ /** List messages for a ticket. */
673
886
  async list(ticketId) {
674
887
  const teamId = this.chat.getTeamId();
675
888
  const response = await this.chat.fetch(
@@ -684,7 +897,7 @@ var MessagesApi = class {
684
897
  );
685
898
  return response.data.messages;
686
899
  }
687
- /** Send a message to a conversation. */
900
+ /** Send a message to a ticket. */
688
901
  async send(ticketId, content) {
689
902
  const teamId = this.chat.getTeamId();
690
903
  const response = await this.chat.fetch(
@@ -701,7 +914,26 @@ var MessagesApi = class {
701
914
  return response.data.message;
702
915
  }
703
916
  };
917
+ function decodeJwtSub(jwt) {
918
+ try {
919
+ const parts = jwt.split(".");
920
+ if (parts.length !== 3) return null;
921
+ const payloadB64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
922
+ const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
923
+ const payload = JSON.parse(
924
+ typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8")
925
+ );
926
+ return typeof payload.sub === "string" ? payload.sub : null;
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
704
931
  // Annotate the CommonJS export names for ESM import in node:
705
932
  0 && (module.exports = {
706
- ChatClient
933
+ ChatClient,
934
+ ChatClientAuthError,
935
+ ChatClientError,
936
+ ChatClientNetworkError,
937
+ ChatClientRateLimitError,
938
+ ChatClientServerError
707
939
  });