@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 +16 -0
- package/dist/chat/index.cjs +3 -2
- package/dist/chat/index.js +3 -2
- package/dist/index.cjs +13 -3
- package/dist/index.d.cts +37 -10
- package/dist/index.d.ts +37 -10
- package/dist/index.js +13 -3
- package/package.json +1 -1
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)
|
package/dist/chat/index.cjs
CHANGED
|
@@ -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
|
}
|
package/dist/chat/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
363
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2045
|
-
logId: string
|
|
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
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2045
|
-
logId: string
|
|
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
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
322
|
-
|
|
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
|
}
|