@cstar.help/js 0.17.0 → 0.17.3

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
@@ -116,13 +116,17 @@ for await (const ticket of cstar.tickets.listAutoPaginating({ status: 'open' }))
116
116
  | `cstar.game.boss` | `.current()` `.isActive()` `.spawn(params?)` `.damage(params)` `.defeats(params?)` `.getDefeat(id)` |
117
117
  | `cstar.game.achievements` | `.list(params?)` `.unlocked(params)` `.get(id, params?)` |
118
118
  | `cstar.game.quests` | `.today(params?)` `.history(params?)` |
119
- | `cstar.game.puzzles` | `.today(params?)` `.complete(params)` `.streak(params)` `.leaderboard(params?)` |
119
+ | `cstar.game.puzzles` | `.today(params?)` `.complete(params)` `.streak(params)` `.leaderboard(params)` |
120
120
  | `cstar.game.oneUps` | `.list(params?)` `.send(params)` `.remaining(params)` |
121
121
  | `cstar.game.highFives` | `.list(params?)` `.send(params)` `.canSend(teamId, params)` |
122
- | `cstar.game.cosmetics` | `.list(params?)` `.getPreferences(params)` `.updatePreferences(params)` |
123
- | `cstar.game.seasons` | `.list()` `.current()` `.stats(seasonId, params)` |
122
+ | `cstar.game.cosmetics` | `.list(params)` `.getPreferences(params)` `.updatePreferences(params)` |
123
+ | `cstar.game.seasons` | `.list(params?)` `.current()` `.stats(seasonId, params)` |
124
124
  | `cstar.game.skills` | `.get(params)` `.allocate(params)` `.reset(params)` |
125
125
 
126
+ Game writes belong on your server with a secret API key. Use user and team UUIDs from cStar. One-ups and high fives default their sender to the key creator. Puzzle scoreboards require a `puzzleId`; streak boards use `{ type: 'streaks' }`. Cosmetic lists require a `userId`.
127
+
128
+ Cosmetic and season lists return `{ data, pagination, hasMore }`. One-up and high-five lists return `{ object: 'list', data }`. Today's quests return `{ quests, completedToday, totalQuests, allComplete, completionBonus, resetsAt }`. Single resources return bare objects.
129
+
126
130
  **Advanced** — AI, automations, views, billing, webhooks.
127
131
 
128
132
  | Resource | Methods |
@@ -140,20 +144,21 @@ for await (const ticket of cstar.tickets.listAutoPaginating({ status: 'open' }))
140
144
 
141
145
  ```typescript
142
146
  // Team analytics
143
- const { data: overview } = await cstar.analytics.overview({ period: 'week' });
144
- console.log(`${overview.tickets.open} open tickets this week`);
147
+ const overview = await cstar.analytics.overview({ period: 'week' });
148
+ console.log(`${overview.tickets?.open ?? 0} open tickets this week`);
145
149
 
146
150
  // SLA rules
147
151
  const { data: rules } = await cstar.sla.list();
148
152
 
149
- // Boss battle
150
- const { data: boss } = await cstar.game.boss.current();
151
- if (boss.active) {
152
- console.log(`${boss.name} — ${boss.health}/${boss.maxHealth} HP`);
153
- }
153
+ // Cheer on another team. The sender defaults to your API key's creator.
154
+ const highFive = await cstar.game.highFives.send({
155
+ recipientTeamId: '550e8400-e29b-41d4-a716-446655440003',
156
+ message: 'Amazing work!'
157
+ });
158
+ console.log(`A high five from ${highFive.senderTeam.name}`);
154
159
 
155
160
  // AI-powered reply suggestion
156
- const { data: suggestion } = await cstar.ai.suggestReply('tkt_abc123');
161
+ const suggestion = await cstar.ai.suggestReply('tkt_abc123');
157
162
 
158
163
  // Bulk close resolved tickets
159
164
  await cstar.bulk.update({
@@ -233,7 +238,7 @@ The client keeps the access token internally. The public result contains `custom
233
238
 
234
239
  ### Session (existing customer token)
235
240
 
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.
241
+ 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
242
 
238
243
  ```typescript
239
244
  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. */