@cstar.help/js 0.18.0 → 0.18.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
@@ -480,6 +480,22 @@ const ticket = await cstar.tickets.create(
480
480
  );
481
481
  ```
482
482
 
483
+ For 24 hours, the same request with the same key returns the first successful
484
+ response. The same key with a different body throws `CStarValidationError`
485
+ (`idempotency_key_reused`, 422). While the first request is still running, the
486
+ API answers `409 idempotency_key_in_use` with `Retry-After`. From 0.18.1, the client waits and retries within `maxRetries`; 0.18.0 throws
487
+ `CStarConflictError`.
488
+
489
+ The API ignores the key on `webhooks.create`, `webhooks.listen` and
490
+ `export.create`, so a key does not make their retries safe. From 0.18.2 the
491
+ client does not send the key on those calls and does not retry a network error
492
+ or 5xx; 0.18.1 and earlier still retry them, so there leave the key off (without
493
+ one the client does not retry them) or use a client with `maxRetries: 0`. Check
494
+ `webhooks.list()` before you retry `webhooks.create`. A repeated `export.create`
495
+ only reads your data again (nothing is stored). A repeated `webhooks.listen` is a
496
+ second CLI listener: remove it with `webhooks.unlisten(id)`, or it is swept after
497
+ 5 minutes without a heartbeat.
498
+
483
499
  ## Requirements
484
500
 
485
501
  - Node.js 22+ (matching the supported Supabase dependency)
@@ -222,6 +222,7 @@ var RealtimeManager = class {
222
222
  // src/chat/presence.ts
223
223
  var TYPING_THROTTLE_MS = 2e3;
224
224
  var TYPING_CLEAR_DELAY_MS = 3e3;
225
+ var PRIVATE_TOPIC = { config: { private: true } };
225
226
  var PresenceManager = class {
226
227
  constructor(options) {
227
228
  this.client = null;
@@ -245,7 +246,7 @@ var PresenceManager = class {
245
246
  };
246
247
  const channelName = `typing:${ticketId}`;
247
248
  this.removeChannel(channelName);
248
- const channel = this.client.channel(`ticket:${ticketId}`);
249
+ const channel = this.client.channel(`ticket:${ticketId}`, PRIVATE_TOPIC);
249
250
  this.channels.set(channelName, channel);
250
251
  channel.on("broadcast", { event: "typing" }, (data) => {
251
252
  if (this.channels.get(channelName) !== channel) return;
@@ -297,7 +298,7 @@ var PresenceManager = class {
297
298
  const sendChannelName = `typing-send:${ticketId}`;
298
299
  let channel = this.channels.get(sendChannelName);
299
300
  if (!channel) {
300
- channel = this.client.channel(`ticket:${ticketId}`);
301
+ channel = this.client.channel(`ticket:${ticketId}`, PRIVATE_TOPIC);
301
302
  channel.subscribe();
302
303
  this.channels.set(sendChannelName, channel);
303
304
  }
@@ -1,4 +1,4 @@
1
- import { C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-DF7GprCV.cjs';
1
+ import { C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-BHCahwev.cjs';
2
2
 
3
3
  /** Configuration for the ChatClient. */
4
4
  interface ChatConfig {
@@ -1,4 +1,4 @@
1
- import { C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-DF7GprCV.js';
1
+ import { C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-BHCahwev.js';
2
2
 
3
3
  /** Configuration for the ChatClient. */
4
4
  interface ChatConfig {
@@ -181,6 +181,7 @@ var RealtimeManager = class {
181
181
  // src/chat/presence.ts
182
182
  var TYPING_THROTTLE_MS = 2e3;
183
183
  var TYPING_CLEAR_DELAY_MS = 3e3;
184
+ var PRIVATE_TOPIC = { config: { private: true } };
184
185
  var PresenceManager = class {
185
186
  constructor(options) {
186
187
  this.client = null;
@@ -204,7 +205,7 @@ var PresenceManager = class {
204
205
  };
205
206
  const channelName = `typing:${ticketId}`;
206
207
  this.removeChannel(channelName);
207
- const channel = this.client.channel(`ticket:${ticketId}`);
208
+ const channel = this.client.channel(`ticket:${ticketId}`, PRIVATE_TOPIC);
208
209
  this.channels.set(channelName, channel);
209
210
  channel.on("broadcast", { event: "typing" }, (data) => {
210
211
  if (this.channels.get(channelName) !== channel) return;
@@ -256,7 +257,7 @@ var PresenceManager = class {
256
257
  const sendChannelName = `typing-send:${ticketId}`;
257
258
  let channel = this.channels.get(sendChannelName);
258
259
  if (!channel) {
259
- channel = this.client.channel(`ticket:${ticketId}`);
260
+ channel = this.client.channel(`ticket:${ticketId}`, PRIVATE_TOPIC);
260
261
  channel.subscribe();
261
262
  this.channels.set(sendChannelName, channel);
262
263
  }
@@ -1,5 +1,5 @@
1
- import { h as CommunityConfig, i as CommunityTopic, j as CommunityPostListParams, k as CommunityPost, l as CommunityPostDetail, m as CommunitySearchResult, C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-DF7GprCV.cjs';
2
- export { n as CommunityComment } from '../types-DF7GprCV.cjs';
1
+ import { h as CommunityConfig, i as CommunityTopic, j as CommunityPostListParams, k as CommunityPost, l as CommunityPostDetail, m as CommunitySearchResult, C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-BHCahwev.cjs';
2
+ export { n as CommunityComment } from '../types-BHCahwev.cjs';
3
3
  export { A as ApiErrorBody, C as CStarAuthenticationError, a as CStarConflictError, b as CStarError, c as CStarFeatureDisabledError, d as CStarNotFoundError, e as CStarPermissionError, f as CStarRateLimitError, g as CStarRealtimeUnavailableError, h as CStarServerError, i as CStarValidationError, E as ErrorType, p as parseError, j as parsePublicError } from '../errors-bW8CwTn4.cjs';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { h as CommunityConfig, i as CommunityTopic, j as CommunityPostListParams, k as CommunityPost, l as CommunityPostDetail, m as CommunitySearchResult, C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-DF7GprCV.js';
2
- export { n as CommunityComment } from '../types-DF7GprCV.js';
1
+ import { h as CommunityConfig, i as CommunityTopic, j as CommunityPostListParams, k as CommunityPost, l as CommunityPostDetail, m as CommunitySearchResult, C as CommunityCreatePostParams, a as CommunityCreatedPost, b as CommunityUpdatePostParams, c as CommunityUpdateCommentParams, d as CommunityCreatedComment, e as CommunityVoteResult, f as CommunityCreateCommentParams, g as CommunityFollowResult } from '../types-BHCahwev.js';
2
+ export { n as CommunityComment } from '../types-BHCahwev.js';
3
3
  export { A as ApiErrorBody, C as CStarAuthenticationError, a as CStarConflictError, b as CStarError, c as CStarFeatureDisabledError, d as CStarNotFoundError, e as CStarPermissionError, f as CStarRateLimitError, g as CStarRealtimeUnavailableError, h as CStarServerError, i as CStarValidationError, E as ErrorType, p as parseError, j as parsePublicError } from '../errors-bW8CwTn4.js';
4
4
 
5
5
  /**
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.18.0";
281
+ var SDK_VERSION = "0.18.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;
@@ -357,10 +357,12 @@ var HttpClient = class {
357
357
  const body = options?.body ? JSON.stringify(options.body) : void 0;
358
358
  const canRetryAmbiguousFailure = !["POST", "PATCH"].includes(method.toUpperCase()) || !!options?.idempotencyKey;
359
359
  let lastError = null;
360
+ const RATE_LIMIT_RETRY_CAP_MS = 6e4;
361
+ let inProgressWaitMs = null;
360
362
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
361
363
  if (attempt > 0 && lastError) {
362
- const RATE_LIMIT_RETRY_CAP_MS = 6e4;
363
- const delay = lastError instanceof CStarRateLimitError ? Math.min(lastError.retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS) : this.getBackoffDelay(attempt);
364
+ const delay = inProgressWaitMs ?? (lastError instanceof CStarRateLimitError ? Math.min(lastError.retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS) : this.getBackoffDelay(attempt));
365
+ inProgressWaitMs = null;
364
366
  await this.sleep(delay);
365
367
  }
366
368
  let response;
@@ -400,6 +402,14 @@ var HttpClient = class {
400
402
  if (requestId && errorBody?.error) {
401
403
  errorBody.error.request_id = requestId;
402
404
  }
405
+ if (response.status === 409 && options?.idempotencyKey && errorBody?.error?.code === "idempotency_key_in_use" && retryAfter !== void 0) {
406
+ lastError = parseError(409, errorBody, retryAfter);
407
+ if (attempt < this.config.maxRetries) {
408
+ inProgressWaitMs = Math.min(retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS);
409
+ continue;
410
+ }
411
+ throw lastError;
412
+ }
403
413
  if (response.status < 500 && response.status !== 429) {
404
414
  throw parseError(response.status, errorBody, retryAfter);
405
415
  }
@@ -1242,6 +1252,15 @@ var WebhooksResource = class extends BaseResource {
1242
1252
  constructor(client) {
1243
1253
  super(client, "/webhooks");
1244
1254
  }
1255
+ /**
1256
+ * Create a webhook endpoint. Its answer carries the signing secret, so the
1257
+ * API keeps it out of the Idempotency-Key cache: an `idempotencyKey` is
1258
+ * not sent, and a network error or 5xx is not retried, since the webhook
1259
+ * may already exist. List webhooks before creating it again.
1260
+ */
1261
+ async create(params, _options) {
1262
+ return this.client.request("POST", this.basePath, { body: params });
1263
+ }
1245
1264
  /** Send a test delivery to a webhook endpoint. */
1246
1265
  async test(webhookId, params) {
1247
1266
  return this.client.request(
@@ -1276,11 +1295,14 @@ var WebhooksResource = class extends BaseResource {
1276
1295
  async unsubscribe(subscriptionId) {
1277
1296
  await this.client.request("DELETE", `/webhooks/subscribe/${subscriptionId}`);
1278
1297
  }
1279
- /** Register a CLI listener (returns listenerId and signing secret). */
1280
- async listen(params, options) {
1298
+ /**
1299
+ * Register a CLI listener (returns listenerId and signing secret). The API
1300
+ * ignores Idempotency-Key here (the answer is a secret), so an
1301
+ * `idempotencyKey` is not sent and a network error or 5xx is not retried.
1302
+ */
1303
+ async listen(params, _options) {
1281
1304
  return this.client.request("POST", "/webhooks/listen", {
1282
- body: params,
1283
- idempotencyKey: options?.idempotencyKey
1305
+ body: params
1284
1306
  });
1285
1307
  }
1286
1308
  /** Deregister a CLI listener. */
@@ -1868,10 +1890,9 @@ var ExportResource = class {
1868
1890
  constructor(client) {
1869
1891
  this.client = client;
1870
1892
  }
1871
- async create(params, options) {
1893
+ async create(params, _options) {
1872
1894
  return this.client.request("POST", "/export", {
1873
1895
  body: params,
1874
- idempotencyKey: options?.idempotencyKey,
1875
1896
  responseType: params.format === "csv" ? "text" : "json"
1876
1897
  });
1877
1898
  }
package/dist/index.d.cts CHANGED
@@ -772,6 +772,13 @@ interface WebhookTriggerTestResult {
772
772
  /** Manage webhook endpoints. */
773
773
  declare class WebhooksResource extends BaseResource<Webhook, WebhookListParams, WebhookCreateParams, WebhookUpdateParams> {
774
774
  constructor(client: HttpClient);
775
+ /**
776
+ * Create a webhook endpoint. Its answer carries the signing secret, so the
777
+ * API keeps it out of the Idempotency-Key cache: an `idempotencyKey` is
778
+ * not sent, and a network error or 5xx is not retried, since the webhook
779
+ * may already exist. List webhooks before creating it again.
780
+ */
781
+ create(params: WebhookCreateParams, _options?: RequestOptions): Promise<Webhook>;
775
782
  /** Send a test delivery to a webhook endpoint. */
776
783
  test(webhookId: string, params?: {
777
784
  event?: WebhookEventType;
@@ -784,8 +791,12 @@ declare class WebhooksResource extends BaseResource<Webhook, WebhookListParams,
784
791
  restSubscribe(params: WebhookSubscribeParams, options?: RequestOptions): Promise<ApiResponse<WebhookSubscription>>;
785
792
  /** Unsubscribe from an event (REST Hooks / Zapier protocol). Returns void (204 No Content). */
786
793
  unsubscribe(subscriptionId: string): Promise<void>;
787
- /** Register a CLI listener (returns listenerId and signing secret). */
788
- listen(params: WebhookListenerParams, options?: RequestOptions): Promise<ApiResponse<WebhookListener>>;
794
+ /**
795
+ * Register a CLI listener (returns listenerId and signing secret). The API
796
+ * ignores Idempotency-Key here (the answer is a secret), so an
797
+ * `idempotencyKey` is not sent and a network error or 5xx is not retried.
798
+ */
799
+ listen(params: WebhookListenerParams, _options?: RequestOptions): Promise<ApiResponse<WebhookListener>>;
789
800
  /** Deregister a CLI listener. */
790
801
  unlisten(listenerId: string): Promise<ApiResponse<{
791
802
  deleted: true;
@@ -1581,6 +1592,7 @@ interface Mention {
1581
1592
  senderName: string | null;
1582
1593
  recipientId: string;
1583
1594
  recipientName: string | null;
1595
+ /** The ticket's prefixed id (`tkt_*`), or null for a mention with no ticket. */
1584
1596
  ticketId: string | null;
1585
1597
  ticketTitle: string | null;
1586
1598
  isRead: boolean;
@@ -1674,6 +1686,11 @@ declare class AttachmentsResource {
1674
1686
  type CustomFieldContentType = 'ticket' | 'customer' | 'article';
1675
1687
  /** Input type of a custom field. */
1676
1688
  type CustomFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox';
1689
+ /** One choice of a `select` field. A record stores its `value`. */
1690
+ interface CustomFieldOption {
1691
+ value: string;
1692
+ label: string;
1693
+ }
1677
1694
  /** A custom field definition (mirrors `mapFieldToApi` on the server). */
1678
1695
  interface CustomField {
1679
1696
  id: string;
@@ -1683,7 +1700,7 @@ interface CustomField {
1683
1700
  fieldType: CustomFieldType;
1684
1701
  defaultValue: string | null;
1685
1702
  /** Choices for `select` fields. */
1686
- options: string[] | null;
1703
+ options: CustomFieldOption[] | null;
1687
1704
  minValue: number | null;
1688
1705
  maxValue: number | null;
1689
1706
  required: boolean;
@@ -1702,7 +1719,11 @@ interface CustomFieldCreateParams {
1702
1719
  /** Defaults to `text`. */
1703
1720
  fieldType?: CustomFieldType;
1704
1721
  defaultValue?: string;
1705
- options?: string[];
1722
+ /** Choices for `select` fields. A string is its own label. */
1723
+ options?: Array<string | {
1724
+ value: string;
1725
+ label?: string;
1726
+ }>;
1706
1727
  minValue?: number;
1707
1728
  maxValue?: number;
1708
1729
  required?: boolean;
@@ -1712,7 +1733,11 @@ interface CustomFieldUpdateParams {
1712
1733
  label?: string;
1713
1734
  fieldType?: CustomFieldType;
1714
1735
  defaultValue?: string | null;
1715
- options?: string[];
1736
+ /** Replaces the choices. A string is its own label. */
1737
+ options?: Array<string | {
1738
+ value: string;
1739
+ label?: string;
1740
+ }>;
1716
1741
  minValue?: number | null;
1717
1742
  maxValue?: number | null;
1718
1743
  required?: boolean;
@@ -2032,17 +2057,25 @@ interface ImportCreateParams {
2032
2057
  * A truthy `source.body` paired with an array at `conversation_parts` or
2033
2058
  * `conversation_parts.conversation_parts` is also rejected, even if that
2034
2059
  * array is empty. Empty history without this initial-message shape,
2035
- * customFields/metadata and wrapped records with `action: 'skip'` are allowed.
2060
+ * customFields and wrapped records with `action: 'skip'` are allowed (empty
2061
+ * history and `metadata` are ignored with a row warning). camelCase field
2062
+ * names (`customerEmail`, `externalId`) work; any field the import doesn't
2063
+ * read is named in `warnings`.
2036
2064
  */
2037
2065
  records: Record<string, unknown>[];
2038
- /** How to handle duplicates. Defaults to `'overwrite'`. */
2066
+ /**
2067
+ * What a customer row does when the team already has a customer with its
2068
+ * email: `'skip'` (the default) leaves that customer alone and reports the
2069
+ * row in `warnings`, `'overwrite'` updates it, `'merge'` fills only its
2070
+ * empty fields. A row repeating an earlier row's email is skipped.
2071
+ */
2039
2072
  mergeStrategy?: 'skip' | 'overwrite' | 'merge';
2040
2073
  }
2041
2074
  /** Import result. */
2042
2075
  interface ImportResult {
2043
2076
  object: 'import_result';
2044
- /** ID of the persisted import-log row (null if the log row could not be written). */
2045
- logId: string | null;
2077
+ /** ID of the import's log row (GET /import/logs/{logId}, `import.getLog()`). */
2078
+ logId: string;
2046
2079
  /** Entity type imported — `ticket`, `customer`, or `article`. */
2047
2080
  entityType: string;
2048
2081
  /** True when every record imported without error. */
@@ -2066,21 +2099,35 @@ interface ImportResult {
2066
2099
  interface ImportLog {
2067
2100
  id: string;
2068
2101
  object: 'import_log';
2069
- resource: string;
2070
- status: string;
2102
+ /** `ticket`, `customer` or `article`. */
2103
+ entityType: string;
2104
+ /** `api` for REST imports, or the platform the dashboard importer read. */
2105
+ sourcePlatform: string | null;
2106
+ sourceFilename: string | null;
2071
2107
  totalRecords: number;
2072
2108
  created: number;
2073
2109
  updated: number;
2074
2110
  skipped: number;
2075
- errors: number;
2111
+ failed: number;
2112
+ /** The failed rows, as "Row N: ..." lines, or null. */
2113
+ errors: string[] | null;
2114
+ matchKey: string | null;
2115
+ mergeStrategy: Record<string, string> | null;
2116
+ /** When the import finished; null while it runs. */
2117
+ completedAt: string | null;
2076
2118
  createdAt: string;
2077
2119
  }
2078
2120
  /** Parameters for listing import logs. */
2079
2121
  interface ImportLogListParams extends PaginationParams {
2080
- resource?: string;
2122
+ /** Only imports of this kind. */
2123
+ resource?: 'tickets' | 'customers' | 'articles';
2081
2124
  }
2082
2125
 
2083
- /** Export data from your team. */
2126
+ /**
2127
+ * Export data from your team. The API ignores Idempotency-Key on exports, so
2128
+ * `options.idempotencyKey` is accepted but not sent, and an export that
2129
+ * fails with a network error or 5xx is not retried.
2130
+ */
2084
2131
  declare class ExportResource {
2085
2132
  private readonly client;
2086
2133
  constructor(client: HttpClient);
package/dist/index.d.ts CHANGED
@@ -772,6 +772,13 @@ interface WebhookTriggerTestResult {
772
772
  /** Manage webhook endpoints. */
773
773
  declare class WebhooksResource extends BaseResource<Webhook, WebhookListParams, WebhookCreateParams, WebhookUpdateParams> {
774
774
  constructor(client: HttpClient);
775
+ /**
776
+ * Create a webhook endpoint. Its answer carries the signing secret, so the
777
+ * API keeps it out of the Idempotency-Key cache: an `idempotencyKey` is
778
+ * not sent, and a network error or 5xx is not retried, since the webhook
779
+ * may already exist. List webhooks before creating it again.
780
+ */
781
+ create(params: WebhookCreateParams, _options?: RequestOptions): Promise<Webhook>;
775
782
  /** Send a test delivery to a webhook endpoint. */
776
783
  test(webhookId: string, params?: {
777
784
  event?: WebhookEventType;
@@ -784,8 +791,12 @@ declare class WebhooksResource extends BaseResource<Webhook, WebhookListParams,
784
791
  restSubscribe(params: WebhookSubscribeParams, options?: RequestOptions): Promise<ApiResponse<WebhookSubscription>>;
785
792
  /** Unsubscribe from an event (REST Hooks / Zapier protocol). Returns void (204 No Content). */
786
793
  unsubscribe(subscriptionId: string): Promise<void>;
787
- /** Register a CLI listener (returns listenerId and signing secret). */
788
- listen(params: WebhookListenerParams, options?: RequestOptions): Promise<ApiResponse<WebhookListener>>;
794
+ /**
795
+ * Register a CLI listener (returns listenerId and signing secret). The API
796
+ * ignores Idempotency-Key here (the answer is a secret), so an
797
+ * `idempotencyKey` is not sent and a network error or 5xx is not retried.
798
+ */
799
+ listen(params: WebhookListenerParams, _options?: RequestOptions): Promise<ApiResponse<WebhookListener>>;
789
800
  /** Deregister a CLI listener. */
790
801
  unlisten(listenerId: string): Promise<ApiResponse<{
791
802
  deleted: true;
@@ -1581,6 +1592,7 @@ interface Mention {
1581
1592
  senderName: string | null;
1582
1593
  recipientId: string;
1583
1594
  recipientName: string | null;
1595
+ /** The ticket's prefixed id (`tkt_*`), or null for a mention with no ticket. */
1584
1596
  ticketId: string | null;
1585
1597
  ticketTitle: string | null;
1586
1598
  isRead: boolean;
@@ -1674,6 +1686,11 @@ declare class AttachmentsResource {
1674
1686
  type CustomFieldContentType = 'ticket' | 'customer' | 'article';
1675
1687
  /** Input type of a custom field. */
1676
1688
  type CustomFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox';
1689
+ /** One choice of a `select` field. A record stores its `value`. */
1690
+ interface CustomFieldOption {
1691
+ value: string;
1692
+ label: string;
1693
+ }
1677
1694
  /** A custom field definition (mirrors `mapFieldToApi` on the server). */
1678
1695
  interface CustomField {
1679
1696
  id: string;
@@ -1683,7 +1700,7 @@ interface CustomField {
1683
1700
  fieldType: CustomFieldType;
1684
1701
  defaultValue: string | null;
1685
1702
  /** Choices for `select` fields. */
1686
- options: string[] | null;
1703
+ options: CustomFieldOption[] | null;
1687
1704
  minValue: number | null;
1688
1705
  maxValue: number | null;
1689
1706
  required: boolean;
@@ -1702,7 +1719,11 @@ interface CustomFieldCreateParams {
1702
1719
  /** Defaults to `text`. */
1703
1720
  fieldType?: CustomFieldType;
1704
1721
  defaultValue?: string;
1705
- options?: string[];
1722
+ /** Choices for `select` fields. A string is its own label. */
1723
+ options?: Array<string | {
1724
+ value: string;
1725
+ label?: string;
1726
+ }>;
1706
1727
  minValue?: number;
1707
1728
  maxValue?: number;
1708
1729
  required?: boolean;
@@ -1712,7 +1733,11 @@ interface CustomFieldUpdateParams {
1712
1733
  label?: string;
1713
1734
  fieldType?: CustomFieldType;
1714
1735
  defaultValue?: string | null;
1715
- options?: string[];
1736
+ /** Replaces the choices. A string is its own label. */
1737
+ options?: Array<string | {
1738
+ value: string;
1739
+ label?: string;
1740
+ }>;
1716
1741
  minValue?: number | null;
1717
1742
  maxValue?: number | null;
1718
1743
  required?: boolean;
@@ -2032,17 +2057,25 @@ interface ImportCreateParams {
2032
2057
  * A truthy `source.body` paired with an array at `conversation_parts` or
2033
2058
  * `conversation_parts.conversation_parts` is also rejected, even if that
2034
2059
  * array is empty. Empty history without this initial-message shape,
2035
- * customFields/metadata and wrapped records with `action: 'skip'` are allowed.
2060
+ * customFields and wrapped records with `action: 'skip'` are allowed (empty
2061
+ * history and `metadata` are ignored with a row warning). camelCase field
2062
+ * names (`customerEmail`, `externalId`) work; any field the import doesn't
2063
+ * read is named in `warnings`.
2036
2064
  */
2037
2065
  records: Record<string, unknown>[];
2038
- /** How to handle duplicates. Defaults to `'overwrite'`. */
2066
+ /**
2067
+ * What a customer row does when the team already has a customer with its
2068
+ * email: `'skip'` (the default) leaves that customer alone and reports the
2069
+ * row in `warnings`, `'overwrite'` updates it, `'merge'` fills only its
2070
+ * empty fields. A row repeating an earlier row's email is skipped.
2071
+ */
2039
2072
  mergeStrategy?: 'skip' | 'overwrite' | 'merge';
2040
2073
  }
2041
2074
  /** Import result. */
2042
2075
  interface ImportResult {
2043
2076
  object: 'import_result';
2044
- /** ID of the persisted import-log row (null if the log row could not be written). */
2045
- logId: string | null;
2077
+ /** ID of the import's log row (GET /import/logs/{logId}, `import.getLog()`). */
2078
+ logId: string;
2046
2079
  /** Entity type imported — `ticket`, `customer`, or `article`. */
2047
2080
  entityType: string;
2048
2081
  /** True when every record imported without error. */
@@ -2066,21 +2099,35 @@ interface ImportResult {
2066
2099
  interface ImportLog {
2067
2100
  id: string;
2068
2101
  object: 'import_log';
2069
- resource: string;
2070
- status: string;
2102
+ /** `ticket`, `customer` or `article`. */
2103
+ entityType: string;
2104
+ /** `api` for REST imports, or the platform the dashboard importer read. */
2105
+ sourcePlatform: string | null;
2106
+ sourceFilename: string | null;
2071
2107
  totalRecords: number;
2072
2108
  created: number;
2073
2109
  updated: number;
2074
2110
  skipped: number;
2075
- errors: number;
2111
+ failed: number;
2112
+ /** The failed rows, as "Row N: ..." lines, or null. */
2113
+ errors: string[] | null;
2114
+ matchKey: string | null;
2115
+ mergeStrategy: Record<string, string> | null;
2116
+ /** When the import finished; null while it runs. */
2117
+ completedAt: string | null;
2076
2118
  createdAt: string;
2077
2119
  }
2078
2120
  /** Parameters for listing import logs. */
2079
2121
  interface ImportLogListParams extends PaginationParams {
2080
- resource?: string;
2122
+ /** Only imports of this kind. */
2123
+ resource?: 'tickets' | 'customers' | 'articles';
2081
2124
  }
2082
2125
 
2083
- /** Export data from your team. */
2126
+ /**
2127
+ * Export data from your team. The API ignores Idempotency-Key on exports, so
2128
+ * `options.idempotencyKey` is accepted but not sent, and an export that
2129
+ * fails with a network error or 5xx is not retried.
2130
+ */
2084
2131
  declare class ExportResource {
2085
2132
  private readonly client;
2086
2133
  constructor(client: HttpClient);
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.18.0";
240
+ var SDK_VERSION = "0.18.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;
@@ -316,10 +316,12 @@ var HttpClient = class {
316
316
  const body = options?.body ? JSON.stringify(options.body) : void 0;
317
317
  const canRetryAmbiguousFailure = !["POST", "PATCH"].includes(method.toUpperCase()) || !!options?.idempotencyKey;
318
318
  let lastError = null;
319
+ const RATE_LIMIT_RETRY_CAP_MS = 6e4;
320
+ let inProgressWaitMs = null;
319
321
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
320
322
  if (attempt > 0 && lastError) {
321
- const RATE_LIMIT_RETRY_CAP_MS = 6e4;
322
- const delay = lastError instanceof CStarRateLimitError ? Math.min(lastError.retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS) : this.getBackoffDelay(attempt);
323
+ const delay = inProgressWaitMs ?? (lastError instanceof CStarRateLimitError ? Math.min(lastError.retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS) : this.getBackoffDelay(attempt));
324
+ inProgressWaitMs = null;
323
325
  await this.sleep(delay);
324
326
  }
325
327
  let response;
@@ -359,6 +361,14 @@ var HttpClient = class {
359
361
  if (requestId && errorBody?.error) {
360
362
  errorBody.error.request_id = requestId;
361
363
  }
364
+ if (response.status === 409 && options?.idempotencyKey && errorBody?.error?.code === "idempotency_key_in_use" && retryAfter !== void 0) {
365
+ lastError = parseError(409, errorBody, retryAfter);
366
+ if (attempt < this.config.maxRetries) {
367
+ inProgressWaitMs = Math.min(retryAfter * 1e3, RATE_LIMIT_RETRY_CAP_MS);
368
+ continue;
369
+ }
370
+ throw lastError;
371
+ }
362
372
  if (response.status < 500 && response.status !== 429) {
363
373
  throw parseError(response.status, errorBody, retryAfter);
364
374
  }
@@ -1201,6 +1211,15 @@ var WebhooksResource = class extends BaseResource {
1201
1211
  constructor(client) {
1202
1212
  super(client, "/webhooks");
1203
1213
  }
1214
+ /**
1215
+ * Create a webhook endpoint. Its answer carries the signing secret, so the
1216
+ * API keeps it out of the Idempotency-Key cache: an `idempotencyKey` is
1217
+ * not sent, and a network error or 5xx is not retried, since the webhook
1218
+ * may already exist. List webhooks before creating it again.
1219
+ */
1220
+ async create(params, _options) {
1221
+ return this.client.request("POST", this.basePath, { body: params });
1222
+ }
1204
1223
  /** Send a test delivery to a webhook endpoint. */
1205
1224
  async test(webhookId, params) {
1206
1225
  return this.client.request(
@@ -1235,11 +1254,14 @@ var WebhooksResource = class extends BaseResource {
1235
1254
  async unsubscribe(subscriptionId) {
1236
1255
  await this.client.request("DELETE", `/webhooks/subscribe/${subscriptionId}`);
1237
1256
  }
1238
- /** Register a CLI listener (returns listenerId and signing secret). */
1239
- async listen(params, options) {
1257
+ /**
1258
+ * Register a CLI listener (returns listenerId and signing secret). The API
1259
+ * ignores Idempotency-Key here (the answer is a secret), so an
1260
+ * `idempotencyKey` is not sent and a network error or 5xx is not retried.
1261
+ */
1262
+ async listen(params, _options) {
1240
1263
  return this.client.request("POST", "/webhooks/listen", {
1241
- body: params,
1242
- idempotencyKey: options?.idempotencyKey
1264
+ body: params
1243
1265
  });
1244
1266
  }
1245
1267
  /** Deregister a CLI listener. */
@@ -1827,10 +1849,9 @@ var ExportResource = class {
1827
1849
  constructor(client) {
1828
1850
  this.client = client;
1829
1851
  }
1830
- async create(params, options) {
1852
+ async create(params, _options) {
1831
1853
  return this.client.request("POST", "/export", {
1832
1854
  body: params,
1833
- idempotencyKey: options?.idempotencyKey,
1834
1855
  responseType: params.format === "csv" ? "text" : "json"
1835
1856
  });
1836
1857
  }
@@ -1,4 +1,4 @@
1
- /** Attachment metadata for community posts and comments (images only, max 5MB each). */
1
+ /** Attachment metadata for community posts and comments (images only, max 4 MB each). */
2
2
  interface CommunityAttachment {
3
3
  filename: string;
4
4
  content_type: string;
@@ -30,7 +30,7 @@ interface CommunityCreatePostParams {
30
30
  topicId: string;
31
31
  /** Custom field values keyed by field definition ID. */
32
32
  customFields?: Record<string, string>;
33
- /** Image attachments (max 10, images only, 5MB each). */
33
+ /** Image attachments (max 10, images only, 4 MB each). */
34
34
  attachments?: CommunityAttachment[];
35
35
  }
36
36
  /** Parameters for updating a post as the original author. */
@@ -53,7 +53,7 @@ interface CommunityCreateCommentParams {
53
53
  body: string;
54
54
  /** Parent comment ID for replies (optional, 1-level nesting). */
55
55
  parentCommentId?: string | null;
56
- /** Image attachments (max 5, images only, 5MB each). */
56
+ /** Image attachments (max 5, images only, 4 MB each). */
57
57
  attachments?: CommunityAttachment[];
58
58
  }
59
59
  /** Response from creating a post. */
@@ -1,4 +1,4 @@
1
- /** Attachment metadata for community posts and comments (images only, max 5MB each). */
1
+ /** Attachment metadata for community posts and comments (images only, max 4 MB each). */
2
2
  interface CommunityAttachment {
3
3
  filename: string;
4
4
  content_type: string;
@@ -30,7 +30,7 @@ interface CommunityCreatePostParams {
30
30
  topicId: string;
31
31
  /** Custom field values keyed by field definition ID. */
32
32
  customFields?: Record<string, string>;
33
- /** Image attachments (max 10, images only, 5MB each). */
33
+ /** Image attachments (max 10, images only, 4 MB each). */
34
34
  attachments?: CommunityAttachment[];
35
35
  }
36
36
  /** Parameters for updating a post as the original author. */
@@ -53,7 +53,7 @@ interface CommunityCreateCommentParams {
53
53
  body: string;
54
54
  /** Parent comment ID for replies (optional, 1-level nesting). */
55
55
  parentCommentId?: string | null;
56
- /** Image attachments (max 5, images only, 5MB each). */
56
+ /** Image attachments (max 5, images only, 4 MB each). */
57
57
  attachments?: CommunityAttachment[];
58
58
  }
59
59
  /** Response from creating a post. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstar.help/js",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Official TypeScript SDK for the cStar customer support platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",