@cstar.help/js 0.18.0 → 0.18.1

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
487
+ (unreleased), the client waits and retries within `maxRetries`; 0.18.0 throws
488
+ `CStarConflictError`.
489
+
490
+ The API ignores the key on `webhooks.create`, `webhooks.listen` and
491
+ `export.create`, so a key does not make their retries safe: the client still
492
+ retries a network error or 5xx. Leave the key off those calls (without one the
493
+ 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
  }
@@ -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
  }
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.1";
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
  }
package/dist/index.d.cts CHANGED
@@ -1581,6 +1581,7 @@ interface Mention {
1581
1581
  senderName: string | null;
1582
1582
  recipientId: string;
1583
1583
  recipientName: string | null;
1584
+ /** The ticket's prefixed id (`tkt_*`), or null for a mention with no ticket. */
1584
1585
  ticketId: string | null;
1585
1586
  ticketTitle: string | null;
1586
1587
  isRead: boolean;
@@ -1674,6 +1675,11 @@ declare class AttachmentsResource {
1674
1675
  type CustomFieldContentType = 'ticket' | 'customer' | 'article';
1675
1676
  /** Input type of a custom field. */
1676
1677
  type CustomFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox';
1678
+ /** One choice of a `select` field. A record stores its `value`. */
1679
+ interface CustomFieldOption {
1680
+ value: string;
1681
+ label: string;
1682
+ }
1677
1683
  /** A custom field definition (mirrors `mapFieldToApi` on the server). */
1678
1684
  interface CustomField {
1679
1685
  id: string;
@@ -1683,7 +1689,7 @@ interface CustomField {
1683
1689
  fieldType: CustomFieldType;
1684
1690
  defaultValue: string | null;
1685
1691
  /** Choices for `select` fields. */
1686
- options: string[] | null;
1692
+ options: CustomFieldOption[] | null;
1687
1693
  minValue: number | null;
1688
1694
  maxValue: number | null;
1689
1695
  required: boolean;
@@ -1702,7 +1708,11 @@ interface CustomFieldCreateParams {
1702
1708
  /** Defaults to `text`. */
1703
1709
  fieldType?: CustomFieldType;
1704
1710
  defaultValue?: string;
1705
- options?: string[];
1711
+ /** Choices for `select` fields. A string is its own label. */
1712
+ options?: Array<string | {
1713
+ value: string;
1714
+ label?: string;
1715
+ }>;
1706
1716
  minValue?: number;
1707
1717
  maxValue?: number;
1708
1718
  required?: boolean;
@@ -1712,7 +1722,11 @@ interface CustomFieldUpdateParams {
1712
1722
  label?: string;
1713
1723
  fieldType?: CustomFieldType;
1714
1724
  defaultValue?: string | null;
1715
- options?: string[];
1725
+ /** Replaces the choices. A string is its own label. */
1726
+ options?: Array<string | {
1727
+ value: string;
1728
+ label?: string;
1729
+ }>;
1716
1730
  minValue?: number | null;
1717
1731
  maxValue?: number | null;
1718
1732
  required?: boolean;
@@ -2032,7 +2046,10 @@ interface ImportCreateParams {
2032
2046
  * A truthy `source.body` paired with an array at `conversation_parts` or
2033
2047
  * `conversation_parts.conversation_parts` is also rejected, even if that
2034
2048
  * array is empty. Empty history without this initial-message shape,
2035
- * customFields/metadata and wrapped records with `action: 'skip'` are allowed.
2049
+ * customFields and wrapped records with `action: 'skip'` are allowed (empty
2050
+ * history and `metadata` are ignored with a row warning). camelCase field
2051
+ * names (`customerEmail`, `externalId`) work; any field the import doesn't
2052
+ * read is named in `warnings`.
2036
2053
  */
2037
2054
  records: Record<string, unknown>[];
2038
2055
  /** How to handle duplicates. Defaults to `'overwrite'`. */
@@ -2041,8 +2058,8 @@ interface ImportCreateParams {
2041
2058
  /** Import result. */
2042
2059
  interface ImportResult {
2043
2060
  object: 'import_result';
2044
- /** ID of the persisted import-log row (null if the log row could not be written). */
2045
- logId: string | null;
2061
+ /** ID of the import's log row (GET /import/logs/{logId}, `import.getLog()`). */
2062
+ logId: string;
2046
2063
  /** Entity type imported — `ticket`, `customer`, or `article`. */
2047
2064
  entityType: string;
2048
2065
  /** True when every record imported without error. */
@@ -2066,18 +2083,28 @@ interface ImportResult {
2066
2083
  interface ImportLog {
2067
2084
  id: string;
2068
2085
  object: 'import_log';
2069
- resource: string;
2070
- status: string;
2086
+ /** `ticket`, `customer` or `article`. */
2087
+ entityType: string;
2088
+ /** `api` for REST imports, or the platform the dashboard importer read. */
2089
+ sourcePlatform: string | null;
2090
+ sourceFilename: string | null;
2071
2091
  totalRecords: number;
2072
2092
  created: number;
2073
2093
  updated: number;
2074
2094
  skipped: number;
2075
- errors: number;
2095
+ failed: number;
2096
+ /** The failed rows, as "Row N: ..." lines, or null. */
2097
+ errors: string[] | null;
2098
+ matchKey: string | null;
2099
+ mergeStrategy: Record<string, string> | null;
2100
+ /** When the import finished; null while it runs. */
2101
+ completedAt: string | null;
2076
2102
  createdAt: string;
2077
2103
  }
2078
2104
  /** Parameters for listing import logs. */
2079
2105
  interface ImportLogListParams extends PaginationParams {
2080
- resource?: string;
2106
+ /** Only imports of this kind. */
2107
+ resource?: 'tickets' | 'customers' | 'articles';
2081
2108
  }
2082
2109
 
2083
2110
  /** Export data from your team. */
package/dist/index.d.ts CHANGED
@@ -1581,6 +1581,7 @@ interface Mention {
1581
1581
  senderName: string | null;
1582
1582
  recipientId: string;
1583
1583
  recipientName: string | null;
1584
+ /** The ticket's prefixed id (`tkt_*`), or null for a mention with no ticket. */
1584
1585
  ticketId: string | null;
1585
1586
  ticketTitle: string | null;
1586
1587
  isRead: boolean;
@@ -1674,6 +1675,11 @@ declare class AttachmentsResource {
1674
1675
  type CustomFieldContentType = 'ticket' | 'customer' | 'article';
1675
1676
  /** Input type of a custom field. */
1676
1677
  type CustomFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox';
1678
+ /** One choice of a `select` field. A record stores its `value`. */
1679
+ interface CustomFieldOption {
1680
+ value: string;
1681
+ label: string;
1682
+ }
1677
1683
  /** A custom field definition (mirrors `mapFieldToApi` on the server). */
1678
1684
  interface CustomField {
1679
1685
  id: string;
@@ -1683,7 +1689,7 @@ interface CustomField {
1683
1689
  fieldType: CustomFieldType;
1684
1690
  defaultValue: string | null;
1685
1691
  /** Choices for `select` fields. */
1686
- options: string[] | null;
1692
+ options: CustomFieldOption[] | null;
1687
1693
  minValue: number | null;
1688
1694
  maxValue: number | null;
1689
1695
  required: boolean;
@@ -1702,7 +1708,11 @@ interface CustomFieldCreateParams {
1702
1708
  /** Defaults to `text`. */
1703
1709
  fieldType?: CustomFieldType;
1704
1710
  defaultValue?: string;
1705
- options?: string[];
1711
+ /** Choices for `select` fields. A string is its own label. */
1712
+ options?: Array<string | {
1713
+ value: string;
1714
+ label?: string;
1715
+ }>;
1706
1716
  minValue?: number;
1707
1717
  maxValue?: number;
1708
1718
  required?: boolean;
@@ -1712,7 +1722,11 @@ interface CustomFieldUpdateParams {
1712
1722
  label?: string;
1713
1723
  fieldType?: CustomFieldType;
1714
1724
  defaultValue?: string | null;
1715
- options?: string[];
1725
+ /** Replaces the choices. A string is its own label. */
1726
+ options?: Array<string | {
1727
+ value: string;
1728
+ label?: string;
1729
+ }>;
1716
1730
  minValue?: number | null;
1717
1731
  maxValue?: number | null;
1718
1732
  required?: boolean;
@@ -2032,7 +2046,10 @@ interface ImportCreateParams {
2032
2046
  * A truthy `source.body` paired with an array at `conversation_parts` or
2033
2047
  * `conversation_parts.conversation_parts` is also rejected, even if that
2034
2048
  * array is empty. Empty history without this initial-message shape,
2035
- * customFields/metadata and wrapped records with `action: 'skip'` are allowed.
2049
+ * customFields and wrapped records with `action: 'skip'` are allowed (empty
2050
+ * history and `metadata` are ignored with a row warning). camelCase field
2051
+ * names (`customerEmail`, `externalId`) work; any field the import doesn't
2052
+ * read is named in `warnings`.
2036
2053
  */
2037
2054
  records: Record<string, unknown>[];
2038
2055
  /** How to handle duplicates. Defaults to `'overwrite'`. */
@@ -2041,8 +2058,8 @@ interface ImportCreateParams {
2041
2058
  /** Import result. */
2042
2059
  interface ImportResult {
2043
2060
  object: 'import_result';
2044
- /** ID of the persisted import-log row (null if the log row could not be written). */
2045
- logId: string | null;
2061
+ /** ID of the import's log row (GET /import/logs/{logId}, `import.getLog()`). */
2062
+ logId: string;
2046
2063
  /** Entity type imported — `ticket`, `customer`, or `article`. */
2047
2064
  entityType: string;
2048
2065
  /** True when every record imported without error. */
@@ -2066,18 +2083,28 @@ interface ImportResult {
2066
2083
  interface ImportLog {
2067
2084
  id: string;
2068
2085
  object: 'import_log';
2069
- resource: string;
2070
- status: string;
2086
+ /** `ticket`, `customer` or `article`. */
2087
+ entityType: string;
2088
+ /** `api` for REST imports, or the platform the dashboard importer read. */
2089
+ sourcePlatform: string | null;
2090
+ sourceFilename: string | null;
2071
2091
  totalRecords: number;
2072
2092
  created: number;
2073
2093
  updated: number;
2074
2094
  skipped: number;
2075
- errors: number;
2095
+ failed: number;
2096
+ /** The failed rows, as "Row N: ..." lines, or null. */
2097
+ errors: string[] | null;
2098
+ matchKey: string | null;
2099
+ mergeStrategy: Record<string, string> | null;
2100
+ /** When the import finished; null while it runs. */
2101
+ completedAt: string | null;
2076
2102
  createdAt: string;
2077
2103
  }
2078
2104
  /** Parameters for listing import logs. */
2079
2105
  interface ImportLogListParams extends PaginationParams {
2080
- resource?: string;
2106
+ /** Only imports of this kind. */
2107
+ resource?: 'tickets' | 'customers' | 'articles';
2081
2108
  }
2082
2109
 
2083
2110
  /** Export data from your team. */
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.1";
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
  }
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.1",
4
4
  "description": "Official TypeScript SDK for the cStar customer support platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",