@brandfine/client 0.11.0 → 0.14.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 +37 -0
- package/README.md +22 -12
- package/dist/index.cjs +473 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +242 -12
- package/dist/index.d.ts +242 -12
- package/dist/index.js +473 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,356 @@ export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
|
|
|
2
2
|
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
|
|
3
3
|
export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
|
|
4
4
|
|
|
5
|
+
// src/live-chat-session.ts
|
|
6
|
+
var SESSION_STORAGE_KEY = "bf-live-chat-session";
|
|
7
|
+
function tokenStorageKey(publishableKey) {
|
|
8
|
+
return `bf-live-chat-token:${publishableKey.slice(-12)}`;
|
|
9
|
+
}
|
|
10
|
+
function getVisitorSessionId() {
|
|
11
|
+
try {
|
|
12
|
+
const existing = localStorage.getItem(SESSION_STORAGE_KEY);
|
|
13
|
+
if (existing) return existing;
|
|
14
|
+
const generated = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().replace(/-/g, "") : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
15
|
+
localStorage.setItem(SESSION_STORAGE_KEY, generated);
|
|
16
|
+
return generated;
|
|
17
|
+
} catch {
|
|
18
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readStoredToken(publishableKey) {
|
|
22
|
+
try {
|
|
23
|
+
return localStorage.getItem(tokenStorageKey(publishableKey)) ?? void 0;
|
|
24
|
+
} catch {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeStoredToken(publishableKey, token) {
|
|
29
|
+
try {
|
|
30
|
+
if (token === null) localStorage.removeItem(tokenStorageKey(publishableKey));
|
|
31
|
+
else localStorage.setItem(tokenStorageKey(publishableKey), token);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function resolvePageLocale(explicit) {
|
|
36
|
+
if (explicit) return explicit;
|
|
37
|
+
if (typeof document === "undefined") return void 0;
|
|
38
|
+
const htmlLang = document.documentElement.lang?.trim();
|
|
39
|
+
if (htmlLang) return htmlLang;
|
|
40
|
+
return typeof navigator !== "undefined" ? navigator.language || void 0 : void 0;
|
|
41
|
+
}
|
|
42
|
+
async function createLiveChatSession(wire, opts) {
|
|
43
|
+
if (typeof window === "undefined") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"liveChat.createSession() is browser-only \u2014 build the transcript UI client-side; fetch the bootstrap with getConfig() on the server."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const publishableKey = opts.config.enabled ? opts.config.publishableKey : void 0;
|
|
49
|
+
const locale = resolvePageLocale(opts.locale);
|
|
50
|
+
const pollMs = Math.max(1e3, opts.pollIntervalMs ?? 5e3);
|
|
51
|
+
let snapshot = {
|
|
52
|
+
messages: [],
|
|
53
|
+
status: "CONNECTING",
|
|
54
|
+
online: false,
|
|
55
|
+
greeting: null,
|
|
56
|
+
offlineMessage: null,
|
|
57
|
+
sending: false,
|
|
58
|
+
error: null
|
|
59
|
+
};
|
|
60
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62
|
+
let cursor;
|
|
63
|
+
let lastEventId = 0;
|
|
64
|
+
let token;
|
|
65
|
+
let timer;
|
|
66
|
+
let closed = false;
|
|
67
|
+
let ticking = false;
|
|
68
|
+
const aborter = new AbortController();
|
|
69
|
+
let realtimeUrl = opts.config.realtimeUrl;
|
|
70
|
+
let ws;
|
|
71
|
+
let wsLive = false;
|
|
72
|
+
let wsFailures = 0;
|
|
73
|
+
let wsGivenUp = false;
|
|
74
|
+
let reconnectTimer;
|
|
75
|
+
let pingTimer;
|
|
76
|
+
function emit(patch) {
|
|
77
|
+
snapshot = { ...snapshot, ...patch };
|
|
78
|
+
for (const listener of listeners) listener(snapshot);
|
|
79
|
+
}
|
|
80
|
+
function appendMessages(incoming) {
|
|
81
|
+
const fresh = incoming.filter((m) => !seen.has(m.id));
|
|
82
|
+
if (fresh.length === 0) {
|
|
83
|
+
for (const m of incoming) {
|
|
84
|
+
if (typeof m.eventId === "number" && m.eventId > lastEventId) {
|
|
85
|
+
lastEventId = m.eventId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const m of fresh) {
|
|
91
|
+
seen.add(m.id);
|
|
92
|
+
if (typeof m.eventId === "number" && m.eventId > lastEventId) {
|
|
93
|
+
lastEventId = m.eventId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const messages = [...snapshot.messages, ...fresh];
|
|
97
|
+
cursor = messages[messages.length - 1].createdAt;
|
|
98
|
+
emit({ messages });
|
|
99
|
+
}
|
|
100
|
+
function markClosed() {
|
|
101
|
+
if (snapshot.status !== "CLOSED") emit({ status: "CLOSED" });
|
|
102
|
+
stopPolling();
|
|
103
|
+
teardownRealtime();
|
|
104
|
+
wsGivenUp = true;
|
|
105
|
+
}
|
|
106
|
+
async function tick() {
|
|
107
|
+
if (ticking || closed || !token) return;
|
|
108
|
+
ticking = true;
|
|
109
|
+
try {
|
|
110
|
+
const result = await wire.history(
|
|
111
|
+
token,
|
|
112
|
+
// The eventId cursor is exact (no equal-millisecond drop
|
|
113
|
+
// window); the timestamp cursor remains for old APIs that
|
|
114
|
+
// never sent eventIds.
|
|
115
|
+
lastEventId > 0 ? { afterEvent: lastEventId } : { after: cursor }
|
|
116
|
+
);
|
|
117
|
+
if (closed) return;
|
|
118
|
+
appendMessages(result.messages);
|
|
119
|
+
if (result.status === "CLOSED") markClosed();
|
|
120
|
+
} catch {
|
|
121
|
+
} finally {
|
|
122
|
+
ticking = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function startPolling() {
|
|
126
|
+
stopPolling();
|
|
127
|
+
timer = setInterval(() => void tick(), pollMs);
|
|
128
|
+
}
|
|
129
|
+
function stopPolling() {
|
|
130
|
+
if (timer !== void 0) clearInterval(timer);
|
|
131
|
+
timer = void 0;
|
|
132
|
+
}
|
|
133
|
+
function teardownRealtime() {
|
|
134
|
+
if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
|
|
135
|
+
reconnectTimer = void 0;
|
|
136
|
+
if (pingTimer !== void 0) clearInterval(pingTimer);
|
|
137
|
+
pingTimer = void 0;
|
|
138
|
+
wsLive = false;
|
|
139
|
+
if (ws) {
|
|
140
|
+
const socket = ws;
|
|
141
|
+
ws = void 0;
|
|
142
|
+
try {
|
|
143
|
+
socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
|
|
144
|
+
socket.close();
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function scheduleReconnect() {
|
|
150
|
+
if (closed || wsGivenUp || reconnectTimer !== void 0) return;
|
|
151
|
+
if (wsFailures >= 5) {
|
|
152
|
+
wsGivenUp = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const base = Math.min(3e4, 1e3 * 2 ** wsFailures);
|
|
156
|
+
const delay = base / 2 + Math.random() * (base / 2);
|
|
157
|
+
reconnectTimer = setTimeout(() => {
|
|
158
|
+
reconnectTimer = void 0;
|
|
159
|
+
startRealtime();
|
|
160
|
+
}, delay);
|
|
161
|
+
}
|
|
162
|
+
function handleSocketDown() {
|
|
163
|
+
teardownRealtime();
|
|
164
|
+
if (closed || wsGivenUp || snapshot.status === "CLOSED") return;
|
|
165
|
+
startPolling();
|
|
166
|
+
wsFailures += 1;
|
|
167
|
+
scheduleReconnect();
|
|
168
|
+
}
|
|
169
|
+
function startRealtime() {
|
|
170
|
+
if (closed || wsGivenUp || wsLive || ws !== void 0 || !realtimeUrl || !token || !publishableKey || typeof WebSocket === "undefined" || snapshot.status === "CLOSED") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let socket;
|
|
174
|
+
try {
|
|
175
|
+
socket = new WebSocket(realtimeUrl);
|
|
176
|
+
} catch {
|
|
177
|
+
wsFailures += 1;
|
|
178
|
+
scheduleReconnect();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
ws = socket;
|
|
182
|
+
socket.onopen = () => {
|
|
183
|
+
try {
|
|
184
|
+
socket.send(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
type: "auth",
|
|
187
|
+
publishableKey,
|
|
188
|
+
conversationToken: token,
|
|
189
|
+
lastEventId
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
handleSocketDown();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
socket.onmessage = (event) => {
|
|
197
|
+
let frame;
|
|
198
|
+
try {
|
|
199
|
+
frame = JSON.parse(String(event.data));
|
|
200
|
+
} catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
switch (frame.type) {
|
|
204
|
+
case "ready": {
|
|
205
|
+
wsLive = true;
|
|
206
|
+
wsFailures = 0;
|
|
207
|
+
stopPolling();
|
|
208
|
+
if (pingTimer === void 0) {
|
|
209
|
+
pingTimer = setInterval(() => {
|
|
210
|
+
try {
|
|
211
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}, 3e4);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "message": {
|
|
219
|
+
const m = frame.message;
|
|
220
|
+
if (m && typeof m.id === "string") {
|
|
221
|
+
appendMessages([
|
|
222
|
+
{
|
|
223
|
+
...m,
|
|
224
|
+
eventId: typeof frame.eventId === "number" ? frame.eventId : m.eventId
|
|
225
|
+
}
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "status": {
|
|
231
|
+
if (frame.status === "CLOSED") markClosed();
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "error": {
|
|
235
|
+
wsGivenUp = true;
|
|
236
|
+
teardownRealtime();
|
|
237
|
+
if (!closed && snapshot.status !== "CLOSED") startPolling();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
socket.onclose = () => handleSocketDown();
|
|
243
|
+
socket.onerror = () => {
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function connect(resumeToken) {
|
|
247
|
+
if (!publishableKey) {
|
|
248
|
+
emit({ status: "CLOSED" });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const cfg = await wire.runtimeConfig(locale);
|
|
253
|
+
if (closed) return;
|
|
254
|
+
if (!cfg.enabled) {
|
|
255
|
+
emit({ status: "CLOSED" });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
emit({
|
|
259
|
+
greeting: cfg.greeting,
|
|
260
|
+
offlineMessage: cfg.offlineMessage,
|
|
261
|
+
online: cfg.online !== false
|
|
262
|
+
});
|
|
263
|
+
if (cfg.realtimeUrl !== void 0) realtimeUrl = cfg.realtimeUrl;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (closed) return;
|
|
266
|
+
emit({ status: "ERROR", error });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const started = await wire.startConversation({
|
|
271
|
+
visitorSessionId: getVisitorSessionId(),
|
|
272
|
+
conversationToken: resumeToken,
|
|
273
|
+
pageUrl: opts.pageUrl ?? (typeof location !== "undefined" ? location.href.slice(0, 2048) : void 0),
|
|
274
|
+
referrer: opts.referrer,
|
|
275
|
+
locale,
|
|
276
|
+
visitor: opts.visitor
|
|
277
|
+
});
|
|
278
|
+
if (closed) return;
|
|
279
|
+
if (!started.enabled) {
|
|
280
|
+
emit({ status: "CLOSED" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
token = started.conversationToken;
|
|
284
|
+
writeStoredToken(publishableKey, token);
|
|
285
|
+
if (started.online !== void 0) emit({ online: started.online });
|
|
286
|
+
emit({ status: "OPEN" });
|
|
287
|
+
await tick();
|
|
288
|
+
if (!closed) {
|
|
289
|
+
startPolling();
|
|
290
|
+
startRealtime();
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (closed) return;
|
|
294
|
+
emit({ status: "ERROR", error });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const session = {
|
|
298
|
+
get messages() {
|
|
299
|
+
return snapshot.messages;
|
|
300
|
+
},
|
|
301
|
+
get state() {
|
|
302
|
+
return snapshot.status;
|
|
303
|
+
},
|
|
304
|
+
getSnapshot: () => snapshot,
|
|
305
|
+
subscribe(listener) {
|
|
306
|
+
listeners.add(listener);
|
|
307
|
+
return () => listeners.delete(listener);
|
|
308
|
+
},
|
|
309
|
+
async send(body) {
|
|
310
|
+
const trimmed = body.trim();
|
|
311
|
+
if (!trimmed) throw new Error("send(): message body is empty");
|
|
312
|
+
if (!token) throw new Error("send(): session is not connected");
|
|
313
|
+
if (snapshot.status === "CLOSED") {
|
|
314
|
+
throw new Error("send(): conversation is closed \u2014 call reset()");
|
|
315
|
+
}
|
|
316
|
+
emit({ sending: true });
|
|
317
|
+
try {
|
|
318
|
+
const clientId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
319
|
+
const message = await wire.sendMessage(token, {
|
|
320
|
+
body: trimmed,
|
|
321
|
+
clientId
|
|
322
|
+
});
|
|
323
|
+
appendMessages([message]);
|
|
324
|
+
return message;
|
|
325
|
+
} finally {
|
|
326
|
+
emit({ sending: false });
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
close() {
|
|
330
|
+
if (closed) return;
|
|
331
|
+
closed = true;
|
|
332
|
+
stopPolling();
|
|
333
|
+
teardownRealtime();
|
|
334
|
+
aborter.abort();
|
|
335
|
+
listeners.clear();
|
|
336
|
+
},
|
|
337
|
+
async reset() {
|
|
338
|
+
teardownRealtime();
|
|
339
|
+
wsFailures = 0;
|
|
340
|
+
wsGivenUp = false;
|
|
341
|
+
if (publishableKey) writeStoredToken(publishableKey, null);
|
|
342
|
+
token = void 0;
|
|
343
|
+
cursor = void 0;
|
|
344
|
+
lastEventId = 0;
|
|
345
|
+
seen.clear();
|
|
346
|
+
stopPolling();
|
|
347
|
+
emit({ messages: [], status: "CONNECTING", error: null });
|
|
348
|
+
await connect(void 0);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
await connect(publishableKey ? readStoredToken(publishableKey) : void 0);
|
|
352
|
+
return session;
|
|
353
|
+
}
|
|
354
|
+
|
|
5
355
|
// src/client.ts
|
|
6
356
|
var BrandfineApiError = class extends Error {
|
|
7
357
|
name = "BrandfineApiError";
|
|
@@ -21,6 +371,23 @@ var BrandfineApiError = class extends Error {
|
|
|
21
371
|
};
|
|
22
372
|
var INSTALLED_MARKER = "data-brandfine-analytics";
|
|
23
373
|
var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
|
|
374
|
+
var LIVE_CHAT_IDENTITY_CONTEXT = "brandfine:live-chat:identity:v1";
|
|
375
|
+
async function hmacHex(key, message) {
|
|
376
|
+
const enc = new TextEncoder();
|
|
377
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey(
|
|
378
|
+
"raw",
|
|
379
|
+
enc.encode(key),
|
|
380
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
381
|
+
false,
|
|
382
|
+
["sign"]
|
|
383
|
+
);
|
|
384
|
+
const sig = await globalThis.crypto.subtle.sign(
|
|
385
|
+
"HMAC",
|
|
386
|
+
cryptoKey,
|
|
387
|
+
enc.encode(message)
|
|
388
|
+
);
|
|
389
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
390
|
+
}
|
|
24
391
|
var GTAG_MARKER = "data-brandfine-gtag";
|
|
25
392
|
function injectGoogleTag(measurementId) {
|
|
26
393
|
if (typeof document === "undefined") return;
|
|
@@ -168,6 +535,28 @@ function createBrandfineClient(config) {
|
|
|
168
535
|
return { installed: true, websiteId: cfg.websiteId };
|
|
169
536
|
}
|
|
170
537
|
};
|
|
538
|
+
async function liveChatWireRequest(key, method, path, body) {
|
|
539
|
+
const url = `${baseUrl}${path}`;
|
|
540
|
+
const res = await fetchImpl(url, {
|
|
541
|
+
method,
|
|
542
|
+
headers: {
|
|
543
|
+
Accept: "application/json",
|
|
544
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
545
|
+
...key ? { "X-Api-Key": key } : {}
|
|
546
|
+
},
|
|
547
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
548
|
+
});
|
|
549
|
+
if (!res.ok) {
|
|
550
|
+
const text = await res.text().catch(() => "");
|
|
551
|
+
throw new BrandfineApiError({
|
|
552
|
+
status: res.status,
|
|
553
|
+
statusText: res.statusText,
|
|
554
|
+
body: text,
|
|
555
|
+
url
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
return await res.json();
|
|
559
|
+
}
|
|
171
560
|
const liveChat = {
|
|
172
561
|
getConfig() {
|
|
173
562
|
return get("/external/live-chat/bootstrap");
|
|
@@ -192,6 +581,9 @@ function createBrandfineClient(config) {
|
|
|
192
581
|
if (opts.visitor?.externalId && opts.visitor.identityToken) {
|
|
193
582
|
host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
|
|
194
583
|
}
|
|
584
|
+
if (opts.locale) {
|
|
585
|
+
host.setAttribute("data-locale", opts.locale);
|
|
586
|
+
}
|
|
195
587
|
if (cfg.theme) {
|
|
196
588
|
for (const [key, value] of Object.entries(cfg.theme)) {
|
|
197
589
|
if (key.startsWith("--bf-chat-")) {
|
|
@@ -207,35 +599,67 @@ function createBrandfineClient(config) {
|
|
|
207
599
|
document.head.appendChild(script);
|
|
208
600
|
return { installed: true };
|
|
209
601
|
},
|
|
602
|
+
createSession(opts) {
|
|
603
|
+
const pk = opts.config.enabled ? opts.config.publishableKey : void 0;
|
|
604
|
+
const wire = {
|
|
605
|
+
runtimeConfig: (locale) => liveChatWireRequest(
|
|
606
|
+
pk,
|
|
607
|
+
"GET",
|
|
608
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
609
|
+
),
|
|
610
|
+
startConversation: (input) => liveChatWireRequest(pk, "POST", "/external/live-chat/conversations", input),
|
|
611
|
+
sendMessage: (conversationToken, input) => liveChatWireRequest(
|
|
612
|
+
void 0,
|
|
613
|
+
"POST",
|
|
614
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
615
|
+
input
|
|
616
|
+
),
|
|
617
|
+
history: (conversationToken, o = {}) => liveChatWireRequest(
|
|
618
|
+
void 0,
|
|
619
|
+
"GET",
|
|
620
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.afterEvent !== void 0 ? `?afterEvent=${encodeURIComponent(String(o.afterEvent))}` : o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
|
|
621
|
+
)
|
|
622
|
+
};
|
|
623
|
+
return createLiveChatSession(wire, opts);
|
|
624
|
+
},
|
|
625
|
+
runtimeConfig(locale) {
|
|
626
|
+
return liveChatWireRequest(
|
|
627
|
+
apiKey,
|
|
628
|
+
"GET",
|
|
629
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
630
|
+
);
|
|
631
|
+
},
|
|
632
|
+
startConversation(input) {
|
|
633
|
+
return liveChatWireRequest(apiKey, "POST", "/external/live-chat/conversations", input);
|
|
634
|
+
},
|
|
635
|
+
sendMessage(conversationToken, input) {
|
|
636
|
+
return liveChatWireRequest(
|
|
637
|
+
void 0,
|
|
638
|
+
"POST",
|
|
639
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
640
|
+
input
|
|
641
|
+
);
|
|
642
|
+
},
|
|
643
|
+
history(conversationToken, o = {}) {
|
|
644
|
+
const qs = o.afterEvent !== void 0 ? `?afterEvent=${encodeURIComponent(String(o.afterEvent))}` : o.after ? `?after=${encodeURIComponent(o.after)}` : "";
|
|
645
|
+
return liveChatWireRequest(
|
|
646
|
+
void 0,
|
|
647
|
+
"GET",
|
|
648
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${qs}`
|
|
649
|
+
);
|
|
650
|
+
},
|
|
210
651
|
async identityToken(externalId, opts = {}) {
|
|
211
652
|
if (typeof document !== "undefined" || typeof window !== "undefined") {
|
|
212
653
|
throw new Error(
|
|
213
654
|
"liveChat.identityToken() is server-only \u2014 never compute identity tokens in a browser. Sign the visitor on your server and pass the result to install({ visitor })."
|
|
214
655
|
);
|
|
215
656
|
}
|
|
216
|
-
const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
217
|
-
if (!secret) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
"liveChat.identityToken(): identity secret missing. Pass { secret } or set BRANDFINE_LIVE_CHAT_IDENTITY_SECRET. Generate one in the CMS: Plugins \u2192 Live Chat \u2192 Integrate."
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
657
|
if (!externalId) {
|
|
223
658
|
throw new Error("liveChat.identityToken(): externalId is required.");
|
|
224
659
|
}
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
enc.encode(secret),
|
|
229
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
230
|
-
false,
|
|
231
|
-
["sign"]
|
|
232
|
-
);
|
|
233
|
-
const sig = await globalThis.crypto.subtle.sign(
|
|
234
|
-
"HMAC",
|
|
235
|
-
key,
|
|
236
|
-
enc.encode(externalId)
|
|
237
|
-
);
|
|
238
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
660
|
+
const explicit = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
661
|
+
const secret = explicit ?? await hmacHex(apiKey, LIVE_CHAT_IDENTITY_CONTEXT);
|
|
662
|
+
return hmacHex(secret, externalId);
|
|
239
663
|
}
|
|
240
664
|
};
|
|
241
665
|
const submissions = {
|
|
@@ -303,6 +727,35 @@ function createBrandfineClient(config) {
|
|
|
303
727
|
"/external/appointments/requests",
|
|
304
728
|
input
|
|
305
729
|
);
|
|
730
|
+
},
|
|
731
|
+
async list(identity) {
|
|
732
|
+
const res = await post(
|
|
733
|
+
"/external/appointments/requests/lookup",
|
|
734
|
+
identity
|
|
735
|
+
);
|
|
736
|
+
return res.appointments;
|
|
737
|
+
},
|
|
738
|
+
async get(id, identity) {
|
|
739
|
+
const res = await post(
|
|
740
|
+
"/external/appointments/requests/lookup",
|
|
741
|
+
{ ...identity, id }
|
|
742
|
+
);
|
|
743
|
+
const row = res.appointments[0];
|
|
744
|
+
if (!row) {
|
|
745
|
+
throw new BrandfineApiError({
|
|
746
|
+
status: 404,
|
|
747
|
+
statusText: "Not Found",
|
|
748
|
+
body: "Appointment not found.",
|
|
749
|
+
url: "/external/appointments/requests/lookup"
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return row;
|
|
753
|
+
},
|
|
754
|
+
cancel(cancellationToken) {
|
|
755
|
+
return post(
|
|
756
|
+
`/external/appointments/requests/${encodeURIComponent(cancellationToken)}/cancel`,
|
|
757
|
+
{}
|
|
758
|
+
);
|
|
306
759
|
}
|
|
307
760
|
};
|
|
308
761
|
return {
|