@cstar.help/js 0.17.5 → 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 +43 -25
- package/dist/auth/index.cjs +220 -21
- package/dist/auth/index.d.cts +83 -31
- package/dist/auth/index.d.ts +83 -31
- package/dist/auth/index.js +210 -21
- package/dist/chat/index.cjs +134 -22
- package/dist/chat/index.d.cts +32 -4
- package/dist/chat/index.d.ts +32 -4
- package/dist/chat/index.js +134 -22
- package/dist/index.cjs +101 -40
- package/dist/index.d.cts +477 -157
- package/dist/index.d.ts +477 -157
- package/dist/index.js +101 -40
- package/dist/proactive/index.cjs +46 -33
- package/dist/proactive/index.js +45 -34
- package/dist/webhook/index.d.cts +1 -1
- package/dist/webhook/index.d.ts +1 -1
- package/dist/webhook/node.d.cts +1 -1
- package/dist/webhook/node.d.ts +1 -1
- package/dist/{webhookEvents-1N2i2GcQ.d.cts → webhookEvents-Lqpsj935.d.cts} +0 -1
- package/dist/{webhookEvents-1N2i2GcQ.d.ts → webhookEvents-Lqpsj935.d.ts} +0 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -475,6 +476,8 @@ var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
|
475
476
|
var ChatClient = class {
|
|
476
477
|
constructor(config) {
|
|
477
478
|
this.sessionToken = null;
|
|
479
|
+
this.refreshToken = null;
|
|
480
|
+
this.refreshPromise = null;
|
|
478
481
|
this.teamId = null;
|
|
479
482
|
this.customerId = null;
|
|
480
483
|
this.authMode = null;
|
|
@@ -567,6 +570,7 @@ var ChatClient = class {
|
|
|
567
570
|
this.applySession(
|
|
568
571
|
{
|
|
569
572
|
accessToken: response.accessToken,
|
|
573
|
+
refreshToken: response.refreshToken ?? void 0,
|
|
570
574
|
teamId,
|
|
571
575
|
customerName: response.customer.name ?? response.customer.email
|
|
572
576
|
},
|
|
@@ -597,6 +601,7 @@ var ChatClient = class {
|
|
|
597
601
|
applySession(params, generation) {
|
|
598
602
|
this.assertSession(generation);
|
|
599
603
|
this.sessionToken = params.accessToken;
|
|
604
|
+
this.refreshToken = params.refreshToken ?? null;
|
|
600
605
|
if (params.teamId) {
|
|
601
606
|
this.teamId = params.teamId;
|
|
602
607
|
}
|
|
@@ -706,6 +711,8 @@ var ChatClient = class {
|
|
|
706
711
|
this.pollingSubscriptions.clear();
|
|
707
712
|
this.realtimeMessageSubscriptions.clear();
|
|
708
713
|
this.sessionToken = null;
|
|
714
|
+
this.refreshToken = null;
|
|
715
|
+
this.refreshPromise = null;
|
|
709
716
|
this.teamId = null;
|
|
710
717
|
this.customerId = null;
|
|
711
718
|
this.authMode = null;
|
|
@@ -737,7 +744,7 @@ var ChatClient = class {
|
|
|
737
744
|
* the first authenticated round-trip, `getAuthMode()` returns the
|
|
738
745
|
* server-confirmed mode.
|
|
739
746
|
*/
|
|
740
|
-
async fetch(path, options) {
|
|
747
|
+
async fetch(path, options, retriedAfterRefresh = false) {
|
|
741
748
|
const generation = this.sessionGeneration;
|
|
742
749
|
const assertRequest = () => {
|
|
743
750
|
this.assertSession(generation);
|
|
@@ -748,8 +755,10 @@ var ChatClient = class {
|
|
|
748
755
|
const headers = {
|
|
749
756
|
"Content-Type": "application/json"
|
|
750
757
|
};
|
|
751
|
-
if (
|
|
752
|
-
|
|
758
|
+
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
759
|
+
const sentToken = this.sessionToken;
|
|
760
|
+
if (sentToken) {
|
|
761
|
+
headers["Authorization"] = `Bearer ${sentToken}`;
|
|
753
762
|
}
|
|
754
763
|
let response;
|
|
755
764
|
try {
|
|
@@ -777,11 +786,17 @@ var ChatClient = class {
|
|
|
777
786
|
}
|
|
778
787
|
assertRequest();
|
|
779
788
|
const requestId = responseHeaders?.get("X-Request-Id") ?? null;
|
|
789
|
+
if (response.status === 401 && sentToken && this.refreshToken && !retriedAfterRefresh) {
|
|
790
|
+
const refreshed = await this.refreshSession(sentToken);
|
|
791
|
+
assertRequest();
|
|
792
|
+
if (refreshed) return this.fetch(path, options, true);
|
|
793
|
+
}
|
|
780
794
|
if (!response.ok) {
|
|
781
|
-
const errorBody = await response.json().catch(() =>
|
|
795
|
+
const errorBody = await response.json().catch(() => null);
|
|
782
796
|
assertRequest();
|
|
783
|
-
const
|
|
784
|
-
const
|
|
797
|
+
const structured = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : null;
|
|
798
|
+
const code = structured?.code ?? "UNKNOWN";
|
|
799
|
+
const message = structured?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : null) ?? (typeof errorBody?.message === "string" ? errorBody.message : null) ?? `Widget API error: ${response.status}`;
|
|
785
800
|
if (response.status === 401 || response.status === 403) {
|
|
786
801
|
throw new ChatClientAuthError({
|
|
787
802
|
message,
|
|
@@ -822,9 +837,52 @@ var ChatClient = class {
|
|
|
822
837
|
return body;
|
|
823
838
|
} catch (error) {
|
|
824
839
|
assertRequest();
|
|
840
|
+
if (options.idempotencyKey)
|
|
841
|
+
throw new ChatClientNetworkError({
|
|
842
|
+
message: "Could not read the message acknowledgment",
|
|
843
|
+
originalError: error
|
|
844
|
+
});
|
|
825
845
|
throw error;
|
|
826
846
|
}
|
|
827
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Renew the access token with the stored refresh token. Concurrent 401s
|
|
850
|
+
* share one refresh. Resolves true when a newer token is in place.
|
|
851
|
+
*/
|
|
852
|
+
refreshSession(rejectedToken) {
|
|
853
|
+
if (this.sessionToken && this.sessionToken !== rejectedToken) return Promise.resolve(true);
|
|
854
|
+
if (!this.refreshPromise) {
|
|
855
|
+
const pending = this.requestRefresh().finally(() => {
|
|
856
|
+
if (this.refreshPromise === pending) this.refreshPromise = null;
|
|
857
|
+
});
|
|
858
|
+
this.refreshPromise = pending;
|
|
859
|
+
}
|
|
860
|
+
return this.refreshPromise;
|
|
861
|
+
}
|
|
862
|
+
async requestRefresh() {
|
|
863
|
+
const generation = this.sessionGeneration;
|
|
864
|
+
const refreshToken = this.refreshToken;
|
|
865
|
+
if (!refreshToken) return false;
|
|
866
|
+
try {
|
|
867
|
+
const response = await globalThis.fetch(`${this.baseUrl}/api/widget/auth/refresh`, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: { "Content-Type": "application/json" },
|
|
870
|
+
body: JSON.stringify({ refreshToken })
|
|
871
|
+
});
|
|
872
|
+
if (generation !== this.sessionGeneration) return false;
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
if (response.status === 401) this.refreshToken = null;
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
const body = await response.json();
|
|
878
|
+
if (generation !== this.sessionGeneration || !body.accessToken) return false;
|
|
879
|
+
this.sessionToken = body.accessToken;
|
|
880
|
+
this.refreshToken = body.refreshToken ?? refreshToken;
|
|
881
|
+
return true;
|
|
882
|
+
} catch {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
828
886
|
/**
|
|
829
887
|
* Look up the team UUID from the configured `teamSlug`. Used by
|
|
830
888
|
* `loginWithCredentials`. HMAC integrators (`identify()`) never hit
|
|
@@ -1297,26 +1355,80 @@ var MessagesApi = class {
|
|
|
1297
1355
|
assertCurrent();
|
|
1298
1356
|
return response.data.messages;
|
|
1299
1357
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
|
|
1358
|
+
/**
|
|
1359
|
+
* Send one logical reply. Network and server failures retry twice with the
|
|
1360
|
+
* same key. A separate send call is a new intent unless a retained key is supplied.
|
|
1361
|
+
*/
|
|
1362
|
+
async send(ticketId, content, options = {}) {
|
|
1302
1363
|
const assertCurrent = this.chat.captureSession();
|
|
1364
|
+
options.signal?.throwIfAborted();
|
|
1365
|
+
const key = options.idempotencyKey ?? createMessageRequestKey();
|
|
1366
|
+
if (!key || key.length > 250 || key.trim() !== key || /[^\x20-\x7e]/.test(key))
|
|
1367
|
+
throw new ChatClientError({
|
|
1368
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
1369
|
+
message: "Idempotency-Key must contain 1 to 250 printable characters, without surrounding whitespace."
|
|
1370
|
+
});
|
|
1303
1371
|
const teamId = await this.chat.ensureTeamId();
|
|
1304
1372
|
assertCurrent();
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1373
|
+
for (let attempt = 0; ; attempt++) {
|
|
1374
|
+
assertCurrent();
|
|
1375
|
+
options.signal?.throwIfAborted();
|
|
1376
|
+
try {
|
|
1377
|
+
const response = await this.chat.fetch(
|
|
1378
|
+
"/api/widget/messages",
|
|
1379
|
+
{
|
|
1380
|
+
method: "POST",
|
|
1381
|
+
body: { teamId, ticketId, content },
|
|
1382
|
+
idempotencyKey: key,
|
|
1383
|
+
signal: options.signal
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
assertCurrent();
|
|
1387
|
+
if (typeof response?.data?.message?.id !== "string" || !response.data.message.id)
|
|
1388
|
+
throw new ChatClientNetworkError({
|
|
1389
|
+
message: "Message acknowledgment was incomplete",
|
|
1390
|
+
originalError: new Error(
|
|
1391
|
+
"The reply may already be saved. Retry with the same send identity."
|
|
1392
|
+
)
|
|
1393
|
+
});
|
|
1394
|
+
return response.data.message;
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
assertCurrent();
|
|
1397
|
+
options.signal?.throwIfAborted();
|
|
1398
|
+
if (error instanceof ChatClientError) error.idempotencyKey = key;
|
|
1399
|
+
if (attempt >= 2 || !(error instanceof ChatClientNetworkError || error instanceof ChatClientServerError))
|
|
1400
|
+
throw error;
|
|
1401
|
+
await waitForMessageRetry(250 * 2 ** attempt, options.signal);
|
|
1314
1402
|
}
|
|
1315
|
-
|
|
1316
|
-
assertCurrent();
|
|
1317
|
-
return response.data.message;
|
|
1403
|
+
}
|
|
1318
1404
|
}
|
|
1319
1405
|
};
|
|
1406
|
+
function createMessageRequestKey() {
|
|
1407
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
1408
|
+
if (typeof globalThis.crypto?.getRandomValues !== "function")
|
|
1409
|
+
throw new ChatClientError({
|
|
1410
|
+
code: "RANDOM_UNAVAILABLE",
|
|
1411
|
+
message: "Secure random values are required to send a message safely."
|
|
1412
|
+
});
|
|
1413
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
1414
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1415
|
+
}
|
|
1416
|
+
function waitForMessageRetry(delay, signal) {
|
|
1417
|
+
signal?.throwIfAborted();
|
|
1418
|
+
return new Promise((resolve, reject) => {
|
|
1419
|
+
const finish = () => {
|
|
1420
|
+
signal?.removeEventListener("abort", abort);
|
|
1421
|
+
resolve();
|
|
1422
|
+
};
|
|
1423
|
+
const timer = setTimeout(finish, delay);
|
|
1424
|
+
const abort = () => {
|
|
1425
|
+
clearTimeout(timer);
|
|
1426
|
+
signal?.removeEventListener("abort", abort);
|
|
1427
|
+
reject(signal?.reason);
|
|
1428
|
+
};
|
|
1429
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1320
1432
|
function decodeJwtSub(jwt) {
|
|
1321
1433
|
try {
|
|
1322
1434
|
const parts = jwt.split(".");
|
package/dist/chat/index.d.cts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
505
|
-
|
|
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 };
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
505
|
-
|
|
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 };
|
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
|
}
|
|
@@ -434,6 +435,8 @@ var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
|
434
435
|
var ChatClient = class {
|
|
435
436
|
constructor(config) {
|
|
436
437
|
this.sessionToken = null;
|
|
438
|
+
this.refreshToken = null;
|
|
439
|
+
this.refreshPromise = null;
|
|
437
440
|
this.teamId = null;
|
|
438
441
|
this.customerId = null;
|
|
439
442
|
this.authMode = null;
|
|
@@ -526,6 +529,7 @@ var ChatClient = class {
|
|
|
526
529
|
this.applySession(
|
|
527
530
|
{
|
|
528
531
|
accessToken: response.accessToken,
|
|
532
|
+
refreshToken: response.refreshToken ?? void 0,
|
|
529
533
|
teamId,
|
|
530
534
|
customerName: response.customer.name ?? response.customer.email
|
|
531
535
|
},
|
|
@@ -556,6 +560,7 @@ var ChatClient = class {
|
|
|
556
560
|
applySession(params, generation) {
|
|
557
561
|
this.assertSession(generation);
|
|
558
562
|
this.sessionToken = params.accessToken;
|
|
563
|
+
this.refreshToken = params.refreshToken ?? null;
|
|
559
564
|
if (params.teamId) {
|
|
560
565
|
this.teamId = params.teamId;
|
|
561
566
|
}
|
|
@@ -665,6 +670,8 @@ var ChatClient = class {
|
|
|
665
670
|
this.pollingSubscriptions.clear();
|
|
666
671
|
this.realtimeMessageSubscriptions.clear();
|
|
667
672
|
this.sessionToken = null;
|
|
673
|
+
this.refreshToken = null;
|
|
674
|
+
this.refreshPromise = null;
|
|
668
675
|
this.teamId = null;
|
|
669
676
|
this.customerId = null;
|
|
670
677
|
this.authMode = null;
|
|
@@ -696,7 +703,7 @@ var ChatClient = class {
|
|
|
696
703
|
* the first authenticated round-trip, `getAuthMode()` returns the
|
|
697
704
|
* server-confirmed mode.
|
|
698
705
|
*/
|
|
699
|
-
async fetch(path, options) {
|
|
706
|
+
async fetch(path, options, retriedAfterRefresh = false) {
|
|
700
707
|
const generation = this.sessionGeneration;
|
|
701
708
|
const assertRequest = () => {
|
|
702
709
|
this.assertSession(generation);
|
|
@@ -707,8 +714,10 @@ var ChatClient = class {
|
|
|
707
714
|
const headers = {
|
|
708
715
|
"Content-Type": "application/json"
|
|
709
716
|
};
|
|
710
|
-
if (
|
|
711
|
-
|
|
717
|
+
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
718
|
+
const sentToken = this.sessionToken;
|
|
719
|
+
if (sentToken) {
|
|
720
|
+
headers["Authorization"] = `Bearer ${sentToken}`;
|
|
712
721
|
}
|
|
713
722
|
let response;
|
|
714
723
|
try {
|
|
@@ -736,11 +745,17 @@ var ChatClient = class {
|
|
|
736
745
|
}
|
|
737
746
|
assertRequest();
|
|
738
747
|
const requestId = responseHeaders?.get("X-Request-Id") ?? null;
|
|
748
|
+
if (response.status === 401 && sentToken && this.refreshToken && !retriedAfterRefresh) {
|
|
749
|
+
const refreshed = await this.refreshSession(sentToken);
|
|
750
|
+
assertRequest();
|
|
751
|
+
if (refreshed) return this.fetch(path, options, true);
|
|
752
|
+
}
|
|
739
753
|
if (!response.ok) {
|
|
740
|
-
const errorBody = await response.json().catch(() =>
|
|
754
|
+
const errorBody = await response.json().catch(() => null);
|
|
741
755
|
assertRequest();
|
|
742
|
-
const
|
|
743
|
-
const
|
|
756
|
+
const structured = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : null;
|
|
757
|
+
const code = structured?.code ?? "UNKNOWN";
|
|
758
|
+
const message = structured?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : null) ?? (typeof errorBody?.message === "string" ? errorBody.message : null) ?? `Widget API error: ${response.status}`;
|
|
744
759
|
if (response.status === 401 || response.status === 403) {
|
|
745
760
|
throw new ChatClientAuthError({
|
|
746
761
|
message,
|
|
@@ -781,9 +796,52 @@ var ChatClient = class {
|
|
|
781
796
|
return body;
|
|
782
797
|
} catch (error) {
|
|
783
798
|
assertRequest();
|
|
799
|
+
if (options.idempotencyKey)
|
|
800
|
+
throw new ChatClientNetworkError({
|
|
801
|
+
message: "Could not read the message acknowledgment",
|
|
802
|
+
originalError: error
|
|
803
|
+
});
|
|
784
804
|
throw error;
|
|
785
805
|
}
|
|
786
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Renew the access token with the stored refresh token. Concurrent 401s
|
|
809
|
+
* share one refresh. Resolves true when a newer token is in place.
|
|
810
|
+
*/
|
|
811
|
+
refreshSession(rejectedToken) {
|
|
812
|
+
if (this.sessionToken && this.sessionToken !== rejectedToken) return Promise.resolve(true);
|
|
813
|
+
if (!this.refreshPromise) {
|
|
814
|
+
const pending = this.requestRefresh().finally(() => {
|
|
815
|
+
if (this.refreshPromise === pending) this.refreshPromise = null;
|
|
816
|
+
});
|
|
817
|
+
this.refreshPromise = pending;
|
|
818
|
+
}
|
|
819
|
+
return this.refreshPromise;
|
|
820
|
+
}
|
|
821
|
+
async requestRefresh() {
|
|
822
|
+
const generation = this.sessionGeneration;
|
|
823
|
+
const refreshToken = this.refreshToken;
|
|
824
|
+
if (!refreshToken) return false;
|
|
825
|
+
try {
|
|
826
|
+
const response = await globalThis.fetch(`${this.baseUrl}/api/widget/auth/refresh`, {
|
|
827
|
+
method: "POST",
|
|
828
|
+
headers: { "Content-Type": "application/json" },
|
|
829
|
+
body: JSON.stringify({ refreshToken })
|
|
830
|
+
});
|
|
831
|
+
if (generation !== this.sessionGeneration) return false;
|
|
832
|
+
if (!response.ok) {
|
|
833
|
+
if (response.status === 401) this.refreshToken = null;
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
const body = await response.json();
|
|
837
|
+
if (generation !== this.sessionGeneration || !body.accessToken) return false;
|
|
838
|
+
this.sessionToken = body.accessToken;
|
|
839
|
+
this.refreshToken = body.refreshToken ?? refreshToken;
|
|
840
|
+
return true;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
787
845
|
/**
|
|
788
846
|
* Look up the team UUID from the configured `teamSlug`. Used by
|
|
789
847
|
* `loginWithCredentials`. HMAC integrators (`identify()`) never hit
|
|
@@ -1256,26 +1314,80 @@ var MessagesApi = class {
|
|
|
1256
1314
|
assertCurrent();
|
|
1257
1315
|
return response.data.messages;
|
|
1258
1316
|
}
|
|
1259
|
-
/**
|
|
1260
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* Send one logical reply. Network and server failures retry twice with the
|
|
1319
|
+
* same key. A separate send call is a new intent unless a retained key is supplied.
|
|
1320
|
+
*/
|
|
1321
|
+
async send(ticketId, content, options = {}) {
|
|
1261
1322
|
const assertCurrent = this.chat.captureSession();
|
|
1323
|
+
options.signal?.throwIfAborted();
|
|
1324
|
+
const key = options.idempotencyKey ?? createMessageRequestKey();
|
|
1325
|
+
if (!key || key.length > 250 || key.trim() !== key || /[^\x20-\x7e]/.test(key))
|
|
1326
|
+
throw new ChatClientError({
|
|
1327
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
1328
|
+
message: "Idempotency-Key must contain 1 to 250 printable characters, without surrounding whitespace."
|
|
1329
|
+
});
|
|
1262
1330
|
const teamId = await this.chat.ensureTeamId();
|
|
1263
1331
|
assertCurrent();
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1332
|
+
for (let attempt = 0; ; attempt++) {
|
|
1333
|
+
assertCurrent();
|
|
1334
|
+
options.signal?.throwIfAborted();
|
|
1335
|
+
try {
|
|
1336
|
+
const response = await this.chat.fetch(
|
|
1337
|
+
"/api/widget/messages",
|
|
1338
|
+
{
|
|
1339
|
+
method: "POST",
|
|
1340
|
+
body: { teamId, ticketId, content },
|
|
1341
|
+
idempotencyKey: key,
|
|
1342
|
+
signal: options.signal
|
|
1343
|
+
}
|
|
1344
|
+
);
|
|
1345
|
+
assertCurrent();
|
|
1346
|
+
if (typeof response?.data?.message?.id !== "string" || !response.data.message.id)
|
|
1347
|
+
throw new ChatClientNetworkError({
|
|
1348
|
+
message: "Message acknowledgment was incomplete",
|
|
1349
|
+
originalError: new Error(
|
|
1350
|
+
"The reply may already be saved. Retry with the same send identity."
|
|
1351
|
+
)
|
|
1352
|
+
});
|
|
1353
|
+
return response.data.message;
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
assertCurrent();
|
|
1356
|
+
options.signal?.throwIfAborted();
|
|
1357
|
+
if (error instanceof ChatClientError) error.idempotencyKey = key;
|
|
1358
|
+
if (attempt >= 2 || !(error instanceof ChatClientNetworkError || error instanceof ChatClientServerError))
|
|
1359
|
+
throw error;
|
|
1360
|
+
await waitForMessageRetry(250 * 2 ** attempt, options.signal);
|
|
1273
1361
|
}
|
|
1274
|
-
|
|
1275
|
-
assertCurrent();
|
|
1276
|
-
return response.data.message;
|
|
1362
|
+
}
|
|
1277
1363
|
}
|
|
1278
1364
|
};
|
|
1365
|
+
function createMessageRequestKey() {
|
|
1366
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
1367
|
+
if (typeof globalThis.crypto?.getRandomValues !== "function")
|
|
1368
|
+
throw new ChatClientError({
|
|
1369
|
+
code: "RANDOM_UNAVAILABLE",
|
|
1370
|
+
message: "Secure random values are required to send a message safely."
|
|
1371
|
+
});
|
|
1372
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
1373
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1374
|
+
}
|
|
1375
|
+
function waitForMessageRetry(delay, signal) {
|
|
1376
|
+
signal?.throwIfAborted();
|
|
1377
|
+
return new Promise((resolve, reject) => {
|
|
1378
|
+
const finish = () => {
|
|
1379
|
+
signal?.removeEventListener("abort", abort);
|
|
1380
|
+
resolve();
|
|
1381
|
+
};
|
|
1382
|
+
const timer = setTimeout(finish, delay);
|
|
1383
|
+
const abort = () => {
|
|
1384
|
+
clearTimeout(timer);
|
|
1385
|
+
signal?.removeEventListener("abort", abort);
|
|
1386
|
+
reject(signal?.reason);
|
|
1387
|
+
};
|
|
1388
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1279
1391
|
function decodeJwtSub(jwt) {
|
|
1280
1392
|
try {
|
|
1281
1393
|
const parts = jwt.split(".");
|