@cstar.help/js 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -7
- package/dist/chat/index.cjs +279 -47
- package/dist/chat/index.d.cts +237 -51
- package/dist/chat/index.d.ts +237 -51
- package/dist/chat/index.js +273 -46
- package/dist/index.cjs +14 -2
- package/dist/index.d.cts +399 -15
- package/dist/index.d.ts +399 -15
- package/dist/index.js +14 -2
- package/dist/quickhelp/index.cjs +102 -0
- package/dist/quickhelp/index.d.cts +97 -0
- package/dist/quickhelp/index.d.ts +97 -0
- package/dist/quickhelp/index.js +76 -0
- package/dist/webhook/index.d.cts +390 -102
- package/dist/webhook/index.d.ts +390 -102
- package/package.json +13 -8
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ cStar has one package with **six entry points**, each designed for a different c
|
|
|
30
30
|
|
|
31
31
|
### The Security Model
|
|
32
32
|
|
|
33
|
-
cStar uses **two key types** to control access — the same pattern as Stripe
|
|
33
|
+
cStar uses **two key types** to control access — the same pattern as Stripe:
|
|
34
34
|
|
|
35
35
|
```
|
|
36
36
|
Secret keys (sk_live_*, sk_test_*)
|
|
@@ -200,32 +200,84 @@ const { data: searchResults } = await cstar.search.query({
|
|
|
200
200
|
|
|
201
201
|
Build custom chat experiences in the browser. Uses a publishable key — safe for frontend code.
|
|
202
202
|
|
|
203
|
+
The chat client supports two authentication modes:
|
|
204
|
+
|
|
205
|
+
- **HMAC** — your server signs the customer payload with the team's signing secret. Best for sites where users are already authenticated server-side.
|
|
206
|
+
- **Session** — the customer signs in directly with email/password (or magic link). Best for standalone widgets and customer portals where there's no host-app session to bridge.
|
|
207
|
+
|
|
208
|
+
You don't have to pick the mode up-front. The first response from any cstar endpoint includes an `X-Cstar-Auth-Mode` header, which the SDK caches; `chat.getAuthMode()` returns `'hmac'`, `'session'`, or `null` after the first call.
|
|
209
|
+
|
|
210
|
+
### HMAC
|
|
211
|
+
|
|
203
212
|
```typescript
|
|
204
213
|
import { ChatClient } from '@cstar.help/js/chat';
|
|
205
214
|
|
|
206
215
|
const chat = new ChatClient({ teamSlug: 'acme' });
|
|
207
216
|
|
|
208
|
-
// Identify the customer (HMAC signature from your server)
|
|
209
217
|
await chat.identify(
|
|
210
218
|
{ externalId: 'usr_123', email: 'jane@example.com', name: 'Jane', timestamp: Date.now() / 1000 },
|
|
211
219
|
hmacSignature
|
|
212
220
|
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Session (email/password)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const chat = new ChatClient({ teamSlug: 'acme' });
|
|
227
|
+
|
|
228
|
+
// Login with credentials — returns the customer + tokens
|
|
229
|
+
const { customer, accessToken, refreshToken } = await chat.loginWithCredentials(
|
|
230
|
+
'jane@example.com',
|
|
231
|
+
'hunter2'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Or rehydrate from previously stored tokens
|
|
235
|
+
await chat.loginWithSession(accessToken, refreshToken);
|
|
236
|
+
```
|
|
213
237
|
|
|
214
|
-
|
|
215
|
-
const { conversations } = await chat.conversations.list();
|
|
238
|
+
### Once authenticated (either mode)
|
|
216
239
|
|
|
217
|
-
|
|
240
|
+
```typescript
|
|
241
|
+
const { tickets } = await chat.tickets.list();
|
|
218
242
|
await chat.messages.send('tkt_abc123', 'Thanks for the help!');
|
|
219
243
|
|
|
220
|
-
// Subscribe to new messages
|
|
221
244
|
const unsubscribe = chat.onMessage('tkt_abc123', (message) => {
|
|
222
245
|
console.log(`${message.sender_name}: ${message.content}`);
|
|
223
246
|
});
|
|
224
247
|
|
|
225
|
-
// Clean up
|
|
226
248
|
chat.disconnect();
|
|
227
249
|
```
|
|
228
250
|
|
|
251
|
+
### Typed errors
|
|
252
|
+
|
|
253
|
+
Every `ChatClient` request throws a typed subclass of `ChatClientError` on failure. Each carries a `requestId` (from the `X-Request-Id` response header) so customer support can correlate against server logs.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import {
|
|
257
|
+
ChatClientError,
|
|
258
|
+
ChatClientAuthError,
|
|
259
|
+
ChatClientRateLimitError,
|
|
260
|
+
ChatClientServerError,
|
|
261
|
+
ChatClientNetworkError
|
|
262
|
+
} from '@cstar.help/js/chat';
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await chat.identify(customer, signature);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof ChatClientAuthError) {
|
|
268
|
+
// 401 / 403 — bad signature, expired token, wrong team
|
|
269
|
+
} else if (err instanceof ChatClientRateLimitError) {
|
|
270
|
+
// 429 — back off for err.retryAfter seconds
|
|
271
|
+
} else if (err instanceof ChatClientServerError) {
|
|
272
|
+
// 5xx — usually transient, retry with backoff
|
|
273
|
+
} else if (err instanceof ChatClientNetworkError) {
|
|
274
|
+
// fetch() itself failed — DNS, offline, CORS — see err.originalError
|
|
275
|
+
} else if (err instanceof ChatClientError) {
|
|
276
|
+
console.log('Error', err.code, err.statusCode, 'requestId:', err.requestId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
229
281
|
---
|
|
230
282
|
|
|
231
283
|
## Client-Side: Public Knowledge Base
|
package/dist/chat/index.cjs
CHANGED
|
@@ -30,7 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/chat/index.ts
|
|
31
31
|
var chat_exports = {};
|
|
32
32
|
__export(chat_exports, {
|
|
33
|
-
ChatClient: () => ChatClient
|
|
33
|
+
ChatClient: () => ChatClient,
|
|
34
|
+
ChatClientAuthError: () => ChatClientAuthError,
|
|
35
|
+
ChatClientError: () => ChatClientError,
|
|
36
|
+
ChatClientNetworkError: () => ChatClientNetworkError,
|
|
37
|
+
ChatClientRateLimitError: () => ChatClientRateLimitError,
|
|
38
|
+
ChatClientServerError: () => ChatClientServerError
|
|
34
39
|
});
|
|
35
40
|
module.exports = __toCommonJS(chat_exports);
|
|
36
41
|
|
|
@@ -45,8 +50,8 @@ var RealtimeManager = class {
|
|
|
45
50
|
this.options = options;
|
|
46
51
|
}
|
|
47
52
|
/**
|
|
48
|
-
* Initialize the
|
|
49
|
-
* Returns true if successful, false if
|
|
53
|
+
* Initialize the realtime client and authenticate with a token.
|
|
54
|
+
* Returns true if successful, false if the realtime runtime is unavailable.
|
|
50
55
|
*/
|
|
51
56
|
init() {
|
|
52
57
|
if (this.initPromise) return this.initPromise;
|
|
@@ -55,15 +60,15 @@ var RealtimeManager = class {
|
|
|
55
60
|
}
|
|
56
61
|
async doInit() {
|
|
57
62
|
try {
|
|
63
|
+
const tokenData = await this.options.fetchToken();
|
|
58
64
|
const { createClient } = await import(
|
|
59
65
|
/* webpackIgnore: true */
|
|
60
66
|
"@supabase/supabase-js"
|
|
61
67
|
);
|
|
62
|
-
this.client = createClient(
|
|
68
|
+
this.client = createClient(tokenData.realtimeUrl, tokenData.realtimeKey, {
|
|
63
69
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
64
70
|
realtime: { params: { eventsPerSecond: 10 } }
|
|
65
71
|
});
|
|
66
|
-
const tokenData = await this.options.fetchToken();
|
|
67
72
|
this.setToken(tokenData);
|
|
68
73
|
return true;
|
|
69
74
|
} catch {
|
|
@@ -121,7 +126,8 @@ var RealtimeManager = class {
|
|
|
121
126
|
sender_name: row.sender_name ?? "",
|
|
122
127
|
created_at: row.created_at,
|
|
123
128
|
read_at: row.read_at ?? null,
|
|
124
|
-
attachments: row.attachments ?? []
|
|
129
|
+
attachments: row.attachments ?? [],
|
|
130
|
+
payload: row.payload ?? null
|
|
125
131
|
};
|
|
126
132
|
callback(message);
|
|
127
133
|
}
|
|
@@ -129,10 +135,18 @@ var RealtimeManager = class {
|
|
|
129
135
|
this.channels.set(channelName, channel);
|
|
130
136
|
return () => this.removeChannel(channelName);
|
|
131
137
|
}
|
|
132
|
-
/** Get the underlying
|
|
138
|
+
/** Get the underlying realtime client (for sharing with PresenceManager). */
|
|
133
139
|
getClient() {
|
|
134
140
|
return this.client;
|
|
135
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The current Supabase JWT (set by init/refresh). Returned so callers can
|
|
144
|
+
* decode the `sub` claim to learn the customerId — required after a
|
|
145
|
+
* session-mode login where the SDK doesn't have customerId at hand.
|
|
146
|
+
*/
|
|
147
|
+
getCurrentToken() {
|
|
148
|
+
return this.currentToken;
|
|
149
|
+
}
|
|
136
150
|
/** Whether the realtime manager is initialized and has a token. */
|
|
137
151
|
get isReady() {
|
|
138
152
|
return this.client !== null && this.currentToken !== null;
|
|
@@ -171,7 +185,7 @@ var PresenceManager = class {
|
|
|
171
185
|
this.typingClearTimers = /* @__PURE__ */ new Map();
|
|
172
186
|
this.options = options;
|
|
173
187
|
}
|
|
174
|
-
/** Set the
|
|
188
|
+
/** Set the realtime client (shared from RealtimeManager). */
|
|
175
189
|
setClient(client) {
|
|
176
190
|
this.client = client;
|
|
177
191
|
}
|
|
@@ -274,6 +288,43 @@ var PresenceManager = class {
|
|
|
274
288
|
}
|
|
275
289
|
};
|
|
276
290
|
|
|
291
|
+
// src/chat/errors.ts
|
|
292
|
+
var ChatClientError = class extends Error {
|
|
293
|
+
constructor(message, opts) {
|
|
294
|
+
super(message);
|
|
295
|
+
this.name = "ChatClientError";
|
|
296
|
+
this.code = opts.code ?? "UNKNOWN";
|
|
297
|
+
this.statusCode = opts.statusCode;
|
|
298
|
+
this.requestId = opts.requestId ?? null;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var ChatClientAuthError = class extends ChatClientError {
|
|
302
|
+
constructor(opts) {
|
|
303
|
+
super(opts.message, opts);
|
|
304
|
+
this.name = "ChatClientAuthError";
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var ChatClientRateLimitError = class extends ChatClientError {
|
|
308
|
+
constructor(opts) {
|
|
309
|
+
super(opts.message, opts);
|
|
310
|
+
this.name = "ChatClientRateLimitError";
|
|
311
|
+
this.retryAfter = opts.retryAfter ?? null;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var ChatClientServerError = class extends ChatClientError {
|
|
315
|
+
constructor(opts) {
|
|
316
|
+
super(opts.message, opts);
|
|
317
|
+
this.name = "ChatClientServerError";
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var ChatClientNetworkError = class extends ChatClientError {
|
|
321
|
+
constructor(opts) {
|
|
322
|
+
super(opts.message, { code: "NETWORK_ERROR", statusCode: 0, requestId: null });
|
|
323
|
+
this.name = "ChatClientNetworkError";
|
|
324
|
+
this.originalError = opts.originalError;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
277
328
|
// src/chat/index.ts
|
|
278
329
|
var DEFAULT_BASE_URL = "https://www.cstar.help";
|
|
279
330
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
@@ -282,6 +333,7 @@ var ChatClient = class {
|
|
|
282
333
|
this.sessionToken = null;
|
|
283
334
|
this.teamId = null;
|
|
284
335
|
this.customerId = null;
|
|
336
|
+
this.authMode = null;
|
|
285
337
|
this.pollingTimers = /* @__PURE__ */ new Map();
|
|
286
338
|
this.realtimeManager = null;
|
|
287
339
|
this.presenceManager = null;
|
|
@@ -290,18 +342,17 @@ var ChatClient = class {
|
|
|
290
342
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
291
343
|
this.teamSlug = config.teamSlug;
|
|
292
344
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
293
|
-
this.
|
|
294
|
-
this.
|
|
295
|
-
this.conversations = new ConversationsApi(this);
|
|
345
|
+
this.realtimeEnabled = config.realtime !== false;
|
|
346
|
+
this.tickets = new TicketsApi(this);
|
|
296
347
|
this.messages = new MessagesApi(this);
|
|
297
348
|
this.community = new ChatCommunityApi(this);
|
|
298
349
|
}
|
|
299
350
|
/**
|
|
300
351
|
* Identify a customer using HMAC signature verification.
|
|
301
|
-
* Must be called before accessing
|
|
352
|
+
* Must be called before accessing tickets or messages.
|
|
302
353
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
354
|
+
* Real-time subscriptions are initialized automatically (non-blocking)
|
|
355
|
+
* unless `realtime: false` was passed in config.
|
|
305
356
|
*
|
|
306
357
|
* @param customer - Customer identity fields
|
|
307
358
|
* @param signature - HMAC-SHA256 hex signature of the signed fields
|
|
@@ -323,7 +374,7 @@ var ChatClient = class {
|
|
|
323
374
|
this.sessionToken = response.data.sessionToken;
|
|
324
375
|
this.teamId = response.data.teamId;
|
|
325
376
|
this.customerId = response.data.customer.id;
|
|
326
|
-
if (this.
|
|
377
|
+
if (this.realtimeEnabled) {
|
|
327
378
|
this.realtimeInitPromise = this.initRealtime(
|
|
328
379
|
response.data.customer.id,
|
|
329
380
|
response.data.customer.name || customer.name || customer.email
|
|
@@ -332,8 +383,72 @@ var ChatClient = class {
|
|
|
332
383
|
return response.data;
|
|
333
384
|
}
|
|
334
385
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
386
|
+
* Sign in with cStar's hosted password auth. The SDK keeps the resulting
|
|
387
|
+
* Supabase access token and uses it as Bearer for every subsequent call.
|
|
388
|
+
* Real-time bootstraps automatically (same flow as `identify()`).
|
|
389
|
+
*
|
|
390
|
+
* Use this when your customers don't have a session in your app and you
|
|
391
|
+
* want cStar to handle login. For the case where your app already has a
|
|
392
|
+
* Supabase session, use `loginWithSession({ accessToken })` instead.
|
|
393
|
+
*/
|
|
394
|
+
async loginWithCredentials(params) {
|
|
395
|
+
const teamId = await this.resolveTeamIdFromSlug();
|
|
396
|
+
const response = await this.fetch("/api/widget/auth/login", {
|
|
397
|
+
method: "POST",
|
|
398
|
+
body: { email: params.email, password: params.password, teamId }
|
|
399
|
+
});
|
|
400
|
+
if (!response.accessToken) {
|
|
401
|
+
throw new ChatClientAuthError({
|
|
402
|
+
message: "Login succeeded but no session was issued",
|
|
403
|
+
code: "NO_SESSION",
|
|
404
|
+
statusCode: 200,
|
|
405
|
+
requestId: null
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
await this.loginWithSession({
|
|
409
|
+
accessToken: response.accessToken,
|
|
410
|
+
teamId,
|
|
411
|
+
customerName: response.customer.name ?? response.customer.email
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
customer: {
|
|
415
|
+
id: this.customerId ?? response.customer.id,
|
|
416
|
+
email: response.customer.email,
|
|
417
|
+
name: response.customer.name
|
|
418
|
+
},
|
|
419
|
+
teamId
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Use an existing Supabase access token for widget calls. Skip the cStar
|
|
424
|
+
* login flow entirely; the SDK uses the supplied token as Bearer and
|
|
425
|
+
* bootstraps real-time the same way `identify()` does.
|
|
426
|
+
*
|
|
427
|
+
* The widget API verifies the token via the `widget-session` strategy on
|
|
428
|
+
* every request — there is no client-side validation here.
|
|
429
|
+
*/
|
|
430
|
+
async loginWithSession(params) {
|
|
431
|
+
this.sessionToken = params.accessToken;
|
|
432
|
+
if (params.teamId) {
|
|
433
|
+
this.teamId = params.teamId;
|
|
434
|
+
}
|
|
435
|
+
this.authMode = "session";
|
|
436
|
+
if (this.realtimeEnabled) {
|
|
437
|
+
const customerName = params.customerName ?? "";
|
|
438
|
+
this.realtimeInitPromise = this.initRealtimeFromSession(customerName);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Returns the auth mode the client is currently using, or `null` before
|
|
443
|
+
* any authenticated call has resolved one. Useful for branching UI
|
|
444
|
+
* (e.g. to show a "Sign out" button only in session mode).
|
|
445
|
+
*/
|
|
446
|
+
getAuthMode() {
|
|
447
|
+
return this.authMode;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Subscribe to new messages on a ticket.
|
|
451
|
+
* Uses real-time delivery when available, falls back to polling.
|
|
337
452
|
*
|
|
338
453
|
* @returns Unsubscribe function
|
|
339
454
|
*/
|
|
@@ -345,7 +460,7 @@ var ChatClient = class {
|
|
|
345
460
|
}
|
|
346
461
|
/**
|
|
347
462
|
* Subscribe to typing indicator events.
|
|
348
|
-
* Requires
|
|
463
|
+
* Requires real-time — returns a no-op when unavailable.
|
|
349
464
|
*
|
|
350
465
|
* @returns Unsubscribe function
|
|
351
466
|
*/
|
|
@@ -357,8 +472,8 @@ var ChatClient = class {
|
|
|
357
472
|
};
|
|
358
473
|
}
|
|
359
474
|
/**
|
|
360
|
-
* Send a customer typing indicator (requires
|
|
361
|
-
* No-op
|
|
475
|
+
* Send a customer typing indicator (requires real-time).
|
|
476
|
+
* No-op when real-time is unavailable.
|
|
362
477
|
*/
|
|
363
478
|
async sendTyping(ticketId, isTyping) {
|
|
364
479
|
if (this.presenceManager) {
|
|
@@ -384,8 +499,20 @@ var ChatClient = class {
|
|
|
384
499
|
this.sessionToken = null;
|
|
385
500
|
this.teamId = null;
|
|
386
501
|
this.customerId = null;
|
|
502
|
+
this.authMode = null;
|
|
387
503
|
}
|
|
388
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* @internal Make an authenticated request to the widget API.
|
|
506
|
+
*
|
|
507
|
+
* On any non-2xx response, throws a typed subclass of `ChatClientError`
|
|
508
|
+
* (`ChatClientAuthError`, `ChatClientRateLimitError`, etc.) so consumers
|
|
509
|
+
* can branch on error class instead of parsing message strings.
|
|
510
|
+
*
|
|
511
|
+
* On the way out, reads the `X-Cstar-Auth-Mode` response header (set by
|
|
512
|
+
* widgetHandler on every response) and caches it on the client. After
|
|
513
|
+
* the first authenticated round-trip, `getAuthMode()` returns the
|
|
514
|
+
* server-confirmed mode.
|
|
515
|
+
*/
|
|
389
516
|
async fetch(path, options) {
|
|
390
517
|
const url = this.buildUrl(path, options.query);
|
|
391
518
|
const headers = {
|
|
@@ -394,17 +521,104 @@ var ChatClient = class {
|
|
|
394
521
|
if (this.sessionToken) {
|
|
395
522
|
headers["Authorization"] = `Bearer ${this.sessionToken}`;
|
|
396
523
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
524
|
+
let response;
|
|
525
|
+
try {
|
|
526
|
+
response = await globalThis.fetch(url, {
|
|
527
|
+
method: options.method,
|
|
528
|
+
headers,
|
|
529
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
530
|
+
});
|
|
531
|
+
} catch (networkErr) {
|
|
532
|
+
throw new ChatClientNetworkError({
|
|
533
|
+
message: networkErr instanceof Error ? networkErr.message : "Network request failed",
|
|
534
|
+
originalError: networkErr
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const responseHeaders = response.headers && typeof response.headers.get === "function" ? response.headers : null;
|
|
538
|
+
const headerMode = responseHeaders?.get("X-Cstar-Auth-Mode") ?? null;
|
|
539
|
+
if (headerMode === "hmac" || headerMode === "session") {
|
|
540
|
+
this.authMode = headerMode;
|
|
541
|
+
}
|
|
542
|
+
const requestId = responseHeaders?.get("X-Request-Id") ?? null;
|
|
402
543
|
if (!response.ok) {
|
|
403
|
-
const errorBody = await response.json().catch(() => ({ error: { message: "Request failed" } }));
|
|
404
|
-
|
|
544
|
+
const errorBody = await response.json().catch(() => ({ error: { code: "UNKNOWN", message: "Request failed" } }));
|
|
545
|
+
const code = errorBody.error?.code ?? "UNKNOWN";
|
|
546
|
+
const message = errorBody.error?.message ?? `Widget API error: ${response.status}`;
|
|
547
|
+
if (response.status === 401 || response.status === 403) {
|
|
548
|
+
throw new ChatClientAuthError({
|
|
549
|
+
message,
|
|
550
|
+
code,
|
|
551
|
+
statusCode: response.status,
|
|
552
|
+
requestId
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (response.status === 429) {
|
|
556
|
+
const retryAfterHeader = responseHeaders?.get("Retry-After") ?? null;
|
|
557
|
+
const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : null;
|
|
558
|
+
throw new ChatClientRateLimitError({
|
|
559
|
+
message,
|
|
560
|
+
code,
|
|
561
|
+
statusCode: response.status,
|
|
562
|
+
requestId,
|
|
563
|
+
retryAfter: Number.isFinite(retryAfter ?? NaN) ? retryAfter : null
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
if (response.status >= 500) {
|
|
567
|
+
throw new ChatClientServerError({
|
|
568
|
+
message,
|
|
569
|
+
code,
|
|
570
|
+
statusCode: response.status,
|
|
571
|
+
requestId
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
throw new ChatClientError(message, {
|
|
575
|
+
code,
|
|
576
|
+
statusCode: response.status,
|
|
577
|
+
requestId
|
|
578
|
+
});
|
|
405
579
|
}
|
|
406
580
|
return response.json();
|
|
407
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Look up the team UUID from the configured `teamSlug`. Used by
|
|
584
|
+
* `loginWithCredentials`. HMAC integrators (`identify()`) never hit
|
|
585
|
+
* this path — verify-identity accepts the slug directly.
|
|
586
|
+
*
|
|
587
|
+
* @internal
|
|
588
|
+
*/
|
|
589
|
+
async resolveTeamIdFromSlug() {
|
|
590
|
+
const response = await this.fetch(
|
|
591
|
+
"/api/widget/team-by-slug",
|
|
592
|
+
{
|
|
593
|
+
method: "GET",
|
|
594
|
+
query: { teamSlug: this.teamSlug }
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
return response.data.teamId;
|
|
598
|
+
}
|
|
599
|
+
/** @internal Bootstrap realtime from a session login (no precomputed customerId). */
|
|
600
|
+
async initRealtimeFromSession(customerName) {
|
|
601
|
+
try {
|
|
602
|
+
this.realtimeManager = new RealtimeManager({
|
|
603
|
+
fetchToken: () => this.fetchRealtimeToken()
|
|
604
|
+
});
|
|
605
|
+
const ready = await this.realtimeManager.init();
|
|
606
|
+
if (!ready) return;
|
|
607
|
+
this.realtimeReady = true;
|
|
608
|
+
const token = this.realtimeManager.getCurrentToken();
|
|
609
|
+
const customerId = token ? decodeJwtSub(token) : null;
|
|
610
|
+
if (customerId) {
|
|
611
|
+
this.customerId = customerId;
|
|
612
|
+
this.presenceManager = new PresenceManager({ customerId, customerName });
|
|
613
|
+
const client = this.realtimeManager.getClient();
|
|
614
|
+
if (client) {
|
|
615
|
+
this.presenceManager.setClient(client);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
this.realtimeReady = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
408
622
|
/** @internal Get the stored team ID for requests that need it. */
|
|
409
623
|
getTeamId() {
|
|
410
624
|
return this.teamId;
|
|
@@ -422,8 +636,6 @@ var ChatClient = class {
|
|
|
422
636
|
async initRealtime(customerId, customerName) {
|
|
423
637
|
try {
|
|
424
638
|
this.realtimeManager = new RealtimeManager({
|
|
425
|
-
supabaseUrl: this.supabaseUrl,
|
|
426
|
-
supabaseAnonKey: this.supabaseAnonKey,
|
|
427
639
|
fetchToken: () => this.fetchRealtimeToken()
|
|
428
640
|
});
|
|
429
641
|
const ready = await this.realtimeManager.init();
|
|
@@ -490,28 +702,29 @@ var ChatClient = class {
|
|
|
490
702
|
return qs ? `${base}?${qs}` : base;
|
|
491
703
|
}
|
|
492
704
|
};
|
|
493
|
-
var
|
|
705
|
+
var TicketsApi = class {
|
|
494
706
|
constructor(chat) {
|
|
495
707
|
this.chat = chat;
|
|
496
708
|
}
|
|
497
|
-
/** List
|
|
709
|
+
/** List tickets for the identified customer. */
|
|
498
710
|
async list(params) {
|
|
499
711
|
const teamId = this.chat.getTeamId();
|
|
500
712
|
if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
|
|
501
|
-
const response = await this.chat.fetch(
|
|
502
|
-
"
|
|
503
|
-
{
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
limit: params?.limit?.toString(),
|
|
508
|
-
cursor: params?.cursor
|
|
509
|
-
}
|
|
713
|
+
const response = await this.chat.fetch("/api/widget/conversations", {
|
|
714
|
+
method: "GET",
|
|
715
|
+
query: {
|
|
716
|
+
teamId,
|
|
717
|
+
limit: params?.limit?.toString(),
|
|
718
|
+
cursor: params?.cursor
|
|
510
719
|
}
|
|
511
|
-
);
|
|
512
|
-
return
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
tickets: response.data.tickets ?? response.data.conversations ?? [],
|
|
723
|
+
cursor: response.data.cursor,
|
|
724
|
+
hasMore: response.data.hasMore
|
|
725
|
+
};
|
|
513
726
|
}
|
|
514
|
-
/** Create a new
|
|
727
|
+
/** Create a new ticket. */
|
|
515
728
|
async create(params) {
|
|
516
729
|
const teamId = this.chat.getTeamId();
|
|
517
730
|
if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
|
|
@@ -669,7 +882,7 @@ var MessagesApi = class {
|
|
|
669
882
|
constructor(chat) {
|
|
670
883
|
this.chat = chat;
|
|
671
884
|
}
|
|
672
|
-
/** List messages for a
|
|
885
|
+
/** List messages for a ticket. */
|
|
673
886
|
async list(ticketId) {
|
|
674
887
|
const teamId = this.chat.getTeamId();
|
|
675
888
|
const response = await this.chat.fetch(
|
|
@@ -684,7 +897,7 @@ var MessagesApi = class {
|
|
|
684
897
|
);
|
|
685
898
|
return response.data.messages;
|
|
686
899
|
}
|
|
687
|
-
/** Send a message to a
|
|
900
|
+
/** Send a message to a ticket. */
|
|
688
901
|
async send(ticketId, content) {
|
|
689
902
|
const teamId = this.chat.getTeamId();
|
|
690
903
|
const response = await this.chat.fetch(
|
|
@@ -701,7 +914,26 @@ var MessagesApi = class {
|
|
|
701
914
|
return response.data.message;
|
|
702
915
|
}
|
|
703
916
|
};
|
|
917
|
+
function decodeJwtSub(jwt) {
|
|
918
|
+
try {
|
|
919
|
+
const parts = jwt.split(".");
|
|
920
|
+
if (parts.length !== 3) return null;
|
|
921
|
+
const payloadB64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
922
|
+
const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
|
|
923
|
+
const payload = JSON.parse(
|
|
924
|
+
typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8")
|
|
925
|
+
);
|
|
926
|
+
return typeof payload.sub === "string" ? payload.sub : null;
|
|
927
|
+
} catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
704
931
|
// Annotate the CommonJS export names for ESM import in node:
|
|
705
932
|
0 && (module.exports = {
|
|
706
|
-
ChatClient
|
|
933
|
+
ChatClient,
|
|
934
|
+
ChatClientAuthError,
|
|
935
|
+
ChatClientError,
|
|
936
|
+
ChatClientNetworkError,
|
|
937
|
+
ChatClientRateLimitError,
|
|
938
|
+
ChatClientServerError
|
|
707
939
|
});
|