@cstar.help/js 0.17.5 → 0.18.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.
@@ -28,6 +28,11 @@ type ChatAuthMode = 'hmac' | 'session';
28
28
  interface LoginWithSessionParams {
29
29
  /** Supabase access token (JWT). Used as Bearer for every subsequent call. */
30
30
  accessToken: string;
31
+ /**
32
+ * Optional: the matching refresh token. When supplied, the client renews
33
+ * an expired access token through `/api/widget/auth/refresh` on a 401.
34
+ */
35
+ refreshToken?: string;
31
36
  /** Optional: the team UUID. If omitted, derived server-side from the token. */
32
37
  teamId?: string;
33
38
  /** Optional: a hint for the customer name (UI only — server stays authoritative). */
@@ -132,6 +137,8 @@ interface TicketListResponse {
132
137
  /** A message on a ticket. */
133
138
  interface Message {
134
139
  id: string;
140
+ /** Opaque send identity, used to reconcile a reply after an uncertain response. */
141
+ client_request_key?: string | null;
135
142
  content: string;
136
143
  sender: 'customer' | 'agent';
137
144
  sender_name: string;
@@ -144,6 +151,12 @@ interface Message {
144
151
  /** Whether this message was sent by an AI agent. */
145
152
  is_bot?: boolean;
146
153
  }
154
+ /** Options for one logical customer reply, including any retries. */
155
+ interface MessageSendOptions {
156
+ /** Retain for a manual retry of the same ticket, content and attachments. Use a new key for each intentional send. Receipts last 24 hours. */
157
+ idempotencyKey?: string;
158
+ signal?: AbortSignal;
159
+ }
147
160
  /** A confirmed customer's message attachment. Never a Storage path or URL. */
148
161
  interface CustomerAttachmentReference {
149
162
  readonly ticketId: string;
@@ -170,6 +183,7 @@ interface TicketListParams {
170
183
  }
171
184
  /** Parameters for creating a new ticket. */
172
185
  interface TicketCreateParams {
186
+ /** Conversation subject. Subjects over 255 characters are shortened with an ellipsis, never rejected. */
173
187
  subject: string;
174
188
  message?: string;
175
189
  }
@@ -231,6 +245,8 @@ declare class ChatClientError extends Error {
231
245
  readonly code: string;
232
246
  readonly statusCode: number;
233
247
  readonly requestId: string | null;
248
+ /** Customer message send key retained for a manual retry after an uncertain outcome. */
249
+ idempotencyKey?: string;
234
250
  constructor(arg: ChatErrorOpts | string);
235
251
  }
236
252
  /** 401 / 403 — auth failed (bad/expired token, wrong team, etc.). */
@@ -316,6 +332,8 @@ declare class ChatClient {
316
332
  private readonly pollingInterval;
317
333
  private readonly realtimeEnabled;
318
334
  private sessionToken;
335
+ private refreshToken;
336
+ private refreshPromise;
319
337
  private teamId;
320
338
  private customerId;
321
339
  private authMode;
@@ -429,7 +447,14 @@ declare class ChatClient {
429
447
  signal?: AbortSignal;
430
448
  cache?: RequestCache;
431
449
  readBody?: (response: Response) => Promise<T>;
432
- }): Promise<T>;
450
+ idempotencyKey?: string;
451
+ }, retriedAfterRefresh?: boolean): Promise<T>;
452
+ /**
453
+ * Renew the access token with the stored refresh token. Concurrent 401s
454
+ * share one refresh. Resolves true when a newer token is in place.
455
+ */
456
+ private refreshSession;
457
+ private requestRefresh;
433
458
  /**
434
459
  * Look up the team UUID from the configured `teamSlug`. Used by
435
460
  * `loginWithCredentials`. HMAC integrators (`identify()`) never hit
@@ -501,8 +526,11 @@ declare class MessagesApi {
501
526
  constructor(chat: ChatClient);
502
527
  /** List messages for a ticket. */
503
528
  list(ticketId: string): Promise<Message[]>;
504
- /** Send a message to a ticket. */
505
- send(ticketId: string, content: string): Promise<Message>;
529
+ /**
530
+ * Send one logical reply. Network and server failures retry twice with the
531
+ * same key. A separate send call is a new intent unless a retained key is supplied.
532
+ */
533
+ send(ticketId: string, content: string, options?: MessageSendOptions): Promise<Message>;
506
534
  }
507
535
 
508
- export { type AttachmentDownloadOptions, type ChatAuthMode, ChatClient, ChatClientAuthError, ChatClientError, ChatClientNetworkError, ChatClientRateLimitError, ChatClientServerError, type ChatConfig, type ChatStateListener, type CustomerAttachmentReference, type DownloadedAttachment, type IdentifyParams, type IdentifyResult, type LoginWithCredentialsParams, type LoginWithCredentialsResult, type LoginWithSessionParams, type Message, type MessageCallback, type RealtimeTokenResponse, type Ticket, type TicketCreateParams, type TicketListParams, type TicketListResponse, type TypingCallback, type TypingPayload };
536
+ export { type AttachmentDownloadOptions, type ChatAuthMode, ChatClient, ChatClientAuthError, ChatClientError, ChatClientNetworkError, ChatClientRateLimitError, ChatClientServerError, type ChatConfig, type ChatStateListener, type CustomerAttachmentReference, type DownloadedAttachment, type IdentifyParams, type IdentifyResult, type LoginWithCredentialsParams, type LoginWithCredentialsResult, type LoginWithSessionParams, type Message, type MessageCallback, type MessageSendOptions, type RealtimeTokenResponse, type Ticket, type TicketCreateParams, type TicketListParams, type TicketListResponse, type TypingCallback, type TypingPayload };
@@ -28,6 +28,11 @@ type ChatAuthMode = 'hmac' | 'session';
28
28
  interface LoginWithSessionParams {
29
29
  /** Supabase access token (JWT). Used as Bearer for every subsequent call. */
30
30
  accessToken: string;
31
+ /**
32
+ * Optional: the matching refresh token. When supplied, the client renews
33
+ * an expired access token through `/api/widget/auth/refresh` on a 401.
34
+ */
35
+ refreshToken?: string;
31
36
  /** Optional: the team UUID. If omitted, derived server-side from the token. */
32
37
  teamId?: string;
33
38
  /** Optional: a hint for the customer name (UI only — server stays authoritative). */
@@ -132,6 +137,8 @@ interface TicketListResponse {
132
137
  /** A message on a ticket. */
133
138
  interface Message {
134
139
  id: string;
140
+ /** Opaque send identity, used to reconcile a reply after an uncertain response. */
141
+ client_request_key?: string | null;
135
142
  content: string;
136
143
  sender: 'customer' | 'agent';
137
144
  sender_name: string;
@@ -144,6 +151,12 @@ interface Message {
144
151
  /** Whether this message was sent by an AI agent. */
145
152
  is_bot?: boolean;
146
153
  }
154
+ /** Options for one logical customer reply, including any retries. */
155
+ interface MessageSendOptions {
156
+ /** Retain for a manual retry of the same ticket, content and attachments. Use a new key for each intentional send. Receipts last 24 hours. */
157
+ idempotencyKey?: string;
158
+ signal?: AbortSignal;
159
+ }
147
160
  /** A confirmed customer's message attachment. Never a Storage path or URL. */
148
161
  interface CustomerAttachmentReference {
149
162
  readonly ticketId: string;
@@ -170,6 +183,7 @@ interface TicketListParams {
170
183
  }
171
184
  /** Parameters for creating a new ticket. */
172
185
  interface TicketCreateParams {
186
+ /** Conversation subject. Subjects over 255 characters are shortened with an ellipsis, never rejected. */
173
187
  subject: string;
174
188
  message?: string;
175
189
  }
@@ -231,6 +245,8 @@ declare class ChatClientError extends Error {
231
245
  readonly code: string;
232
246
  readonly statusCode: number;
233
247
  readonly requestId: string | null;
248
+ /** Customer message send key retained for a manual retry after an uncertain outcome. */
249
+ idempotencyKey?: string;
234
250
  constructor(arg: ChatErrorOpts | string);
235
251
  }
236
252
  /** 401 / 403 — auth failed (bad/expired token, wrong team, etc.). */
@@ -316,6 +332,8 @@ declare class ChatClient {
316
332
  private readonly pollingInterval;
317
333
  private readonly realtimeEnabled;
318
334
  private sessionToken;
335
+ private refreshToken;
336
+ private refreshPromise;
319
337
  private teamId;
320
338
  private customerId;
321
339
  private authMode;
@@ -429,7 +447,14 @@ declare class ChatClient {
429
447
  signal?: AbortSignal;
430
448
  cache?: RequestCache;
431
449
  readBody?: (response: Response) => Promise<T>;
432
- }): Promise<T>;
450
+ idempotencyKey?: string;
451
+ }, retriedAfterRefresh?: boolean): Promise<T>;
452
+ /**
453
+ * Renew the access token with the stored refresh token. Concurrent 401s
454
+ * share one refresh. Resolves true when a newer token is in place.
455
+ */
456
+ private refreshSession;
457
+ private requestRefresh;
433
458
  /**
434
459
  * Look up the team UUID from the configured `teamSlug`. Used by
435
460
  * `loginWithCredentials`. HMAC integrators (`identify()`) never hit
@@ -501,8 +526,11 @@ declare class MessagesApi {
501
526
  constructor(chat: ChatClient);
502
527
  /** List messages for a ticket. */
503
528
  list(ticketId: string): Promise<Message[]>;
504
- /** Send a message to a ticket. */
505
- send(ticketId: string, content: string): Promise<Message>;
529
+ /**
530
+ * Send one logical reply. Network and server failures retry twice with the
531
+ * same key. A separate send call is a new intent unless a retained key is supplied.
532
+ */
533
+ send(ticketId: string, content: string, options?: MessageSendOptions): Promise<Message>;
506
534
  }
507
535
 
508
- export { type AttachmentDownloadOptions, type ChatAuthMode, ChatClient, ChatClientAuthError, ChatClientError, ChatClientNetworkError, ChatClientRateLimitError, ChatClientServerError, type ChatConfig, type ChatStateListener, type CustomerAttachmentReference, type DownloadedAttachment, type IdentifyParams, type IdentifyResult, type LoginWithCredentialsParams, type LoginWithCredentialsResult, type LoginWithSessionParams, type Message, type MessageCallback, type RealtimeTokenResponse, type Ticket, type TicketCreateParams, type TicketListParams, type TicketListResponse, type TypingCallback, type TypingPayload };
536
+ export { type AttachmentDownloadOptions, type ChatAuthMode, ChatClient, ChatClientAuthError, ChatClientError, ChatClientNetworkError, ChatClientRateLimitError, ChatClientServerError, type ChatConfig, type ChatStateListener, type CustomerAttachmentReference, type DownloadedAttachment, type IdentifyParams, type IdentifyResult, type LoginWithCredentialsParams, type LoginWithCredentialsResult, type LoginWithSessionParams, type Message, type MessageCallback, type MessageSendOptions, type RealtimeTokenResponse, type Ticket, type TicketCreateParams, type TicketListParams, type TicketListResponse, type TypingCallback, type TypingPayload };
@@ -434,6 +434,8 @@ var DEFAULT_POLLING_INTERVAL = 3e3;
434
434
  var ChatClient = class {
435
435
  constructor(config) {
436
436
  this.sessionToken = null;
437
+ this.refreshToken = null;
438
+ this.refreshPromise = null;
437
439
  this.teamId = null;
438
440
  this.customerId = null;
439
441
  this.authMode = null;
@@ -526,6 +528,7 @@ var ChatClient = class {
526
528
  this.applySession(
527
529
  {
528
530
  accessToken: response.accessToken,
531
+ refreshToken: response.refreshToken ?? void 0,
529
532
  teamId,
530
533
  customerName: response.customer.name ?? response.customer.email
531
534
  },
@@ -556,6 +559,7 @@ var ChatClient = class {
556
559
  applySession(params, generation) {
557
560
  this.assertSession(generation);
558
561
  this.sessionToken = params.accessToken;
562
+ this.refreshToken = params.refreshToken ?? null;
559
563
  if (params.teamId) {
560
564
  this.teamId = params.teamId;
561
565
  }
@@ -665,6 +669,8 @@ var ChatClient = class {
665
669
  this.pollingSubscriptions.clear();
666
670
  this.realtimeMessageSubscriptions.clear();
667
671
  this.sessionToken = null;
672
+ this.refreshToken = null;
673
+ this.refreshPromise = null;
668
674
  this.teamId = null;
669
675
  this.customerId = null;
670
676
  this.authMode = null;
@@ -696,7 +702,7 @@ var ChatClient = class {
696
702
  * the first authenticated round-trip, `getAuthMode()` returns the
697
703
  * server-confirmed mode.
698
704
  */
699
- async fetch(path, options) {
705
+ async fetch(path, options, retriedAfterRefresh = false) {
700
706
  const generation = this.sessionGeneration;
701
707
  const assertRequest = () => {
702
708
  this.assertSession(generation);
@@ -707,8 +713,10 @@ var ChatClient = class {
707
713
  const headers = {
708
714
  "Content-Type": "application/json"
709
715
  };
710
- if (this.sessionToken) {
711
- headers["Authorization"] = `Bearer ${this.sessionToken}`;
716
+ if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
717
+ const sentToken = this.sessionToken;
718
+ if (sentToken) {
719
+ headers["Authorization"] = `Bearer ${sentToken}`;
712
720
  }
713
721
  let response;
714
722
  try {
@@ -736,11 +744,17 @@ var ChatClient = class {
736
744
  }
737
745
  assertRequest();
738
746
  const requestId = responseHeaders?.get("X-Request-Id") ?? null;
747
+ if (response.status === 401 && sentToken && this.refreshToken && !retriedAfterRefresh) {
748
+ const refreshed = await this.refreshSession(sentToken);
749
+ assertRequest();
750
+ if (refreshed) return this.fetch(path, options, true);
751
+ }
739
752
  if (!response.ok) {
740
- const errorBody = await response.json().catch(() => ({ error: { code: "UNKNOWN", message: "Request failed" } }));
753
+ const errorBody = await response.json().catch(() => null);
741
754
  assertRequest();
742
- const code = errorBody.error?.code ?? "UNKNOWN";
743
- const message = errorBody.error?.message ?? `Widget API error: ${response.status}`;
755
+ const structured = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : null;
756
+ const code = structured?.code ?? "UNKNOWN";
757
+ const message = structured?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : null) ?? (typeof errorBody?.message === "string" ? errorBody.message : null) ?? `Widget API error: ${response.status}`;
744
758
  if (response.status === 401 || response.status === 403) {
745
759
  throw new ChatClientAuthError({
746
760
  message,
@@ -781,9 +795,52 @@ var ChatClient = class {
781
795
  return body;
782
796
  } catch (error) {
783
797
  assertRequest();
798
+ if (options.idempotencyKey)
799
+ throw new ChatClientNetworkError({
800
+ message: "Could not read the message acknowledgment",
801
+ originalError: error
802
+ });
784
803
  throw error;
785
804
  }
786
805
  }
806
+ /**
807
+ * Renew the access token with the stored refresh token. Concurrent 401s
808
+ * share one refresh. Resolves true when a newer token is in place.
809
+ */
810
+ refreshSession(rejectedToken) {
811
+ if (this.sessionToken && this.sessionToken !== rejectedToken) return Promise.resolve(true);
812
+ if (!this.refreshPromise) {
813
+ const pending = this.requestRefresh().finally(() => {
814
+ if (this.refreshPromise === pending) this.refreshPromise = null;
815
+ });
816
+ this.refreshPromise = pending;
817
+ }
818
+ return this.refreshPromise;
819
+ }
820
+ async requestRefresh() {
821
+ const generation = this.sessionGeneration;
822
+ const refreshToken = this.refreshToken;
823
+ if (!refreshToken) return false;
824
+ try {
825
+ const response = await globalThis.fetch(`${this.baseUrl}/api/widget/auth/refresh`, {
826
+ method: "POST",
827
+ headers: { "Content-Type": "application/json" },
828
+ body: JSON.stringify({ refreshToken })
829
+ });
830
+ if (generation !== this.sessionGeneration) return false;
831
+ if (!response.ok) {
832
+ if (response.status === 401) this.refreshToken = null;
833
+ return false;
834
+ }
835
+ const body = await response.json();
836
+ if (generation !== this.sessionGeneration || !body.accessToken) return false;
837
+ this.sessionToken = body.accessToken;
838
+ this.refreshToken = body.refreshToken ?? refreshToken;
839
+ return true;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
787
844
  /**
788
845
  * Look up the team UUID from the configured `teamSlug`. Used by
789
846
  * `loginWithCredentials`. HMAC integrators (`identify()`) never hit
@@ -1256,26 +1313,80 @@ var MessagesApi = class {
1256
1313
  assertCurrent();
1257
1314
  return response.data.messages;
1258
1315
  }
1259
- /** Send a message to a ticket. */
1260
- async send(ticketId, content) {
1316
+ /**
1317
+ * Send one logical reply. Network and server failures retry twice with the
1318
+ * same key. A separate send call is a new intent unless a retained key is supplied.
1319
+ */
1320
+ async send(ticketId, content, options = {}) {
1261
1321
  const assertCurrent = this.chat.captureSession();
1322
+ options.signal?.throwIfAborted();
1323
+ const key = options.idempotencyKey ?? createMessageRequestKey();
1324
+ if (!key || key.length > 250 || key.trim() !== key || /[^\x20-\x7e]/.test(key))
1325
+ throw new ChatClientError({
1326
+ code: "INVALID_IDEMPOTENCY_KEY",
1327
+ message: "Idempotency-Key must contain 1 to 250 printable characters, without surrounding whitespace."
1328
+ });
1262
1329
  const teamId = await this.chat.ensureTeamId();
1263
1330
  assertCurrent();
1264
- const response = await this.chat.fetch(
1265
- "/api/widget/messages",
1266
- {
1267
- method: "POST",
1268
- body: {
1269
- teamId,
1270
- ticketId,
1271
- content
1272
- }
1331
+ for (let attempt = 0; ; attempt++) {
1332
+ assertCurrent();
1333
+ options.signal?.throwIfAborted();
1334
+ try {
1335
+ const response = await this.chat.fetch(
1336
+ "/api/widget/messages",
1337
+ {
1338
+ method: "POST",
1339
+ body: { teamId, ticketId, content },
1340
+ idempotencyKey: key,
1341
+ signal: options.signal
1342
+ }
1343
+ );
1344
+ assertCurrent();
1345
+ if (typeof response?.data?.message?.id !== "string" || !response.data.message.id)
1346
+ throw new ChatClientNetworkError({
1347
+ message: "Message acknowledgment was incomplete",
1348
+ originalError: new Error(
1349
+ "The reply may already be saved. Retry with the same send identity."
1350
+ )
1351
+ });
1352
+ return response.data.message;
1353
+ } catch (error) {
1354
+ assertCurrent();
1355
+ options.signal?.throwIfAborted();
1356
+ if (error instanceof ChatClientError) error.idempotencyKey = key;
1357
+ if (attempt >= 2 || !(error instanceof ChatClientNetworkError || error instanceof ChatClientServerError))
1358
+ throw error;
1359
+ await waitForMessageRetry(250 * 2 ** attempt, options.signal);
1273
1360
  }
1274
- );
1275
- assertCurrent();
1276
- return response.data.message;
1361
+ }
1277
1362
  }
1278
1363
  };
1364
+ function createMessageRequestKey() {
1365
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
1366
+ if (typeof globalThis.crypto?.getRandomValues !== "function")
1367
+ throw new ChatClientError({
1368
+ code: "RANDOM_UNAVAILABLE",
1369
+ message: "Secure random values are required to send a message safely."
1370
+ });
1371
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
1372
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1373
+ }
1374
+ function waitForMessageRetry(delay, signal) {
1375
+ signal?.throwIfAborted();
1376
+ return new Promise((resolve, reject) => {
1377
+ const finish = () => {
1378
+ signal?.removeEventListener("abort", abort);
1379
+ resolve();
1380
+ };
1381
+ const timer = setTimeout(finish, delay);
1382
+ const abort = () => {
1383
+ clearTimeout(timer);
1384
+ signal?.removeEventListener("abort", abort);
1385
+ reject(signal?.reason);
1386
+ };
1387
+ signal?.addEventListener("abort", abort, { once: true });
1388
+ });
1389
+ }
1279
1390
  function decodeJwtSub(jwt) {
1280
1391
  try {
1281
1392
  const parts = jwt.split(".");
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.17.5";
281
+ var SDK_VERSION = "0.18.0";
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;
@@ -298,7 +298,7 @@ var HttpClient = class {
298
298
  this.lastMeta = { requestId: null, timestamp: null, pagination: null };
299
299
  if (!config || typeof config.apiKey !== "string" || config.apiKey.length === 0) {
300
300
  throw new Error(
301
- "[cStar] CStarClient: apiKey is required. Pass a secret key (sk_live_... / sk_test_...) or a publishable key (pk_live_... / pk_test_...). See https://cstar.help/developers/quickstart"
301
+ "[cStar] CStarClient: apiKey is required. Pass a secret key (sk_live_... / sk_test_...). Publishable keys (pk_...) cannot read team endpoints; use them with LibraryClient or CommunityClient. See https://cstar.help/developers/quickstart"
302
302
  );
303
303
  }
304
304
  if (typeof config.teamId !== "string" || config.teamId.length === 0) {
@@ -530,6 +530,8 @@ var CStarRealtime = class {
530
530
  this.retryTimer = null;
531
531
  this._connected = false;
532
532
  this.destroyed = false;
533
+ /** The server closed the stream because the API key was revoked or expired. */
534
+ this.revoked = false;
533
535
  this.connecting = false;
534
536
  /** Short-lived SSE token (exchanged from secret key). */
535
537
  this.sseToken = null;
@@ -605,7 +607,7 @@ var CStarRealtime = class {
605
607
  }
606
608
  /** Start the SSE connection. Called automatically on first handler registration. */
607
609
  connect() {
608
- if (this.eventSource || this.destroyed || this.connecting) return;
610
+ if (this.eventSource || this.destroyed || this.revoked || this.connecting) return;
609
611
  if (!this.resolveEventSource()) {
610
612
  throw new CStarRealtimeUnavailableError();
611
613
  }
@@ -665,6 +667,7 @@ var CStarRealtime = class {
665
667
  this.eventSource.close();
666
668
  this.eventSource = null;
667
669
  }
670
+ if (this.revoked) return;
668
671
  if (this.autoReconnect && !this.destroyed && this.retryCount < this.maxRetries) {
669
672
  this.scheduleReconnect();
670
673
  } else if (this.retryCount >= this.maxRetries) {
@@ -674,6 +677,9 @@ var CStarRealtime = class {
674
677
  this.eventSource.onmessage = (event) => {
675
678
  this.handleMessage(event);
676
679
  };
680
+ this.eventSource.addEventListener("revoked", (event) => {
681
+ this.handleRevoked(event);
682
+ });
677
683
  const knownTypes = [
678
684
  "ticket.created",
679
685
  "ticket.updated",
@@ -802,6 +808,40 @@ var CStarRealtime = class {
802
808
  for (const handler of this.connectionHandlers) handler(value);
803
809
  }
804
810
  }
811
+ handleRevoked(event) {
812
+ if (this.revoked || this.destroyed) return;
813
+ this.revoked = true;
814
+ let body = {};
815
+ try {
816
+ body = JSON.parse(event.data).error ?? {};
817
+ } catch {
818
+ }
819
+ if (this.retryTimer) {
820
+ clearTimeout(this.retryTimer);
821
+ this.retryTimer = null;
822
+ }
823
+ if (this.tokenRefreshTimer) {
824
+ clearTimeout(this.tokenRefreshTimer);
825
+ this.tokenRefreshTimer = null;
826
+ }
827
+ if (this.eventSource) {
828
+ this.eventSource.close();
829
+ this.eventSource = null;
830
+ }
831
+ this.sseToken = null;
832
+ this.sseTokenExpiresAt = 0;
833
+ this.connecting = false;
834
+ this.setConnected(false);
835
+ this.emitError(
836
+ new CStarAuthenticationError(401, {
837
+ type: body.type ?? "authentication_error",
838
+ code: body.code ?? "invalid_api_key",
839
+ message: body.message ?? "This API key was revoked or has expired, so the stream is closed.",
840
+ doc_url: body.doc_url ?? "https://www.cstar.help/developers/errors#invalid_api_key",
841
+ ...body.request_id ? { request_id: body.request_id } : {}
842
+ })
843
+ );
844
+ }
805
845
  emitError(error) {
806
846
  for (const handler of this.errorHandlers) handler(error);
807
847
  }
@@ -1521,11 +1561,17 @@ var SettingsResource = class {
1521
1561
  async update(params) {
1522
1562
  return this.client.request("PATCH", "/settings", { body: params });
1523
1563
  }
1524
- /** Get business hours configuration. */
1564
+ /**
1565
+ * Get the chat widget's online hours (outside them the widget shows its
1566
+ * offline message). Not the SLA working hours that SLA deadlines use.
1567
+ */
1525
1568
  async getBusinessHours() {
1526
1569
  return this.client.request("GET", "/settings/business-hours");
1527
1570
  }
1528
- /** Update business hours configuration. */
1571
+ /**
1572
+ * Update the chat widget's online hours. SLA deadlines don't change:
1573
+ * they follow the SLA working hours in Settings > Business Hours.
1574
+ */
1529
1575
  async updateBusinessHours(params) {
1530
1576
  return this.client.request("PATCH", "/settings/business-hours", {
1531
1577
  body: params
@@ -1550,8 +1596,10 @@ var NotificationsResource = class {
1550
1596
  }
1551
1597
  /** List notifications for a user. */
1552
1598
  async list(params) {
1599
+ const { unreadOnly, ...query } = params;
1600
+ if (unreadOnly && query.isRead === void 0) query.isRead = false;
1553
1601
  return this.client.request("GET", "/notifications", {
1554
- query: params
1602
+ query
1555
1603
  });
1556
1604
  }
1557
1605
  /** Get unread notification count for a user. */
@@ -1625,15 +1673,22 @@ var AttachmentsResource = class {
1625
1673
  constructor(client) {
1626
1674
  this.client = client;
1627
1675
  }
1628
- /** List attachments. */
1676
+ /** List message files, each with a short-lived signed `url`. */
1629
1677
  async list(params) {
1630
1678
  return this.client.request("GET", "/attachments", {
1631
1679
  query: params
1632
1680
  });
1633
1681
  }
1634
- /** Get a single attachment. */
1635
- async get(attachmentId) {
1636
- return this.client.request("GET", `/attachments/${attachmentId}`);
1682
+ /**
1683
+ * Get a signed download URL, valid for `expiresIn` seconds, for a stored
1684
+ * file. Pass `storagePath` and `bucket` from `list()`.
1685
+ */
1686
+ async get(storagePath, params) {
1687
+ return this.client.request(
1688
+ "GET",
1689
+ `/attachments/${storagePath}`,
1690
+ params?.bucket ? { query: { bucket: params.bucket } } : void 0
1691
+ );
1637
1692
  }
1638
1693
  /** Delete an attachment. */
1639
1694
  async del(attachmentId) {
@@ -1649,16 +1704,12 @@ var CustomFieldsResource = class {
1649
1704
  constructor(client) {
1650
1705
  this.client = client;
1651
1706
  }
1652
- /** List custom fields. */
1707
+ /** List custom field definitions, optionally for one record type. There is no single-field GET. */
1653
1708
  async list(params) {
1654
1709
  return this.client.request("GET", "/custom-fields", {
1655
1710
  query: params
1656
1711
  });
1657
1712
  }
1658
- /** Get a single custom field. */
1659
- async get(fieldId) {
1660
- return this.client.request("GET", `/custom-fields/${fieldId}`);
1661
- }
1662
1713
  /** Create a custom field. */
1663
1714
  async create(params, options) {
1664
1715
  return this.client.request("POST", "/custom-fields", {
@@ -1693,11 +1744,9 @@ var SlaResource = class {
1693
1744
  constructor(client) {
1694
1745
  this.client = client;
1695
1746
  }
1696
- /** List SLA rules. */
1697
- async list(params) {
1698
- return this.client.request("GET", "/sla/rules", {
1699
- query: params
1700
- });
1747
+ /** List SLA rules in evaluation order. */
1748
+ async list() {
1749
+ return this.client.request("GET", "/sla/rules");
1701
1750
  }
1702
1751
  /** Create an SLA rule. */
1703
1752
  async create(params, options) {
@@ -1886,25 +1935,28 @@ var TagsResource = class {
1886
1935
  constructor(client) {
1887
1936
  this.client = client;
1888
1937
  }
1889
- /** List all tags with usage counts. */
1938
+ /** List tags with usage counts, one row per tag and entity type. */
1890
1939
  async list(params) {
1891
1940
  return this.client.request("GET", "/tags", {
1892
1941
  query: params
1893
1942
  });
1894
1943
  }
1895
- /** Create a tag. */
1896
- async create(name, options) {
1944
+ /** Replace `fromTag` with `toTag` on every record that carries it. */
1945
+ async merge(params, options) {
1897
1946
  return this.client.request("POST", "/tags", {
1898
- body: { name },
1947
+ body: { action: "merge", ...params },
1899
1948
  idempotencyKey: options?.idempotencyKey
1900
1949
  });
1901
1950
  }
1902
- /** Delete a tag. */
1903
- async del(name) {
1904
- return this.client.request(
1905
- "DELETE",
1906
- `/tags/${encodeURIComponent(name)}`
1907
- );
1951
+ /** Remove a tag from every record that carries it. */
1952
+ async remove(tag, params) {
1953
+ return this.client.request("DELETE", "/tags", {
1954
+ body: { tag, ...params }
1955
+ });
1956
+ }
1957
+ /** @deprecated Use `remove()`. */
1958
+ async del(tag, params) {
1959
+ return this.remove(tag, params);
1908
1960
  }
1909
1961
  };
1910
1962
 
@@ -2350,7 +2402,7 @@ var ViewsResource = class extends BaseResource {
2350
2402
  constructor(client) {
2351
2403
  super(client, "/views");
2352
2404
  }
2353
- /** Execute a view — returns the filtered results. The server uses the view's entity_type to determine which table to query. */
2405
+ /** Execute a view and return the filtered results. The server uses the view's entity_type to determine which table to query. */
2354
2406
  async execute(viewId, params) {
2355
2407
  return this.client.request("GET", `/views/${viewId}/execute`, {
2356
2408
  query: params
@@ -2586,7 +2638,7 @@ var WEBHOOK_EVENT_SAMPLES = {
2586
2638
  "comment": {
2587
2639
  "id": "cmt_01H8X9YZ7E0J3K4M5N6X",
2588
2640
  "post_id": "post_01H8X9YZ7E0J3K4M5N6W",
2589
- "content": "I would love this \u2014 bumping for visibility.",
2641
+ "content": "I would love this. Bumping for visibility.",
2590
2642
  "author_id": "cus_01H8X9YZ7E0J3K4M5N6Q",
2591
2643
  "created_at": "2026-04-27T18:55:02.000Z"
2592
2644
  }
@@ -2806,15 +2858,14 @@ var WEBHOOK_EVENT_SAMPLES = {
2806
2858
  "id": "tkt_01H8X9YZ7E0J3K4M5N6P",
2807
2859
  "prefixed_id": "CST-1024",
2808
2860
  "title": "Cannot reset password",
2809
- "status": "resolved",
2861
+ "status": "closed",
2810
2862
  "priority": "normal",
2811
- "channel": "widget",
2812
- "resolved_at": "2026-04-27T19:08:43.000Z"
2863
+ "channel": "widget"
2813
2864
  },
2814
2865
  "changes": {
2815
2866
  "status": {
2816
2867
  "from": "open",
2817
- "to": "resolved"
2868
+ "to": "closed"
2818
2869
  }
2819
2870
  }
2820
2871
  },
@@ -2847,7 +2898,7 @@ var WEBHOOK_EVENT_SAMPLES = {
2847
2898
  "id": "msg_01H8X9YZ7E0J3K4M5N6R",
2848
2899
  "sender": "agent",
2849
2900
  "sender_name": "Riley",
2850
- "content": "I just sent a reset link to your inbox \u2014 let me know if it lands.",
2901
+ "content": "I just sent a reset link to your inbox. Let me know if it lands.",
2851
2902
  "created_at": "2026-04-27T18:55:02.000Z"
2852
2903
  },
2853
2904
  "customer": {
@@ -2913,7 +2964,7 @@ var WEBHOOK_EVENT_CATALOG = {
2913
2964
  "survey.submitted": { label: "Customer Feedback Received", description: "Fired when a customer submits a CSAT survey response", category: "csat" },
2914
2965
  "team.preferences_updated": { label: "Team Preferences Updated", description: "Fired when team-wide UX preferences change (e.g., bossBattlesEnabled toggled)", category: "team" },
2915
2966
  "ticket.assigned": { label: "Ticket Assigned", description: "Fired when a ticket is assigned to an agent", category: "tickets" },
2916
- "ticket.closed": { label: "Ticket Closed/Resolved", description: "Fired when a ticket is resolved or closed", category: "tickets" },
2967
+ "ticket.closed": { label: "Ticket Closed", description: "Fired when a ticket's status changes to Closed. Moving a ticket to Resolved sends ticket.updated instead.", category: "tickets" },
2917
2968
  "ticket.created": { label: "New Ticket Created", description: "Fired when a new ticket is created via app, widget, API, or email", category: "tickets" },
2918
2969
  "ticket.deleted": { label: "Ticket Deleted", description: "Fired when a ticket is deleted via app or API", category: "tickets" },
2919
2970
  "ticket.message_added": { label: "New Message on Ticket", description: "Fired when a new message is added to a ticket", category: "tickets" },