@chatlychat/sdk-js 0.0.0-stage → 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/index.d.ts +175 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +99 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +158 -0
- package/dist/live.js.map +1 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser SDK. Pairs with the widget loader to give product code a way to
|
|
3
|
+
* identify visitors, fire CDP events, and control the widget.
|
|
4
|
+
*
|
|
5
|
+
* import { Chatly } from '@chatlychat/sdk-js';
|
|
6
|
+
* const lc = Chatly.init({ publicId: 'acme-web', apiUrl: 'https://api.acme.com' });
|
|
7
|
+
* await lc.identify({ email: 'a@b.com', name: 'Alice' });
|
|
8
|
+
* await lc.track('signup_completed', { plan: 'pro' });
|
|
9
|
+
* lc.open();
|
|
10
|
+
*/
|
|
11
|
+
import { createTypingTracker, newMessagesSince, typingActorOf } from './live.js';
|
|
12
|
+
export { TYPING_STOP_AFTER_MS } from './live.js';
|
|
13
|
+
export class Chatly {
|
|
14
|
+
opts;
|
|
15
|
+
base;
|
|
16
|
+
visitorToken;
|
|
17
|
+
/**
|
|
18
|
+
* The realtime host `/v1/widget/init` reported, remembered for `subscribe`.
|
|
19
|
+
*
|
|
20
|
+
* `/v1/widget/realtime/token` returns `{ token, expiresAt }` and NO url —
|
|
21
|
+
* the first version of `subscribe` read a `url` off it and silently fell
|
|
22
|
+
* back to the API host, which is not where the socket server is.
|
|
23
|
+
*/
|
|
24
|
+
realtimeUrl = null;
|
|
25
|
+
/**
|
|
26
|
+
* This visitor's contact id, remembered for the typing event — the realtime
|
|
27
|
+
* gateway's payload requires an `actorId` and the visitor's is their contact.
|
|
28
|
+
*/
|
|
29
|
+
contactId = null;
|
|
30
|
+
static init(opts) {
|
|
31
|
+
return new Chatly(opts);
|
|
32
|
+
}
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
/*
|
|
36
|
+
* Production, not a dev hostname.
|
|
37
|
+
*
|
|
38
|
+
* This was `https://api.livechat.local`, which resolves nowhere — so a
|
|
39
|
+
* developer who followed the published docs and omitted `apiUrl` got an
|
|
40
|
+
* SDK whose every call failed, and the docs page carried a warning
|
|
41
|
+
* telling each reader to override it. Reported by a customer who
|
|
42
|
+
* integrated from the docs alone, not caught here, because every test in
|
|
43
|
+
* this package passes an explicit `apiUrl`.
|
|
44
|
+
*
|
|
45
|
+
* A self-hosted install still overrides it; that is the only caller that
|
|
46
|
+
* ever should.
|
|
47
|
+
*/
|
|
48
|
+
this.base = opts.apiUrl ?? 'https://api.chatlychat.com';
|
|
49
|
+
// Storage key deliberately left as `livechat:` — the internal scope name.
|
|
50
|
+
// Renaming it would orphan the visitor token of every browser that has
|
|
51
|
+
// one, silently signing those visitors out of their own conversation.
|
|
52
|
+
// Cosmetics are not worth that; the customer-visible strings are what
|
|
53
|
+
// matter and those say Chatly.
|
|
54
|
+
this.visitorToken = localStorage.getItem(`livechat:vt:${opts.publicId}`);
|
|
55
|
+
}
|
|
56
|
+
open() {
|
|
57
|
+
window.Chatly?.open?.();
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
window.Chatly?.close?.();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Attach a known identity to this visitor.
|
|
64
|
+
*
|
|
65
|
+
* Re-initialises the session with an `identity` payload rather than calling
|
|
66
|
+
* a dedicated endpoint. There has never been a `/v1/widget/identify` route —
|
|
67
|
+
* this method has 404'd since it was written, as have the Kotlin and Swift
|
|
68
|
+
* equivalents. Identity is accepted by `init`, which is also where the
|
|
69
|
+
* `userHash` check for verified identity lives, so routing through it means
|
|
70
|
+
* identification and verification cannot drift apart.
|
|
71
|
+
*
|
|
72
|
+
* `identify({ email })` with no `externalId` — the exact call this file's
|
|
73
|
+
* own header comment demonstrates — used to be rejected by the server with
|
|
74
|
+
* a 400, because `widgetInitSchema` required `externalId`. It no longer is;
|
|
75
|
+
* an email-only identity attaches a reply address to whoever this browser
|
|
76
|
+
* already is. It cannot be *verified*, though: `userHash` binds the external
|
|
77
|
+
* id and there is nothing to bind without one.
|
|
78
|
+
*
|
|
79
|
+
* No longer mints an anonymous session first. That cost a round trip, and
|
|
80
|
+
* worse, it was pointless: the token the server returns HERE is the
|
|
81
|
+
* identified one, and it is stored below. Before, the SDK kept the
|
|
82
|
+
* anonymous token it had just fetched, so every later `track` or `send`
|
|
83
|
+
* spoke for the anonymous visitor and the identification was quietly lost.
|
|
84
|
+
*/
|
|
85
|
+
async identify(input) {
|
|
86
|
+
const res = await this.request('/v1/widget/init', {
|
|
87
|
+
publicId: this.opts.publicId,
|
|
88
|
+
...(this.visitorToken === null ? {} : { visitorToken: this.visitorToken }),
|
|
89
|
+
identity: input,
|
|
90
|
+
});
|
|
91
|
+
const body = (await res.json());
|
|
92
|
+
if (typeof body.realtimeUrl === 'string' && body.realtimeUrl.length > 0) {
|
|
93
|
+
this.realtimeUrl = body.realtimeUrl;
|
|
94
|
+
}
|
|
95
|
+
// The session token is now an opaque server-issued credential rather than
|
|
96
|
+
// the external id, and `init` rotates it on every call — so it has to be
|
|
97
|
+
// taken from the response, not assumed to be unchanged.
|
|
98
|
+
const token = body.session?.token;
|
|
99
|
+
if (typeof token === 'string' && token.length > 0) {
|
|
100
|
+
this.setVisitorToken(token);
|
|
101
|
+
}
|
|
102
|
+
const contactId = body.session?.contactId ?? '';
|
|
103
|
+
if (contactId.length > 0) {
|
|
104
|
+
/*
|
|
105
|
+
* Remembered here as well as in `ensureVisitorToken`, because `identify`
|
|
106
|
+
* can be the FIRST call a session makes and `ensureVisitorToken` returns
|
|
107
|
+
* early when a token is already in localStorage. A returning browser
|
|
108
|
+
* therefore never learned its own contact id, and `setTyping` sent
|
|
109
|
+
* `actorId: 'unknown'` — which the realtime gateway now overwrites from
|
|
110
|
+
* the JWT anyway, but a field we can fill correctly should not be a
|
|
111
|
+
* placeholder.
|
|
112
|
+
*/
|
|
113
|
+
this.contactId = contactId;
|
|
114
|
+
}
|
|
115
|
+
return { contactId };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Upload a file as this visitor and return the attachment record.
|
|
119
|
+
*
|
|
120
|
+
* The two-step presign + direct PUT is deliberate: the file never passes
|
|
121
|
+
* through the API, which would mean proxying it in memory only to write it
|
|
122
|
+
* to object storage anyway.
|
|
123
|
+
*/
|
|
124
|
+
async uploadFile(file) {
|
|
125
|
+
await this.ensureVisitorToken();
|
|
126
|
+
const res = await this.request('/v1/widget/uploads/presign', {
|
|
127
|
+
publicId: this.opts.publicId,
|
|
128
|
+
visitorToken: this.visitorToken,
|
|
129
|
+
filename: file.name,
|
|
130
|
+
contentType: file.type || 'application/octet-stream',
|
|
131
|
+
sizeBytes: file.size,
|
|
132
|
+
});
|
|
133
|
+
const presigned = (await res.json());
|
|
134
|
+
const put = await fetch(presigned.url, {
|
|
135
|
+
method: presigned.method,
|
|
136
|
+
headers: presigned.headers,
|
|
137
|
+
body: file,
|
|
138
|
+
});
|
|
139
|
+
if (!put.ok) {
|
|
140
|
+
throw new Error(`Upload failed (${put.status})`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id: presigned.uploadId,
|
|
144
|
+
filename: file.name,
|
|
145
|
+
mimeType: file.type || 'application/octet-stream',
|
|
146
|
+
sizeBytes: file.size,
|
|
147
|
+
url: presigned.publicUrl,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Send a message, optionally with attachments from `uploadFile`. */
|
|
151
|
+
async send(conversationId, body, attachments = []) {
|
|
152
|
+
await this.ensureVisitorToken();
|
|
153
|
+
await this.request('/v1/widget/messages', {
|
|
154
|
+
publicId: this.opts.publicId,
|
|
155
|
+
visitorToken: this.visitorToken,
|
|
156
|
+
conversationId,
|
|
157
|
+
body,
|
|
158
|
+
attachments,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// ── Headless: read ────────────────────────────────────────────────────
|
|
162
|
+
//
|
|
163
|
+
// Everything below exists so a consumer can build their OWN chat UI. Before
|
|
164
|
+
// it, the SDK could `send` into a conversation but could not create one,
|
|
165
|
+
// list threads, read history or learn that a reply had arrived — so it was a
|
|
166
|
+
// companion to the widget, not a client you could render from.
|
|
167
|
+
/**
|
|
168
|
+
* This visitor's own conversations, most recent first.
|
|
169
|
+
*
|
|
170
|
+
* Scoped by the visitor token, so it is exactly the threads this browser is
|
|
171
|
+
* entitled to and never anyone else's.
|
|
172
|
+
*/
|
|
173
|
+
async listConversations(limit = 20) {
|
|
174
|
+
await this.ensureVisitorToken();
|
|
175
|
+
const res = await this.request('/v1/widget/conversations/list', {
|
|
176
|
+
publicId: this.opts.publicId,
|
|
177
|
+
visitorToken: this.visitorToken,
|
|
178
|
+
limit,
|
|
179
|
+
});
|
|
180
|
+
const body = (await res.json());
|
|
181
|
+
return body.conversations ?? [];
|
|
182
|
+
}
|
|
183
|
+
/** The messages of one conversation, oldest first. */
|
|
184
|
+
async listMessages(conversationId, limit = 50) {
|
|
185
|
+
await this.ensureVisitorToken();
|
|
186
|
+
const res = await this.request('/v1/widget/messages/list', {
|
|
187
|
+
publicId: this.opts.publicId,
|
|
188
|
+
visitorToken: this.visitorToken,
|
|
189
|
+
conversationId,
|
|
190
|
+
limit,
|
|
191
|
+
});
|
|
192
|
+
const body = (await res.json());
|
|
193
|
+
return body.messages ?? [];
|
|
194
|
+
}
|
|
195
|
+
// ── Headless: write ───────────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Open a NEW conversation with a first message.
|
|
198
|
+
*
|
|
199
|
+
* `send` replies to one that exists and requires a `conversationId`; this is
|
|
200
|
+
* the other endpoint. Getting them the wrong way round is a 400 complaining
|
|
201
|
+
* about a missing `conversationId`, which is how the repo's own E2E spec was
|
|
202
|
+
* written and why it could never pass.
|
|
203
|
+
*/
|
|
204
|
+
async startConversation(body, attachments = []) {
|
|
205
|
+
await this.ensureVisitorToken();
|
|
206
|
+
const res = await this.request('/v1/widget/conversations', {
|
|
207
|
+
publicId: this.opts.publicId,
|
|
208
|
+
visitorToken: this.visitorToken,
|
|
209
|
+
body,
|
|
210
|
+
attachments,
|
|
211
|
+
});
|
|
212
|
+
const parsed = (await res.json());
|
|
213
|
+
return { conversationId: parsed.conversation?.id ?? '' };
|
|
214
|
+
}
|
|
215
|
+
// ── Headless: live ────────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Call `onMessage` for every message this subscription has not already
|
|
218
|
+
* given you. Returns the unsubscribe function.
|
|
219
|
+
*
|
|
220
|
+
* **Polling by default**, because the alternative is making every consumer
|
|
221
|
+
* ship `socket.io-client` whether they want push or not. Pass your own
|
|
222
|
+
* factory for push:
|
|
223
|
+
*
|
|
224
|
+
* import { io } from 'socket.io-client';
|
|
225
|
+
* const stop = chatly.subscribe(id, { onMessage }, { io });
|
|
226
|
+
*
|
|
227
|
+
* Errors go to `onError`, never a rejected promise: this is long-lived, and
|
|
228
|
+
* a rejection nobody awaits is an unhandled rejection on the customer's own
|
|
229
|
+
* page.
|
|
230
|
+
*/
|
|
231
|
+
subscribe(conversationId, handlers, options = {}) {
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
let stopped = false;
|
|
234
|
+
/** Set once the push socket is up; null on the polling path. */
|
|
235
|
+
let live = null;
|
|
236
|
+
let warnedAboutTyping = false;
|
|
237
|
+
/*
|
|
238
|
+
* Receiving typing needs the same transport as sending it, and for the
|
|
239
|
+
* same reason: there is no HTTP route. A consumer who asked for
|
|
240
|
+
* `onTyping` on the polling path has asked for something the transport
|
|
241
|
+
* cannot carry, so say it once at subscribe time — the alternative is an
|
|
242
|
+
* agent typing visibly in the dashboard and nothing happening here, with
|
|
243
|
+
* nothing to search for.
|
|
244
|
+
*/
|
|
245
|
+
if (handlers.onTyping !== undefined && options.io === undefined) {
|
|
246
|
+
console.warn('[Chatly] onTyping needs the push path — pass `io` to subscribe(). ' +
|
|
247
|
+
'Typing is a socket event and has no HTTP route, so a polling ' +
|
|
248
|
+
'subscription never receives one.');
|
|
249
|
+
}
|
|
250
|
+
const typing = createTypingTracker({
|
|
251
|
+
onChange: (signal) => handlers.onTyping?.(signal),
|
|
252
|
+
});
|
|
253
|
+
const setTyping = (isTyping) => {
|
|
254
|
+
if (live === null) {
|
|
255
|
+
if (!warnedAboutTyping) {
|
|
256
|
+
warnedAboutTyping = true;
|
|
257
|
+
console.warn('[Chatly] setTyping needs the push path — pass `io` to subscribe(). ' +
|
|
258
|
+
'Typing is a socket event and has no HTTP route.');
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
live.emit(isTyping ? 'typing.start' : 'typing.stop', {
|
|
263
|
+
conversationId,
|
|
264
|
+
actorType: 'contact',
|
|
265
|
+
// The gateway's payload requires a non-empty actorId. `unknown` rather
|
|
266
|
+
// than an empty string, which its schema rejects outright.
|
|
267
|
+
actorId: this.contactId ?? 'unknown',
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const deliver = (batch) => {
|
|
271
|
+
for (const message of newMessagesSince(seen, batch)) {
|
|
272
|
+
handlers.onMessage?.(message);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const poll = async () => {
|
|
276
|
+
if (stopped) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
deliver(await this.listMessages(conversationId));
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
handlers.onError?.(err);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
// The first batch is the history, which is also how a consumer gets the
|
|
287
|
+
// existing thread without a separate call.
|
|
288
|
+
void poll();
|
|
289
|
+
if (options.io === undefined) {
|
|
290
|
+
// Floored: a one-second poll on a long conversation is a request every
|
|
291
|
+
// second per open tab, and the endpoint is not a stream.
|
|
292
|
+
const interval = Math.max(2_000, options.intervalMs ?? 6_000);
|
|
293
|
+
const timer = setInterval(() => void poll(), interval);
|
|
294
|
+
return Object.assign(() => {
|
|
295
|
+
stopped = true;
|
|
296
|
+
clearInterval(timer);
|
|
297
|
+
// Nothing can have started the tracker on this path — but a
|
|
298
|
+
// teardown that only cleans up the branch it remembers is how a
|
|
299
|
+
// timer outlives its subscription.
|
|
300
|
+
typing.dispose();
|
|
301
|
+
}, { setTyping });
|
|
302
|
+
}
|
|
303
|
+
let socket = null;
|
|
304
|
+
// Captured before the async gap so the narrowing holds without an
|
|
305
|
+
// assertion: `options` is readonly, but the checker cannot know that
|
|
306
|
+
// across an await.
|
|
307
|
+
const makeSocket = options.io;
|
|
308
|
+
void (async () => {
|
|
309
|
+
try {
|
|
310
|
+
await this.ensureVisitorToken();
|
|
311
|
+
const res = await this.request('/v1/widget/realtime/token', {
|
|
312
|
+
publicId: this.opts.publicId,
|
|
313
|
+
visitorToken: this.visitorToken,
|
|
314
|
+
conversationId,
|
|
315
|
+
});
|
|
316
|
+
const { token } = (await res.json());
|
|
317
|
+
const host = this.opts.realtimeUrl ?? this.realtimeUrl;
|
|
318
|
+
if (stopped || typeof token !== 'string' || host === null || host === undefined) {
|
|
319
|
+
/*
|
|
320
|
+
* No host means no socket. Reported rather than silently falling
|
|
321
|
+
* back, because a consumer who passed `io` asked for push and a
|
|
322
|
+
* quiet downgrade to polling would look like push that is slow.
|
|
323
|
+
*/
|
|
324
|
+
handlers.onError?.(new Error('chatly: no realtime host — set `realtimeUrl` in init options'));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const made = makeSocket(host, {
|
|
328
|
+
path: '/socket.io',
|
|
329
|
+
transports: ['websocket'],
|
|
330
|
+
auth: { token, kind: 'visitor' },
|
|
331
|
+
});
|
|
332
|
+
socket = made;
|
|
333
|
+
live = made;
|
|
334
|
+
made.on('event', (raw) => {
|
|
335
|
+
const envelope = raw;
|
|
336
|
+
if (envelope?.type === 'message.created' && envelope.payload !== undefined) {
|
|
337
|
+
deliver([envelope.payload]);
|
|
338
|
+
/*
|
|
339
|
+
* A reply has arrived, so whoever was typing has stopped — the
|
|
340
|
+
* dashboard sends `typing.stop` on send, but the message and the
|
|
341
|
+
* stop are two envelopes and the message usually wins the race.
|
|
342
|
+
* Without this the indicator sits under the message that was
|
|
343
|
+
* being typed, for the length of the timeout.
|
|
344
|
+
*/
|
|
345
|
+
typing.stop();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (envelope?.type === 'typing.start' || envelope?.type === 'typing.stop') {
|
|
349
|
+
const actor = typingActorOf(envelope.payload);
|
|
350
|
+
if (actor === null) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (envelope.type === 'typing.start') {
|
|
354
|
+
typing.start(actor);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
typing.stop(actor);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
/*
|
|
364
|
+
* Push could not be set up. Fall back to polling rather than leaving
|
|
365
|
+
* the consumer with a subscription that silently never fires — the
|
|
366
|
+
* same choice the widget makes when its realtime chunk cannot load.
|
|
367
|
+
*/
|
|
368
|
+
handlers.onError?.(err);
|
|
369
|
+
if (!stopped) {
|
|
370
|
+
const timer = setInterval(() => void poll(), 6_000);
|
|
371
|
+
socket = { disconnect: () => clearInterval(timer) };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
})();
|
|
375
|
+
return Object.assign(() => {
|
|
376
|
+
stopped = true;
|
|
377
|
+
live = null;
|
|
378
|
+
typing.dispose();
|
|
379
|
+
socket?.disconnect();
|
|
380
|
+
}, { setTyping });
|
|
381
|
+
}
|
|
382
|
+
/** Submit a post-chat satisfaction score (1-5, or 0-10 for NPS). */
|
|
383
|
+
async rate(conversationId, score, comment) {
|
|
384
|
+
await this.ensureVisitorToken();
|
|
385
|
+
await this.request('/v1/widget/csat', {
|
|
386
|
+
publicId: this.opts.publicId,
|
|
387
|
+
visitorToken: this.visitorToken,
|
|
388
|
+
conversationId,
|
|
389
|
+
score,
|
|
390
|
+
...(comment === undefined ? {} : { comment }),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Email this visitor a transcript of their conversation. */
|
|
394
|
+
async emailTranscript(conversationId, email) {
|
|
395
|
+
await this.ensureVisitorToken();
|
|
396
|
+
await this.request('/v1/widget/transcript', {
|
|
397
|
+
publicId: this.opts.publicId,
|
|
398
|
+
visitorToken: this.visitorToken,
|
|
399
|
+
conversationId,
|
|
400
|
+
email,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async track(name, properties) {
|
|
404
|
+
await this.ensureVisitorToken();
|
|
405
|
+
await this.request('/v1/widget/events', {
|
|
406
|
+
publicId: this.opts.publicId,
|
|
407
|
+
visitorToken: this.visitorToken,
|
|
408
|
+
name,
|
|
409
|
+
properties: properties ?? {},
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
setVisitorToken(token) {
|
|
413
|
+
this.visitorToken = token;
|
|
414
|
+
localStorage.setItem(`livechat:vt:${this.opts.publicId}`, token);
|
|
415
|
+
}
|
|
416
|
+
async ensureVisitorToken() {
|
|
417
|
+
if (this.visitorToken !== null) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const res = await this.request('/v1/widget/init', { publicId: this.opts.publicId });
|
|
421
|
+
const body = (await res.json());
|
|
422
|
+
this.setVisitorToken(body.session.token);
|
|
423
|
+
if (typeof body.session.contactId === 'string' && body.session.contactId.length > 0) {
|
|
424
|
+
this.contactId = body.session.contactId;
|
|
425
|
+
}
|
|
426
|
+
/*
|
|
427
|
+
* The realtime host is reported here as well as by `identify`, and this is
|
|
428
|
+
* the path `subscribe` bootstraps through — an anonymous visitor never
|
|
429
|
+
* calls `identify`. Captured in both, because either can be the first
|
|
430
|
+
* init a session ever makes.
|
|
431
|
+
*/
|
|
432
|
+
if (typeof body.realtimeUrl === 'string' && body.realtimeUrl.length > 0) {
|
|
433
|
+
this.realtimeUrl = body.realtimeUrl;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async request(path, body, isRetry = false) {
|
|
437
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
headers: {
|
|
440
|
+
'content-type': 'application/json',
|
|
441
|
+
// Production API requires Idempotency-Key on every write; mint
|
|
442
|
+
// a fresh UUID per call so a stray fetch-retry never duplicates.
|
|
443
|
+
'idempotency-key': newIdempotencyKey(),
|
|
444
|
+
},
|
|
445
|
+
body: JSON.stringify(body),
|
|
446
|
+
});
|
|
447
|
+
if (!res.ok) {
|
|
448
|
+
if (res.status === 403 && !isRetry && (await isStaleSession(res))) {
|
|
449
|
+
// The stored token predates signed visitor sessions, or expired. The
|
|
450
|
+
// integrator's code did nothing wrong and should not have to handle
|
|
451
|
+
// it: drop the dead token, get a fresh one, replay the call once.
|
|
452
|
+
this.visitorToken = null;
|
|
453
|
+
localStorage.removeItem(`livechat:vt:${this.opts.publicId}`);
|
|
454
|
+
await this.ensureVisitorToken();
|
|
455
|
+
const replayed = { ...body };
|
|
456
|
+
if (replayed['visitorToken'] !== undefined) {
|
|
457
|
+
replayed['visitorToken'] = this.visitorToken;
|
|
458
|
+
}
|
|
459
|
+
return this.request(path, replayed, true);
|
|
460
|
+
}
|
|
461
|
+
throw new Error(`Chatly SDK request failed: ${res.status}`);
|
|
462
|
+
}
|
|
463
|
+
return res;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Is this 403 "your session token is no good", rather than one of the other
|
|
468
|
+
* things a widget call can be forbidden for (a disallowed origin, a missing
|
|
469
|
+
* identity hash)? Only the session codes are cured by fetching a new token.
|
|
470
|
+
*/
|
|
471
|
+
/**
|
|
472
|
+
* Does this 403 mean "your session token is no good"?
|
|
473
|
+
*
|
|
474
|
+
* This read `body.code` at the top level, and the API has never sent that
|
|
475
|
+
* shape: `app-exception.filter.ts` wraps every error as
|
|
476
|
+
* `{ error: { code, message, details } }`. So it was `undefined` for every
|
|
477
|
+
* real response, this returned false every time, and **the session-recovery
|
|
478
|
+
* path below never fired** — a visitor holding an expired token got failures
|
|
479
|
+
* the SDK was written to absorb.
|
|
480
|
+
*
|
|
481
|
+
* The widget had the identical bug, fixed on 2026-10-08, and nobody checked
|
|
482
|
+
* its sibling. Both shapes are read now: the nested one the API sends, and
|
|
483
|
+
* the flat one in case anything ever answers that way.
|
|
484
|
+
*/
|
|
485
|
+
async function isStaleSession(res) {
|
|
486
|
+
try {
|
|
487
|
+
const body = (await res.json());
|
|
488
|
+
const code = body.error?.code ?? body.code;
|
|
489
|
+
return typeof code === 'string' && code.startsWith('visitor_session_');
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function newIdempotencyKey() {
|
|
496
|
+
// crypto.randomUUID is in every browser the widget supports
|
|
497
|
+
// (Chrome 92+, Safari 15.4+, Firefox 95+).
|
|
498
|
+
return globalThis.crypto.randomUUID();
|
|
499
|
+
}
|
|
500
|
+
export default Chatly;
|
|
501
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live half of the headless SDK.
|
|
3
|
+
*
|
|
4
|
+
* A consumer building their own chat UI needs messages as they arrive. Two
|
|
5
|
+
* ways, and the SDK does not require the heavier one:
|
|
6
|
+
*
|
|
7
|
+
* - **Polling**, the default. Zero dependencies, works everywhere, a few
|
|
8
|
+
* seconds of latency.
|
|
9
|
+
* - **Push**, by handing `subscribe()` your own socket.io-client factory.
|
|
10
|
+
* The widget does the same thing with a deferred chunk; the SDK will not
|
|
11
|
+
* take the dependency on a consumer's behalf, because a React Native app
|
|
12
|
+
* or a worker may have no use for it and every byte would still ship.
|
|
13
|
+
*
|
|
14
|
+
* This file holds the part worth testing on its own: deciding what is new.
|
|
15
|
+
*/
|
|
16
|
+
import type { HeadlessMessage, TypingSignal } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* The messages in `batch` that `seen` has not yet been told about, oldest
|
|
19
|
+
* first, with `seen` updated.
|
|
20
|
+
*
|
|
21
|
+
* Ordered by `createdAt` rather than by the order the server returned: a list
|
|
22
|
+
* endpoint is free to answer newest-first, and a consumer appending in
|
|
23
|
+
* receive order would render the conversation upside down.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT the widget's `mergePolled`. That one reconciles optimistic
|
|
26
|
+
* `local-*` rows against their server copies, which only matters to a UI that
|
|
27
|
+
* renders a message before the server has one. A headless consumer owns that
|
|
28
|
+
* decision themselves; all they need from us is "what is new".
|
|
29
|
+
*/
|
|
30
|
+
export declare function newMessagesSince(seen: Set<string>, batch: readonly HeadlessMessage[]): HeadlessMessage[];
|
|
31
|
+
/**
|
|
32
|
+
* How long a "typing" indicator survives without another `typing.start`.
|
|
33
|
+
*
|
|
34
|
+
* ── Why the SDK owns this timeout and not the consumer ──────────────────
|
|
35
|
+
*
|
|
36
|
+
* `typing.stop` is best-effort. The dashboard emits one after 5s of idle
|
|
37
|
+
* keystrokes and on send, and its hook emits one on unmount — but an agent
|
|
38
|
+
* who closes the laptop mid-sentence, loses their network, or whose tab is
|
|
39
|
+
* killed sends nothing at all. A consumer rendering the raw start/stop pair
|
|
40
|
+
* gets an indicator that says "Sarah is typing…" forever, which is the exact
|
|
41
|
+
* shape of bug this codebase keeps finding: it appears to work.
|
|
42
|
+
*
|
|
43
|
+
* Leaving it to the consumer means every consumer writes this same timer, and
|
|
44
|
+
* the ones who do not ship the stuck indicator. 3,500ms is the widget's own
|
|
45
|
+
* window (`use-conversation-realtime.tsx`), so a custom UI built on the SDK
|
|
46
|
+
* behaves like the widget rather than subtly differently.
|
|
47
|
+
*/
|
|
48
|
+
export declare const TYPING_STOP_AFTER_MS = 3500;
|
|
49
|
+
/** What the tracker needs from a `typing.*` payload. */
|
|
50
|
+
interface TypingActor {
|
|
51
|
+
readonly actorType: 'agent' | 'bot';
|
|
52
|
+
readonly actorId: string;
|
|
53
|
+
readonly displayName?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turn a stream of `typing.start` / `typing.stop` events into the single
|
|
57
|
+
* boolean a UI actually renders.
|
|
58
|
+
*
|
|
59
|
+
* Edge-triggered: `onChange` fires only when the answer changes, so a
|
|
60
|
+
* keystroke every 300ms does not re-render the consumer's component ten times
|
|
61
|
+
* a second. Exported for its own test — the timer behaviour is the part worth
|
|
62
|
+
* pinning, and it is invisible from the outside.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createTypingTracker(opts: {
|
|
65
|
+
readonly onChange: (signal: TypingSignal) => void;
|
|
66
|
+
readonly stopAfterMs?: number;
|
|
67
|
+
}): {
|
|
68
|
+
start: (actor: TypingActor) => void;
|
|
69
|
+
/**
|
|
70
|
+
* `actor` is optional: the caller does not always have one. A message
|
|
71
|
+
* arriving means "whoever was typing has stopped", and the envelope that
|
|
72
|
+
* carries it says nothing about typing at all.
|
|
73
|
+
*/
|
|
74
|
+
stop: (actor?: TypingActor) => void;
|
|
75
|
+
dispose: () => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The actor a `typing.*` envelope is about, or null if it is not one we show.
|
|
79
|
+
*
|
|
80
|
+
* Two exclusions, both load-bearing:
|
|
81
|
+
*
|
|
82
|
+
* - `kind: 'viewing'`. The agent dashboard publishes room presence and
|
|
83
|
+
* collaborative-cursor frames AS `typing.start` (see `viewing.cursor` in
|
|
84
|
+
* `apps/realtime/src/gateway.ts`), ~10 per second while an agent moves
|
|
85
|
+
* their mouse. Rendering those as typing would make the indicator flicker
|
|
86
|
+
* whenever an agent so much as looked at the conversation.
|
|
87
|
+
* - anything that is not an agent or a bot. The gateway's visitor allowlist
|
|
88
|
+
* already forwards only agent typing, so this is the second of two checks
|
|
89
|
+
* rather than the only one — but a visitor must never be shown their OWN
|
|
90
|
+
* typing as the agent's, and the day the allowlist widens is not the day
|
|
91
|
+
* to discover that.
|
|
92
|
+
*/
|
|
93
|
+
export declare function typingActorOf(payload: unknown): {
|
|
94
|
+
actorType: 'agent' | 'bot';
|
|
95
|
+
actorId: string;
|
|
96
|
+
displayName?: string;
|
|
97
|
+
} | null;
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=live.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAYhE;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,EACjB,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,eAAe,EAAE,CA4BnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,OAAQ,CAAC;AAE1C,wDAAwD;AACxD,UAAU,WAAW;IACnB,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B,GAAG;IACF,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAgDA;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,GACf;IAAE,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqB9E"}
|