@cstar.help/js 0.17.0 → 0.17.2

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
@@ -233,7 +233,7 @@ The client keeps the access token internally. The public result contains `custom
233
233
 
234
234
  ### Session (existing customer token)
235
235
 
236
- Pass an existing cStar customer token as an object. A token from an unrelated Supabase project does not establish a cStar customer session. Include the cStar team UUID when your application has it.
236
+ Pass an existing cStar customer token as an object. A token from an unrelated Supabase project does not establish a cStar customer session. Include the cStar team UUID when your application has it. Otherwise the client resolves its configured team slug before message, community and realtime requests.
237
237
 
238
238
  ```typescript
239
239
  import { ChatClient, type LoginWithSessionParams } from '@cstar.help/js/chat';
@@ -40,6 +40,28 @@ __export(chat_exports, {
40
40
  module.exports = __toCommonJS(chat_exports);
41
41
 
42
42
  // src/chat/realtime.ts
43
+ var channelRemovals = /* @__PURE__ */ new WeakMap();
44
+ var CHANNEL_DRAIN_TIMEOUT_MS = 5e3;
45
+ function removeRealtimeChannel(client, channel) {
46
+ const pending = channelRemovals.get(client) ?? /* @__PURE__ */ new Set();
47
+ channelRemovals.set(client, pending);
48
+ const removal = client.removeChannel(channel);
49
+ pending.add(removal);
50
+ const settled = () => pending.delete(removal);
51
+ void removal.then(settled, settled);
52
+ }
53
+ async function closeRealtimeClient(client) {
54
+ const pending = [...channelRemovals.get(client) ?? []];
55
+ if (pending.length) {
56
+ let timeout;
57
+ const deadline = new Promise((resolve) => {
58
+ timeout = setTimeout(resolve, CHANNEL_DRAIN_TIMEOUT_MS);
59
+ });
60
+ await Promise.race([Promise.allSettled(pending), deadline]);
61
+ clearTimeout(timeout);
62
+ }
63
+ await client.realtime.disconnect();
64
+ }
43
65
  var RealtimeManager = class {
44
66
  constructor(options) {
45
67
  this.client = null;
@@ -48,6 +70,7 @@ var RealtimeManager = class {
48
70
  this.refreshTimer = null;
49
71
  this.currentToken = null;
50
72
  this.destroyed = false;
73
+ this.cleanupPromise = null;
51
74
  this.options = options;
52
75
  }
53
76
  /**
@@ -69,7 +92,11 @@ var RealtimeManager = class {
69
92
  "@supabase/supabase-js"
70
93
  );
71
94
  if (this.destroyed) return false;
95
+ this.currentToken = tokenData.token;
72
96
  this.client = createClient(tokenData.realtimeUrl, tokenData.realtimeKey, {
97
+ // Reconnects and heartbeats must refresh from cStar's customer JWT,
98
+ // rather than the dedicated client's empty hosted-auth session.
99
+ accessToken: async () => this.currentToken,
73
100
  auth: { persistSession: false, autoRefreshToken: false },
74
101
  realtime: { params: { eventsPerSecond: 10 } }
75
102
  });
@@ -110,12 +137,14 @@ var RealtimeManager = class {
110
137
  * Subscribe to new messages on a ticket via Postgres Changes.
111
138
  * Returns an unsubscribe function.
112
139
  */
113
- subscribeToMessages(ticketId, callback) {
140
+ subscribeToMessages(ticketId, callback, onStatus) {
114
141
  if (!this.client) return () => {
115
142
  };
116
143
  const channelName = `messages:${ticketId}`;
117
144
  this.removeChannel(channelName);
118
- const channel = this.client.channel(channelName).on(
145
+ const channel = this.client.channel(channelName);
146
+ this.channels.set(channelName, channel);
147
+ channel.on(
119
148
  "postgres_changes",
120
149
  {
121
150
  event: "INSERT",
@@ -124,6 +153,7 @@ var RealtimeManager = class {
124
153
  filter: `ticket_id=eq.${ticketId}`
125
154
  },
126
155
  (payload) => {
156
+ if (this.channels.get(channelName) !== channel) return;
127
157
  const row = payload.new;
128
158
  if (!row) return;
129
159
  const message = {
@@ -138,9 +168,10 @@ var RealtimeManager = class {
138
168
  };
139
169
  callback(message);
140
170
  }
141
- ).subscribe();
142
- this.channels.set(channelName, channel);
143
- return () => this.removeChannel(channelName);
171
+ ).subscribe((status) => {
172
+ if (this.channels.get(channelName) === channel) onStatus?.(status);
173
+ });
174
+ return () => this.removeChannel(channelName, channel);
144
175
  }
145
176
  /** Get the underlying realtime client (for sharing with PresenceManager). */
146
177
  getClient() {
@@ -158,15 +189,17 @@ var RealtimeManager = class {
158
189
  get isReady() {
159
190
  return this.client !== null && this.currentToken !== null;
160
191
  }
161
- removeChannel(name) {
192
+ removeChannel(name, expected) {
162
193
  const existing = this.channels.get(name);
194
+ if (expected && existing !== expected) return;
163
195
  if (existing && this.client) {
164
- this.client.removeChannel(existing);
165
196
  this.channels.delete(name);
197
+ removeRealtimeChannel(this.client, existing);
166
198
  }
167
199
  }
168
200
  /** Clean up all channels, timers, and the client. */
169
201
  destroy() {
202
+ if (this.destroyed) return this.cleanupPromise ?? Promise.resolve();
170
203
  this.destroyed = true;
171
204
  if (this.refreshTimer) {
172
205
  clearTimeout(this.refreshTimer);
@@ -176,9 +209,13 @@ var RealtimeManager = class {
176
209
  this.removeChannel(name);
177
210
  }
178
211
  this.channels.clear();
212
+ const client = this.client;
179
213
  this.client = null;
180
214
  this.initPromise = null;
181
215
  this.currentToken = null;
216
+ this.cleanupPromise = client ? closeRealtimeClient(client).catch(() => {
217
+ }) : Promise.resolve();
218
+ return this.cleanupPromise;
182
219
  }
183
220
  };
184
221
 
@@ -208,7 +245,10 @@ var PresenceManager = class {
208
245
  };
209
246
  const channelName = `typing:${ticketId}`;
210
247
  this.removeChannel(channelName);
211
- const channel = this.client.channel(`ticket:${ticketId}`).on("broadcast", { event: "typing" }, (data) => {
248
+ const channel = this.client.channel(`ticket:${ticketId}`);
249
+ this.channels.set(channelName, channel);
250
+ channel.on("broadcast", { event: "typing" }, (data) => {
251
+ if (this.channels.get(channelName) !== channel) return;
212
252
  const payload = data?.payload;
213
253
  if (!payload) return;
214
254
  if (payload.agent_id) {
@@ -220,8 +260,7 @@ var PresenceManager = class {
220
260
  callback(typingPayload);
221
261
  }
222
262
  }).subscribe();
223
- this.channels.set(channelName, channel);
224
- return () => this.removeChannel(channelName);
263
+ return () => this.removeChannel(channelName, channel);
225
264
  }
226
265
  /**
227
266
  * Send a customer typing indicator.
@@ -274,11 +313,12 @@ var PresenceManager = class {
274
313
  } catch {
275
314
  }
276
315
  }
277
- removeChannel(name) {
316
+ removeChannel(name, expected) {
278
317
  const existing = this.channels.get(name);
318
+ if (expected && existing !== expected) return;
279
319
  if (existing && this.client) {
280
- this.client.removeChannel(existing);
281
320
  this.channels.delete(name);
321
+ removeRealtimeChannel(this.client, existing);
282
322
  }
283
323
  }
284
324
  /** Clean up all channels and timers. */
@@ -440,6 +480,7 @@ var ChatClient = class {
440
480
  this.authMode = null;
441
481
  this.sessionGeneration = 0;
442
482
  this.pollingSubscriptions = /* @__PURE__ */ new Set();
483
+ this.realtimeMessageSubscriptions = /* @__PURE__ */ new Map();
443
484
  this.stateListeners = /* @__PURE__ */ new Set();
444
485
  this.realtimeManager = null;
445
486
  this.presenceManager = null;
@@ -605,11 +646,16 @@ var ChatClient = class {
605
646
  * @returns Unsubscribe function
606
647
  */
607
648
  onMessage(ticketId, callback) {
608
- const generation = this.sessionGeneration;
609
649
  if (this.realtimeReady && this.realtimeManager) {
610
- return this.realtimeManager.subscribeToMessages(ticketId, (message) => {
611
- if (generation === this.sessionGeneration) callback(message);
612
- });
650
+ this.realtimeMessageSubscriptions.get(ticketId)?.();
651
+ const unsubscribe = this.pollMessages(ticketId, callback, this.realtimeManager);
652
+ this.realtimeMessageSubscriptions.set(ticketId, unsubscribe);
653
+ return () => {
654
+ unsubscribe();
655
+ if (this.realtimeMessageSubscriptions.get(ticketId) === unsubscribe) {
656
+ this.realtimeMessageSubscriptions.delete(ticketId);
657
+ }
658
+ };
613
659
  }
614
660
  return this.pollMessages(ticketId, callback);
615
661
  }
@@ -644,20 +690,21 @@ var ChatClient = class {
644
690
  }
645
691
  retireSession() {
646
692
  const generation = ++this.sessionGeneration;
647
- if (this.realtimeManager) {
648
- this.realtimeManager.destroy();
649
- this.realtimeManager = null;
650
- }
651
693
  if (this.presenceManager) {
652
694
  this.presenceManager.destroy();
653
695
  this.presenceManager = null;
654
696
  }
697
+ if (this.realtimeManager) {
698
+ this.realtimeManager.destroy();
699
+ this.realtimeManager = null;
700
+ }
655
701
  this.realtimeReady = false;
656
702
  this.realtimeInitPromise = null;
657
703
  for (const unsubscribe of this.pollingSubscriptions) {
658
704
  unsubscribe();
659
705
  }
660
706
  this.pollingSubscriptions.clear();
707
+ this.realtimeMessageSubscriptions.clear();
661
708
  this.sessionToken = null;
662
709
  this.teamId = null;
663
710
  this.customerId = null;
@@ -828,14 +875,9 @@ var ChatClient = class {
828
875
  }
829
876
  /**
830
877
  * Resolve a team ID, lazily looking it up from `teamSlug` if no team ID
831
- * has been cached yet. Used by `tickets.list` and friends so that a
832
- * caller who constructed `new ChatClient({ teamSlug, publishableKey })`
833
- * doesn't have to manually call `loginWithCredentials` before reading
834
- * public data.
835
- *
836
- * Closes NEW-v3-05 — `TicketsState(chatClient)` previously errored
837
- * `"Missing teamId"` when the chat client was teamSlug-only because the
838
- * tickets sub-resource read `getTeamId()` synchronously and bailed.
878
+ * has been cached yet. Authenticated resources and realtime bootstrap
879
+ * use this after session login so callers do not need to list tickets
880
+ * or set a UUID before opening an existing conversation.
839
881
  */
840
882
  async ensureTeamId(signal) {
841
883
  const assertCurrent = this.captureSession();
@@ -887,54 +929,90 @@ var ChatClient = class {
887
929
  }
888
930
  }
889
931
  async fetchRealtimeToken(generation) {
932
+ this.assertSession(generation);
933
+ const teamId = await this.ensureTeamId();
890
934
  this.assertSession(generation);
891
935
  const response = await this.fetch(
892
936
  "/api/widget/realtime-token",
893
937
  {
894
938
  method: "POST",
895
- body: { teamId: this.teamId }
939
+ body: { teamId }
896
940
  }
897
941
  );
898
942
  this.assertSession(generation);
899
943
  return response.data;
900
944
  }
901
- pollMessages(ticketId, callback) {
902
- let lastMessageId = null;
945
+ pollMessages(ticketId, callback, manager) {
946
+ const seen = /* @__PURE__ */ new Set();
903
947
  let initialized = false;
904
948
  let active = true;
905
949
  let inFlight = false;
906
- const poll = async () => {
907
- if (!active || inFlight) return;
950
+ let catchUpQueued = false;
951
+ let connected = false;
952
+ let timer;
953
+ let stopRealtime;
954
+ const deliver = (message) => {
955
+ if (!active || seen.has(message.id)) return;
956
+ seen.add(message.id);
957
+ callback(message);
958
+ };
959
+ const stopPolling = () => {
960
+ clearInterval(timer);
961
+ timer = void 0;
962
+ };
963
+ const startPolling = () => {
964
+ if (active && timer === void 0) {
965
+ timer = setInterval(() => void poll(), this.pollingInterval);
966
+ }
967
+ };
968
+ const poll = async (catchUp = false) => {
969
+ if (!active) return;
970
+ if (inFlight) {
971
+ if (catchUp) catchUpQueued = true;
972
+ return;
973
+ }
908
974
  inFlight = true;
909
975
  try {
910
- const messages = await this.messages.list(ticketId);
911
- if (!active) return;
912
- if (messages.length > 0) {
913
- const latest = messages[messages.length - 1];
914
- if (initialized && latest.id !== lastMessageId) {
915
- const lastIdx = messages.findIndex((m) => m.id === lastMessageId);
916
- const newMessages = lastIdx >= 0 ? messages.slice(lastIdx + 1) : lastMessageId ? [latest] : messages;
917
- for (const msg of newMessages) {
976
+ do {
977
+ catchUpQueued = false;
978
+ try {
979
+ const messages = await this.messages.list(ticketId);
980
+ if (!active) return;
981
+ for (const message of messages) {
918
982
  if (!active) return;
919
- callback(msg);
983
+ if (initialized) deliver(message);
984
+ else seen.add(message.id);
920
985
  }
986
+ initialized = true;
987
+ if (connected) stopPolling();
988
+ } catch {
989
+ startPolling();
921
990
  }
922
- lastMessageId = latest.id;
923
- }
924
- initialized = true;
925
- } catch {
991
+ } while (active && catchUpQueued);
926
992
  } finally {
927
993
  inFlight = false;
928
994
  }
929
995
  };
930
- poll();
931
- const timer = setInterval(poll, this.pollingInterval);
932
996
  const unsubscribe = () => {
933
997
  active = false;
934
- clearInterval(timer);
998
+ stopPolling();
999
+ stopRealtime?.();
935
1000
  this.pollingSubscriptions.delete(unsubscribe);
936
1001
  };
937
1002
  this.pollingSubscriptions.add(unsubscribe);
1003
+ void poll();
1004
+ startPolling();
1005
+ stopRealtime = manager?.subscribeToMessages(ticketId, deliver, (status) => {
1006
+ if (!active) return;
1007
+ if (status === "SUBSCRIBED") {
1008
+ connected = true;
1009
+ stopPolling();
1010
+ } else if (["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"].includes(status)) {
1011
+ connected = false;
1012
+ startPolling();
1013
+ } else return;
1014
+ void poll(true);
1015
+ });
938
1016
  return unsubscribe;
939
1017
  }
940
1018
  buildUrl(path, query) {
@@ -1023,8 +1101,12 @@ var ChatCommunityApi = class {
1023
1101
  }
1024
1102
  /** Create a new community post as the identified customer. */
1025
1103
  async createPost(params) {
1026
- const teamId = this.chat.getTeamId();
1027
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1104
+ const assertCurrent = this.chat.captureSession();
1105
+ if (this.chat.getAuthMode() === null) {
1106
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1107
+ }
1108
+ const teamId = await this.chat.ensureTeamId();
1109
+ assertCurrent();
1028
1110
  const response = await this.chat.fetch("/api/widget/community", {
1029
1111
  method: "POST",
1030
1112
  body: {
@@ -1036,12 +1118,17 @@ var ChatCommunityApi = class {
1036
1118
  customFields: params.customFields
1037
1119
  }
1038
1120
  });
1121
+ assertCurrent();
1039
1122
  return response.data.post;
1040
1123
  }
1041
1124
  /** Update a post. Only the original author can edit. */
1042
1125
  async updatePost(postId, params) {
1043
- const teamId = this.chat.getTeamId();
1044
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1126
+ const assertCurrent = this.chat.captureSession();
1127
+ if (this.chat.getAuthMode() === null) {
1128
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1129
+ }
1130
+ const teamId = await this.chat.ensureTeamId();
1131
+ assertCurrent();
1045
1132
  const response = await this.chat.fetch("/api/widget/community", {
1046
1133
  method: "POST",
1047
1134
  body: {
@@ -1053,21 +1140,31 @@ var ChatCommunityApi = class {
1053
1140
  customFields: params.customFields
1054
1141
  }
1055
1142
  });
1143
+ assertCurrent();
1056
1144
  return response.data.post;
1057
1145
  }
1058
1146
  /** Delete a post (soft delete). Only the original author can delete. */
1059
1147
  async deletePost(postId) {
1060
- const teamId = this.chat.getTeamId();
1061
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1148
+ const assertCurrent = this.chat.captureSession();
1149
+ if (this.chat.getAuthMode() === null) {
1150
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1151
+ }
1152
+ const teamId = await this.chat.ensureTeamId();
1153
+ assertCurrent();
1062
1154
  await this.chat.fetch("/api/widget/community", {
1063
1155
  method: "POST",
1064
1156
  body: { teamId, action: "deletePost", postId }
1065
1157
  });
1158
+ assertCurrent();
1066
1159
  }
1067
1160
  /** Update a comment. Only the original author can edit. */
1068
1161
  async updateComment(postId, commentId, params) {
1069
- const teamId = this.chat.getTeamId();
1070
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1162
+ const assertCurrent = this.chat.captureSession();
1163
+ if (this.chat.getAuthMode() === null) {
1164
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1165
+ }
1166
+ const teamId = await this.chat.ensureTeamId();
1167
+ assertCurrent();
1071
1168
  const response = await this.chat.fetch("/api/widget/community", {
1072
1169
  method: "POST",
1073
1170
  body: {
@@ -1078,31 +1175,46 @@ var ChatCommunityApi = class {
1078
1175
  commentBody: params.body
1079
1176
  }
1080
1177
  });
1178
+ assertCurrent();
1081
1179
  return response.data.comment;
1082
1180
  }
1083
1181
  /** Delete a comment (soft delete). Only the original author can delete. */
1084
1182
  async deleteComment(postId, commentId) {
1085
- const teamId = this.chat.getTeamId();
1086
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1183
+ const assertCurrent = this.chat.captureSession();
1184
+ if (this.chat.getAuthMode() === null) {
1185
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1186
+ }
1187
+ const teamId = await this.chat.ensureTeamId();
1188
+ assertCurrent();
1087
1189
  await this.chat.fetch("/api/widget/community", {
1088
1190
  method: "POST",
1089
1191
  body: { teamId, action: "deleteComment", postId, commentId }
1090
1192
  });
1193
+ assertCurrent();
1091
1194
  }
1092
1195
  /** Toggle vote on a post. Returns new vote state and count. */
1093
1196
  async vote(postId) {
1094
- const teamId = this.chat.getTeamId();
1095
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1197
+ const assertCurrent = this.chat.captureSession();
1198
+ if (this.chat.getAuthMode() === null) {
1199
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1200
+ }
1201
+ const teamId = await this.chat.ensureTeamId();
1202
+ assertCurrent();
1096
1203
  const response = await this.chat.fetch("/api/widget/community", {
1097
1204
  method: "POST",
1098
1205
  body: { teamId, action: "vote", postId }
1099
1206
  });
1207
+ assertCurrent();
1100
1208
  return response.data;
1101
1209
  }
1102
1210
  /** Add a comment to a post. Supports 1-level nesting via parentCommentId. */
1103
1211
  async comment(postId, params) {
1104
- const teamId = this.chat.getTeamId();
1105
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1212
+ const assertCurrent = this.chat.captureSession();
1213
+ if (this.chat.getAuthMode() === null) {
1214
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1215
+ }
1216
+ const teamId = await this.chat.ensureTeamId();
1217
+ assertCurrent();
1106
1218
  const response = await this.chat.fetch("/api/widget/community", {
1107
1219
  method: "POST",
1108
1220
  body: {
@@ -1113,37 +1225,53 @@ var ChatCommunityApi = class {
1113
1225
  parentCommentId: params.parentCommentId ?? null
1114
1226
  }
1115
1227
  });
1228
+ assertCurrent();
1116
1229
  return response.data.comment;
1117
1230
  }
1118
1231
  /** Toggle follow on a post. */
1119
1232
  async follow(postId) {
1120
- const teamId = this.chat.getTeamId();
1121
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1233
+ const assertCurrent = this.chat.captureSession();
1234
+ if (this.chat.getAuthMode() === null) {
1235
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1236
+ }
1237
+ const teamId = await this.chat.ensureTeamId();
1238
+ assertCurrent();
1122
1239
  const response = await this.chat.fetch("/api/widget/community", {
1123
1240
  method: "POST",
1124
1241
  body: { teamId, action: "follow", postId }
1125
1242
  });
1243
+ assertCurrent();
1126
1244
  return response.data;
1127
1245
  }
1128
1246
  /** Check if the current user has voted on a post. */
1129
1247
  async getVoteStatus(postId) {
1130
- const teamId = this.chat.getTeamId();
1131
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1248
+ const assertCurrent = this.chat.captureSession();
1249
+ if (this.chat.getAuthMode() === null) {
1250
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1251
+ }
1252
+ const teamId = await this.chat.ensureTeamId();
1253
+ assertCurrent();
1132
1254
  const response = await this.chat.fetch("/api/widget/community", {
1133
1255
  method: "GET",
1134
1256
  query: { teamId, postId }
1135
1257
  });
1258
+ assertCurrent();
1136
1259
  return response.data.voted;
1137
1260
  }
1138
1261
  /** Check vote status for multiple posts in one request. */
1139
1262
  async bulkVoteStatus(postIds) {
1140
1263
  if (postIds.length === 0) return {};
1141
- const teamId = this.chat.getTeamId();
1142
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1264
+ const assertCurrent = this.chat.captureSession();
1265
+ if (this.chat.getAuthMode() === null) {
1266
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1267
+ }
1268
+ const teamId = await this.chat.ensureTeamId();
1269
+ assertCurrent();
1143
1270
  const response = await this.chat.fetch("/api/widget/community", {
1144
1271
  method: "GET",
1145
1272
  query: { teamId, postIds: postIds.join(",") }
1146
1273
  });
1274
+ assertCurrent();
1147
1275
  return response.data;
1148
1276
  }
1149
1277
  };
@@ -1154,13 +1282,14 @@ var MessagesApi = class {
1154
1282
  /** List messages for a ticket. */
1155
1283
  async list(ticketId) {
1156
1284
  const assertCurrent = this.chat.captureSession();
1157
- const teamId = this.chat.getTeamId();
1285
+ const teamId = await this.chat.ensureTeamId();
1286
+ assertCurrent();
1158
1287
  const response = await this.chat.fetch(
1159
1288
  "/api/widget/messages",
1160
1289
  {
1161
1290
  method: "GET",
1162
1291
  query: {
1163
- teamId: teamId ?? void 0,
1292
+ teamId,
1164
1293
  ticketId
1165
1294
  }
1166
1295
  }
@@ -1171,7 +1300,8 @@ var MessagesApi = class {
1171
1300
  /** Send a message to a ticket. */
1172
1301
  async send(ticketId, content) {
1173
1302
  const assertCurrent = this.chat.captureSession();
1174
- const teamId = this.chat.getTeamId();
1303
+ const teamId = await this.chat.ensureTeamId();
1304
+ assertCurrent();
1175
1305
  const response = await this.chat.fetch(
1176
1306
  "/api/widget/messages",
1177
1307
  {
@@ -321,6 +321,7 @@ declare class ChatClient {
321
321
  private authMode;
322
322
  private sessionGeneration;
323
323
  private pollingSubscriptions;
324
+ private realtimeMessageSubscriptions;
324
325
  private stateListeners;
325
326
  private realtimeManager;
326
327
  private presenceManager;
@@ -443,14 +444,9 @@ declare class ChatClient {
443
444
  getTeamId(): string | null;
444
445
  /**
445
446
  * Resolve a team ID, lazily looking it up from `teamSlug` if no team ID
446
- * has been cached yet. Used by `tickets.list` and friends so that a
447
- * caller who constructed `new ChatClient({ teamSlug, publishableKey })`
448
- * doesn't have to manually call `loginWithCredentials` before reading
449
- * public data.
450
- *
451
- * Closes NEW-v3-05 — `TicketsState(chatClient)` previously errored
452
- * `"Missing teamId"` when the chat client was teamSlug-only because the
453
- * tickets sub-resource read `getTeamId()` synchronously and bailed.
447
+ * has been cached yet. Authenticated resources and realtime bootstrap
448
+ * use this after session login so callers do not need to list tickets
449
+ * or set a UUID before opening an existing conversation.
454
450
  */
455
451
  ensureTeamId(signal?: AbortSignal): Promise<string>;
456
452
  /** @internal Set team ID from identify response or other source. */
@@ -321,6 +321,7 @@ declare class ChatClient {
321
321
  private authMode;
322
322
  private sessionGeneration;
323
323
  private pollingSubscriptions;
324
+ private realtimeMessageSubscriptions;
324
325
  private stateListeners;
325
326
  private realtimeManager;
326
327
  private presenceManager;
@@ -443,14 +444,9 @@ declare class ChatClient {
443
444
  getTeamId(): string | null;
444
445
  /**
445
446
  * Resolve a team ID, lazily looking it up from `teamSlug` if no team ID
446
- * has been cached yet. Used by `tickets.list` and friends so that a
447
- * caller who constructed `new ChatClient({ teamSlug, publishableKey })`
448
- * doesn't have to manually call `loginWithCredentials` before reading
449
- * public data.
450
- *
451
- * Closes NEW-v3-05 — `TicketsState(chatClient)` previously errored
452
- * `"Missing teamId"` when the chat client was teamSlug-only because the
453
- * tickets sub-resource read `getTeamId()` synchronously and bailed.
447
+ * has been cached yet. Authenticated resources and realtime bootstrap
448
+ * use this after session login so callers do not need to list tickets
449
+ * or set a UUID before opening an existing conversation.
454
450
  */
455
451
  ensureTeamId(signal?: AbortSignal): Promise<string>;
456
452
  /** @internal Set team ID from identify response or other source. */
@@ -1,4 +1,26 @@
1
1
  // src/chat/realtime.ts
2
+ var channelRemovals = /* @__PURE__ */ new WeakMap();
3
+ var CHANNEL_DRAIN_TIMEOUT_MS = 5e3;
4
+ function removeRealtimeChannel(client, channel) {
5
+ const pending = channelRemovals.get(client) ?? /* @__PURE__ */ new Set();
6
+ channelRemovals.set(client, pending);
7
+ const removal = client.removeChannel(channel);
8
+ pending.add(removal);
9
+ const settled = () => pending.delete(removal);
10
+ void removal.then(settled, settled);
11
+ }
12
+ async function closeRealtimeClient(client) {
13
+ const pending = [...channelRemovals.get(client) ?? []];
14
+ if (pending.length) {
15
+ let timeout;
16
+ const deadline = new Promise((resolve) => {
17
+ timeout = setTimeout(resolve, CHANNEL_DRAIN_TIMEOUT_MS);
18
+ });
19
+ await Promise.race([Promise.allSettled(pending), deadline]);
20
+ clearTimeout(timeout);
21
+ }
22
+ await client.realtime.disconnect();
23
+ }
2
24
  var RealtimeManager = class {
3
25
  constructor(options) {
4
26
  this.client = null;
@@ -7,6 +29,7 @@ var RealtimeManager = class {
7
29
  this.refreshTimer = null;
8
30
  this.currentToken = null;
9
31
  this.destroyed = false;
32
+ this.cleanupPromise = null;
10
33
  this.options = options;
11
34
  }
12
35
  /**
@@ -28,7 +51,11 @@ var RealtimeManager = class {
28
51
  "@supabase/supabase-js"
29
52
  );
30
53
  if (this.destroyed) return false;
54
+ this.currentToken = tokenData.token;
31
55
  this.client = createClient(tokenData.realtimeUrl, tokenData.realtimeKey, {
56
+ // Reconnects and heartbeats must refresh from cStar's customer JWT,
57
+ // rather than the dedicated client's empty hosted-auth session.
58
+ accessToken: async () => this.currentToken,
32
59
  auth: { persistSession: false, autoRefreshToken: false },
33
60
  realtime: { params: { eventsPerSecond: 10 } }
34
61
  });
@@ -69,12 +96,14 @@ var RealtimeManager = class {
69
96
  * Subscribe to new messages on a ticket via Postgres Changes.
70
97
  * Returns an unsubscribe function.
71
98
  */
72
- subscribeToMessages(ticketId, callback) {
99
+ subscribeToMessages(ticketId, callback, onStatus) {
73
100
  if (!this.client) return () => {
74
101
  };
75
102
  const channelName = `messages:${ticketId}`;
76
103
  this.removeChannel(channelName);
77
- const channel = this.client.channel(channelName).on(
104
+ const channel = this.client.channel(channelName);
105
+ this.channels.set(channelName, channel);
106
+ channel.on(
78
107
  "postgres_changes",
79
108
  {
80
109
  event: "INSERT",
@@ -83,6 +112,7 @@ var RealtimeManager = class {
83
112
  filter: `ticket_id=eq.${ticketId}`
84
113
  },
85
114
  (payload) => {
115
+ if (this.channels.get(channelName) !== channel) return;
86
116
  const row = payload.new;
87
117
  if (!row) return;
88
118
  const message = {
@@ -97,9 +127,10 @@ var RealtimeManager = class {
97
127
  };
98
128
  callback(message);
99
129
  }
100
- ).subscribe();
101
- this.channels.set(channelName, channel);
102
- return () => this.removeChannel(channelName);
130
+ ).subscribe((status) => {
131
+ if (this.channels.get(channelName) === channel) onStatus?.(status);
132
+ });
133
+ return () => this.removeChannel(channelName, channel);
103
134
  }
104
135
  /** Get the underlying realtime client (for sharing with PresenceManager). */
105
136
  getClient() {
@@ -117,15 +148,17 @@ var RealtimeManager = class {
117
148
  get isReady() {
118
149
  return this.client !== null && this.currentToken !== null;
119
150
  }
120
- removeChannel(name) {
151
+ removeChannel(name, expected) {
121
152
  const existing = this.channels.get(name);
153
+ if (expected && existing !== expected) return;
122
154
  if (existing && this.client) {
123
- this.client.removeChannel(existing);
124
155
  this.channels.delete(name);
156
+ removeRealtimeChannel(this.client, existing);
125
157
  }
126
158
  }
127
159
  /** Clean up all channels, timers, and the client. */
128
160
  destroy() {
161
+ if (this.destroyed) return this.cleanupPromise ?? Promise.resolve();
129
162
  this.destroyed = true;
130
163
  if (this.refreshTimer) {
131
164
  clearTimeout(this.refreshTimer);
@@ -135,9 +168,13 @@ var RealtimeManager = class {
135
168
  this.removeChannel(name);
136
169
  }
137
170
  this.channels.clear();
171
+ const client = this.client;
138
172
  this.client = null;
139
173
  this.initPromise = null;
140
174
  this.currentToken = null;
175
+ this.cleanupPromise = client ? closeRealtimeClient(client).catch(() => {
176
+ }) : Promise.resolve();
177
+ return this.cleanupPromise;
141
178
  }
142
179
  };
143
180
 
@@ -167,7 +204,10 @@ var PresenceManager = class {
167
204
  };
168
205
  const channelName = `typing:${ticketId}`;
169
206
  this.removeChannel(channelName);
170
- const channel = this.client.channel(`ticket:${ticketId}`).on("broadcast", { event: "typing" }, (data) => {
207
+ const channel = this.client.channel(`ticket:${ticketId}`);
208
+ this.channels.set(channelName, channel);
209
+ channel.on("broadcast", { event: "typing" }, (data) => {
210
+ if (this.channels.get(channelName) !== channel) return;
171
211
  const payload = data?.payload;
172
212
  if (!payload) return;
173
213
  if (payload.agent_id) {
@@ -179,8 +219,7 @@ var PresenceManager = class {
179
219
  callback(typingPayload);
180
220
  }
181
221
  }).subscribe();
182
- this.channels.set(channelName, channel);
183
- return () => this.removeChannel(channelName);
222
+ return () => this.removeChannel(channelName, channel);
184
223
  }
185
224
  /**
186
225
  * Send a customer typing indicator.
@@ -233,11 +272,12 @@ var PresenceManager = class {
233
272
  } catch {
234
273
  }
235
274
  }
236
- removeChannel(name) {
275
+ removeChannel(name, expected) {
237
276
  const existing = this.channels.get(name);
277
+ if (expected && existing !== expected) return;
238
278
  if (existing && this.client) {
239
- this.client.removeChannel(existing);
240
279
  this.channels.delete(name);
280
+ removeRealtimeChannel(this.client, existing);
241
281
  }
242
282
  }
243
283
  /** Clean up all channels and timers. */
@@ -399,6 +439,7 @@ var ChatClient = class {
399
439
  this.authMode = null;
400
440
  this.sessionGeneration = 0;
401
441
  this.pollingSubscriptions = /* @__PURE__ */ new Set();
442
+ this.realtimeMessageSubscriptions = /* @__PURE__ */ new Map();
402
443
  this.stateListeners = /* @__PURE__ */ new Set();
403
444
  this.realtimeManager = null;
404
445
  this.presenceManager = null;
@@ -564,11 +605,16 @@ var ChatClient = class {
564
605
  * @returns Unsubscribe function
565
606
  */
566
607
  onMessage(ticketId, callback) {
567
- const generation = this.sessionGeneration;
568
608
  if (this.realtimeReady && this.realtimeManager) {
569
- return this.realtimeManager.subscribeToMessages(ticketId, (message) => {
570
- if (generation === this.sessionGeneration) callback(message);
571
- });
609
+ this.realtimeMessageSubscriptions.get(ticketId)?.();
610
+ const unsubscribe = this.pollMessages(ticketId, callback, this.realtimeManager);
611
+ this.realtimeMessageSubscriptions.set(ticketId, unsubscribe);
612
+ return () => {
613
+ unsubscribe();
614
+ if (this.realtimeMessageSubscriptions.get(ticketId) === unsubscribe) {
615
+ this.realtimeMessageSubscriptions.delete(ticketId);
616
+ }
617
+ };
572
618
  }
573
619
  return this.pollMessages(ticketId, callback);
574
620
  }
@@ -603,20 +649,21 @@ var ChatClient = class {
603
649
  }
604
650
  retireSession() {
605
651
  const generation = ++this.sessionGeneration;
606
- if (this.realtimeManager) {
607
- this.realtimeManager.destroy();
608
- this.realtimeManager = null;
609
- }
610
652
  if (this.presenceManager) {
611
653
  this.presenceManager.destroy();
612
654
  this.presenceManager = null;
613
655
  }
656
+ if (this.realtimeManager) {
657
+ this.realtimeManager.destroy();
658
+ this.realtimeManager = null;
659
+ }
614
660
  this.realtimeReady = false;
615
661
  this.realtimeInitPromise = null;
616
662
  for (const unsubscribe of this.pollingSubscriptions) {
617
663
  unsubscribe();
618
664
  }
619
665
  this.pollingSubscriptions.clear();
666
+ this.realtimeMessageSubscriptions.clear();
620
667
  this.sessionToken = null;
621
668
  this.teamId = null;
622
669
  this.customerId = null;
@@ -787,14 +834,9 @@ var ChatClient = class {
787
834
  }
788
835
  /**
789
836
  * Resolve a team ID, lazily looking it up from `teamSlug` if no team ID
790
- * has been cached yet. Used by `tickets.list` and friends so that a
791
- * caller who constructed `new ChatClient({ teamSlug, publishableKey })`
792
- * doesn't have to manually call `loginWithCredentials` before reading
793
- * public data.
794
- *
795
- * Closes NEW-v3-05 — `TicketsState(chatClient)` previously errored
796
- * `"Missing teamId"` when the chat client was teamSlug-only because the
797
- * tickets sub-resource read `getTeamId()` synchronously and bailed.
837
+ * has been cached yet. Authenticated resources and realtime bootstrap
838
+ * use this after session login so callers do not need to list tickets
839
+ * or set a UUID before opening an existing conversation.
798
840
  */
799
841
  async ensureTeamId(signal) {
800
842
  const assertCurrent = this.captureSession();
@@ -846,54 +888,90 @@ var ChatClient = class {
846
888
  }
847
889
  }
848
890
  async fetchRealtimeToken(generation) {
891
+ this.assertSession(generation);
892
+ const teamId = await this.ensureTeamId();
849
893
  this.assertSession(generation);
850
894
  const response = await this.fetch(
851
895
  "/api/widget/realtime-token",
852
896
  {
853
897
  method: "POST",
854
- body: { teamId: this.teamId }
898
+ body: { teamId }
855
899
  }
856
900
  );
857
901
  this.assertSession(generation);
858
902
  return response.data;
859
903
  }
860
- pollMessages(ticketId, callback) {
861
- let lastMessageId = null;
904
+ pollMessages(ticketId, callback, manager) {
905
+ const seen = /* @__PURE__ */ new Set();
862
906
  let initialized = false;
863
907
  let active = true;
864
908
  let inFlight = false;
865
- const poll = async () => {
866
- if (!active || inFlight) return;
909
+ let catchUpQueued = false;
910
+ let connected = false;
911
+ let timer;
912
+ let stopRealtime;
913
+ const deliver = (message) => {
914
+ if (!active || seen.has(message.id)) return;
915
+ seen.add(message.id);
916
+ callback(message);
917
+ };
918
+ const stopPolling = () => {
919
+ clearInterval(timer);
920
+ timer = void 0;
921
+ };
922
+ const startPolling = () => {
923
+ if (active && timer === void 0) {
924
+ timer = setInterval(() => void poll(), this.pollingInterval);
925
+ }
926
+ };
927
+ const poll = async (catchUp = false) => {
928
+ if (!active) return;
929
+ if (inFlight) {
930
+ if (catchUp) catchUpQueued = true;
931
+ return;
932
+ }
867
933
  inFlight = true;
868
934
  try {
869
- const messages = await this.messages.list(ticketId);
870
- if (!active) return;
871
- if (messages.length > 0) {
872
- const latest = messages[messages.length - 1];
873
- if (initialized && latest.id !== lastMessageId) {
874
- const lastIdx = messages.findIndex((m) => m.id === lastMessageId);
875
- const newMessages = lastIdx >= 0 ? messages.slice(lastIdx + 1) : lastMessageId ? [latest] : messages;
876
- for (const msg of newMessages) {
935
+ do {
936
+ catchUpQueued = false;
937
+ try {
938
+ const messages = await this.messages.list(ticketId);
939
+ if (!active) return;
940
+ for (const message of messages) {
877
941
  if (!active) return;
878
- callback(msg);
942
+ if (initialized) deliver(message);
943
+ else seen.add(message.id);
879
944
  }
945
+ initialized = true;
946
+ if (connected) stopPolling();
947
+ } catch {
948
+ startPolling();
880
949
  }
881
- lastMessageId = latest.id;
882
- }
883
- initialized = true;
884
- } catch {
950
+ } while (active && catchUpQueued);
885
951
  } finally {
886
952
  inFlight = false;
887
953
  }
888
954
  };
889
- poll();
890
- const timer = setInterval(poll, this.pollingInterval);
891
955
  const unsubscribe = () => {
892
956
  active = false;
893
- clearInterval(timer);
957
+ stopPolling();
958
+ stopRealtime?.();
894
959
  this.pollingSubscriptions.delete(unsubscribe);
895
960
  };
896
961
  this.pollingSubscriptions.add(unsubscribe);
962
+ void poll();
963
+ startPolling();
964
+ stopRealtime = manager?.subscribeToMessages(ticketId, deliver, (status) => {
965
+ if (!active) return;
966
+ if (status === "SUBSCRIBED") {
967
+ connected = true;
968
+ stopPolling();
969
+ } else if (["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"].includes(status)) {
970
+ connected = false;
971
+ startPolling();
972
+ } else return;
973
+ void poll(true);
974
+ });
897
975
  return unsubscribe;
898
976
  }
899
977
  buildUrl(path, query) {
@@ -982,8 +1060,12 @@ var ChatCommunityApi = class {
982
1060
  }
983
1061
  /** Create a new community post as the identified customer. */
984
1062
  async createPost(params) {
985
- const teamId = this.chat.getTeamId();
986
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1063
+ const assertCurrent = this.chat.captureSession();
1064
+ if (this.chat.getAuthMode() === null) {
1065
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1066
+ }
1067
+ const teamId = await this.chat.ensureTeamId();
1068
+ assertCurrent();
987
1069
  const response = await this.chat.fetch("/api/widget/community", {
988
1070
  method: "POST",
989
1071
  body: {
@@ -995,12 +1077,17 @@ var ChatCommunityApi = class {
995
1077
  customFields: params.customFields
996
1078
  }
997
1079
  });
1080
+ assertCurrent();
998
1081
  return response.data.post;
999
1082
  }
1000
1083
  /** Update a post. Only the original author can edit. */
1001
1084
  async updatePost(postId, params) {
1002
- const teamId = this.chat.getTeamId();
1003
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1085
+ const assertCurrent = this.chat.captureSession();
1086
+ if (this.chat.getAuthMode() === null) {
1087
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1088
+ }
1089
+ const teamId = await this.chat.ensureTeamId();
1090
+ assertCurrent();
1004
1091
  const response = await this.chat.fetch("/api/widget/community", {
1005
1092
  method: "POST",
1006
1093
  body: {
@@ -1012,21 +1099,31 @@ var ChatCommunityApi = class {
1012
1099
  customFields: params.customFields
1013
1100
  }
1014
1101
  });
1102
+ assertCurrent();
1015
1103
  return response.data.post;
1016
1104
  }
1017
1105
  /** Delete a post (soft delete). Only the original author can delete. */
1018
1106
  async deletePost(postId) {
1019
- const teamId = this.chat.getTeamId();
1020
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1107
+ const assertCurrent = this.chat.captureSession();
1108
+ if (this.chat.getAuthMode() === null) {
1109
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1110
+ }
1111
+ const teamId = await this.chat.ensureTeamId();
1112
+ assertCurrent();
1021
1113
  await this.chat.fetch("/api/widget/community", {
1022
1114
  method: "POST",
1023
1115
  body: { teamId, action: "deletePost", postId }
1024
1116
  });
1117
+ assertCurrent();
1025
1118
  }
1026
1119
  /** Update a comment. Only the original author can edit. */
1027
1120
  async updateComment(postId, commentId, params) {
1028
- const teamId = this.chat.getTeamId();
1029
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1121
+ const assertCurrent = this.chat.captureSession();
1122
+ if (this.chat.getAuthMode() === null) {
1123
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1124
+ }
1125
+ const teamId = await this.chat.ensureTeamId();
1126
+ assertCurrent();
1030
1127
  const response = await this.chat.fetch("/api/widget/community", {
1031
1128
  method: "POST",
1032
1129
  body: {
@@ -1037,31 +1134,46 @@ var ChatCommunityApi = class {
1037
1134
  commentBody: params.body
1038
1135
  }
1039
1136
  });
1137
+ assertCurrent();
1040
1138
  return response.data.comment;
1041
1139
  }
1042
1140
  /** Delete a comment (soft delete). Only the original author can delete. */
1043
1141
  async deleteComment(postId, commentId) {
1044
- const teamId = this.chat.getTeamId();
1045
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1142
+ const assertCurrent = this.chat.captureSession();
1143
+ if (this.chat.getAuthMode() === null) {
1144
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1145
+ }
1146
+ const teamId = await this.chat.ensureTeamId();
1147
+ assertCurrent();
1046
1148
  await this.chat.fetch("/api/widget/community", {
1047
1149
  method: "POST",
1048
1150
  body: { teamId, action: "deleteComment", postId, commentId }
1049
1151
  });
1152
+ assertCurrent();
1050
1153
  }
1051
1154
  /** Toggle vote on a post. Returns new vote state and count. */
1052
1155
  async vote(postId) {
1053
- const teamId = this.chat.getTeamId();
1054
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1156
+ const assertCurrent = this.chat.captureSession();
1157
+ if (this.chat.getAuthMode() === null) {
1158
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1159
+ }
1160
+ const teamId = await this.chat.ensureTeamId();
1161
+ assertCurrent();
1055
1162
  const response = await this.chat.fetch("/api/widget/community", {
1056
1163
  method: "POST",
1057
1164
  body: { teamId, action: "vote", postId }
1058
1165
  });
1166
+ assertCurrent();
1059
1167
  return response.data;
1060
1168
  }
1061
1169
  /** Add a comment to a post. Supports 1-level nesting via parentCommentId. */
1062
1170
  async comment(postId, params) {
1063
- const teamId = this.chat.getTeamId();
1064
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1171
+ const assertCurrent = this.chat.captureSession();
1172
+ if (this.chat.getAuthMode() === null) {
1173
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1174
+ }
1175
+ const teamId = await this.chat.ensureTeamId();
1176
+ assertCurrent();
1065
1177
  const response = await this.chat.fetch("/api/widget/community", {
1066
1178
  method: "POST",
1067
1179
  body: {
@@ -1072,37 +1184,53 @@ var ChatCommunityApi = class {
1072
1184
  parentCommentId: params.parentCommentId ?? null
1073
1185
  }
1074
1186
  });
1187
+ assertCurrent();
1075
1188
  return response.data.comment;
1076
1189
  }
1077
1190
  /** Toggle follow on a post. */
1078
1191
  async follow(postId) {
1079
- const teamId = this.chat.getTeamId();
1080
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1192
+ const assertCurrent = this.chat.captureSession();
1193
+ if (this.chat.getAuthMode() === null) {
1194
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1195
+ }
1196
+ const teamId = await this.chat.ensureTeamId();
1197
+ assertCurrent();
1081
1198
  const response = await this.chat.fetch("/api/widget/community", {
1082
1199
  method: "POST",
1083
1200
  body: { teamId, action: "follow", postId }
1084
1201
  });
1202
+ assertCurrent();
1085
1203
  return response.data;
1086
1204
  }
1087
1205
  /** Check if the current user has voted on a post. */
1088
1206
  async getVoteStatus(postId) {
1089
- const teamId = this.chat.getTeamId();
1090
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1207
+ const assertCurrent = this.chat.captureSession();
1208
+ if (this.chat.getAuthMode() === null) {
1209
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1210
+ }
1211
+ const teamId = await this.chat.ensureTeamId();
1212
+ assertCurrent();
1091
1213
  const response = await this.chat.fetch("/api/widget/community", {
1092
1214
  method: "GET",
1093
1215
  query: { teamId, postId }
1094
1216
  });
1217
+ assertCurrent();
1095
1218
  return response.data.voted;
1096
1219
  }
1097
1220
  /** Check vote status for multiple posts in one request. */
1098
1221
  async bulkVoteStatus(postIds) {
1099
1222
  if (postIds.length === 0) return {};
1100
- const teamId = this.chat.getTeamId();
1101
- if (!teamId) throw new ChatClientAuthError("Not identified. Call chat.identify() first.");
1223
+ const assertCurrent = this.chat.captureSession();
1224
+ if (this.chat.getAuthMode() === null) {
1225
+ throw new ChatClientAuthError("Sign in or identify a customer before accessing community.");
1226
+ }
1227
+ const teamId = await this.chat.ensureTeamId();
1228
+ assertCurrent();
1102
1229
  const response = await this.chat.fetch("/api/widget/community", {
1103
1230
  method: "GET",
1104
1231
  query: { teamId, postIds: postIds.join(",") }
1105
1232
  });
1233
+ assertCurrent();
1106
1234
  return response.data;
1107
1235
  }
1108
1236
  };
@@ -1113,13 +1241,14 @@ var MessagesApi = class {
1113
1241
  /** List messages for a ticket. */
1114
1242
  async list(ticketId) {
1115
1243
  const assertCurrent = this.chat.captureSession();
1116
- const teamId = this.chat.getTeamId();
1244
+ const teamId = await this.chat.ensureTeamId();
1245
+ assertCurrent();
1117
1246
  const response = await this.chat.fetch(
1118
1247
  "/api/widget/messages",
1119
1248
  {
1120
1249
  method: "GET",
1121
1250
  query: {
1122
- teamId: teamId ?? void 0,
1251
+ teamId,
1123
1252
  ticketId
1124
1253
  }
1125
1254
  }
@@ -1130,7 +1259,8 @@ var MessagesApi = class {
1130
1259
  /** Send a message to a ticket. */
1131
1260
  async send(ticketId, content) {
1132
1261
  const assertCurrent = this.chat.captureSession();
1133
- const teamId = this.chat.getTeamId();
1262
+ const teamId = await this.chat.ensureTeamId();
1263
+ assertCurrent();
1134
1264
  const response = await this.chat.fetch(
1135
1265
  "/api/widget/messages",
1136
1266
  {
package/dist/index.cjs CHANGED
@@ -278,7 +278,7 @@ function parseError(statusCode, body, retryAfter) {
278
278
  }
279
279
 
280
280
  // src/client.ts
281
- var SDK_VERSION = "0.15.1";
281
+ var SDK_VERSION = "0.17.2";
282
282
  var DEFAULT_BASE_URL = "https://www.cstar.help";
283
283
  var DEFAULT_API_VERSION = "2026-03-01";
284
284
  var DEFAULT_MAX_RETRIES = 3;
package/dist/index.js CHANGED
@@ -237,7 +237,7 @@ function parseError(statusCode, body, retryAfter) {
237
237
  }
238
238
 
239
239
  // src/client.ts
240
- var SDK_VERSION = "0.15.1";
240
+ var SDK_VERSION = "0.17.2";
241
241
  var DEFAULT_BASE_URL = "https://www.cstar.help";
242
242
  var DEFAULT_API_VERSION = "2026-03-01";
243
243
  var DEFAULT_MAX_RETRIES = 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstar.help/js",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Official TypeScript SDK for the cStar customer support platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",