@brandfine/client 0.10.0 → 0.13.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 +25 -0
- package/README.md +13 -13
- package/dist/index.cjs +297 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -11
- package/dist/index.d.ts +158 -11
- package/dist/index.js +297 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6c8f190: Headless Live Chat — build your own chat UI:
|
|
8
|
+
- `liveChat.createSession({ config, visitor, locale, pollIntervalMs })` exposes the conversation as typed, subscribable state (`messages`, `status`, `online`, localized `greeting`/`offlineMessage`, `sending`, `error`) with `send()`, `subscribe()` (shaped for `useSyncExternalStore`), `close()` and `reset()`. Shares the floating widget's storage, threading and identity semantics — same conversation everywhere; the widget remains unchanged.
|
|
9
|
+
- Raw wire methods for full control: `liveChat.runtimeConfig()`, `startConversation()`, `sendMessage()`, `history()`. In browsers, construct the client with the publishable key.
|
|
10
|
+
- New root exports: `ChatMessage`, `ChatMessageSender`, `ConversationStatus`, `LiveChatSession`, `LiveChatSessionSnapshot`, `LiveChatSessionState`, `LiveChatCreateSessionOptions`, `LiveChatRuntimeConfig`, `StartConversationResult` — with a root-export test so gaps can't regress again.
|
|
11
|
+
- Transport is polling (default 5000 ms, `pollIntervalMs` option); a future realtime upgrade keeps this API with polling as fallback.
|
|
12
|
+
|
|
13
|
+
## 0.12.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0e6c7c9: Live Chat localization + zero-config identity signing (code already on main, changeset restored after a merge dropped it):
|
|
18
|
+
- `liveChat.install({ config, locale })` pins the widget's language (falls back to the page's `<html lang>`, then the browser). Greeting/away-message translations are configured per locale in the CMS and resolved server-side; the visitor's page language is stored on the conversation.
|
|
19
|
+
- `liveChat.identityToken()` no longer requires a configured secret: by default it derives the signing secret from the client's API key (domain-separated HMAC; the API verifies against the same derivation). `{ secret }` / `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET` still override for decoupled signers and legacy workspaces. The missing-secret throw is gone — only the in-browser guard remains.
|
|
20
|
+
|
|
21
|
+
## 0.11.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- 59bb2ab: - `baseUrl` is now optional: resolution is explicit option → `BRANDFINE_API_URL` env (server-side) → `https://api.brandfine.co`. Consumers only configure a URL for local dev or staging; existing explicit `baseUrl` callers are unaffected.
|
|
26
|
+
- Export the Live Chat types from the root entry: `LiveChatBootstrap`, `LiveChatInstallOptions`, `LiveChatInstallResult`, `LiveChatVisitor`. They were declared in 0.9.0/0.10.0 but missing from the root export list, forcing consumers to re-derive them structurally.
|
|
27
|
+
|
|
3
28
|
## 0.10.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,8 +34,9 @@ Pick the import path that scopes to what you actually use — tree-shaking does
|
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
const bf = createBrandfineClient({
|
|
37
|
-
baseUrl: 'https://api.brandfine.co',
|
|
38
37
|
apiKey: process.env.BRANDFINE_API_KEY!,
|
|
38
|
+
// baseUrl is optional — defaults to https://api.brandfine.co.
|
|
39
|
+
// For local dev, set BRANDFINE_API_URL or pass it explicitly.
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
// Content reads
|
|
@@ -105,16 +106,13 @@ bf.liveChat.install({ config })
|
|
|
105
106
|
|
|
106
107
|
### Verified visitors
|
|
107
108
|
|
|
108
|
-
On signed-in pages, tell the inbox **who** is chatting.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`install()`:
|
|
109
|
+
On signed-in pages, tell the inbox **who** is chatting. Signing
|
|
110
|
+
needs **no extra configuration** — `identityToken()` derives the
|
|
111
|
+
signing secret from the API key the client already holds:
|
|
112
112
|
|
|
113
113
|
```ts
|
|
114
114
|
// Server — never in a browser:
|
|
115
|
-
const identityToken = await bf.liveChat.identityToken(user.id
|
|
116
|
-
secret: process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET,
|
|
117
|
-
})
|
|
115
|
+
const identityToken = await bf.liveChat.identityToken(user.id)
|
|
118
116
|
const visitor = {
|
|
119
117
|
externalId: user.id,
|
|
120
118
|
name: user.name,
|
|
@@ -132,11 +130,13 @@ the Brandfine inbox and continue across devices/sessions (same
|
|
|
132
130
|
`externalId` = same person). An invalid or missing token silently
|
|
133
131
|
downgrades to anonymous chat — never a blocked visitor.
|
|
134
132
|
|
|
135
|
-
> **Security:** never
|
|
136
|
-
>
|
|
137
|
-
> impersonate any visitor.
|
|
138
|
-
>
|
|
139
|
-
>
|
|
133
|
+
> **Security:** never compute the HMAC client-side — the signing
|
|
134
|
+
> secret (derived from your API key, or an explicit one) must never
|
|
135
|
+
> reach a browser, or anyone could impersonate any visitor.
|
|
136
|
+
> `identityToken()` enforces this by throwing in browser contexts.
|
|
137
|
+
> An explicit secret (`{ secret }` / `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET`)
|
|
138
|
+
> overrides the derived default — only needed when the signer
|
|
139
|
+
> shouldn't hold the broad API key.
|
|
140
140
|
|
|
141
141
|
Full reference: [docs.brandfine.co/docs/sdk/live-chat](https://docs.brandfine.co/docs/sdk/live-chat).
|
|
142
142
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,201 @@ var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
|
|
|
4
4
|
var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
|
|
5
5
|
var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
|
|
6
6
|
|
|
7
|
+
// src/live-chat-session.ts
|
|
8
|
+
var SESSION_STORAGE_KEY = "bf-live-chat-session";
|
|
9
|
+
function tokenStorageKey(publishableKey) {
|
|
10
|
+
return `bf-live-chat-token:${publishableKey.slice(-12)}`;
|
|
11
|
+
}
|
|
12
|
+
function getVisitorSessionId() {
|
|
13
|
+
try {
|
|
14
|
+
const existing = localStorage.getItem(SESSION_STORAGE_KEY);
|
|
15
|
+
if (existing) return existing;
|
|
16
|
+
const generated = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().replace(/-/g, "") : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
17
|
+
localStorage.setItem(SESSION_STORAGE_KEY, generated);
|
|
18
|
+
return generated;
|
|
19
|
+
} catch {
|
|
20
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readStoredToken(publishableKey) {
|
|
24
|
+
try {
|
|
25
|
+
return localStorage.getItem(tokenStorageKey(publishableKey)) ?? void 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeStoredToken(publishableKey, token) {
|
|
31
|
+
try {
|
|
32
|
+
if (token === null) localStorage.removeItem(tokenStorageKey(publishableKey));
|
|
33
|
+
else localStorage.setItem(tokenStorageKey(publishableKey), token);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function resolvePageLocale(explicit) {
|
|
38
|
+
if (explicit) return explicit;
|
|
39
|
+
if (typeof document === "undefined") return void 0;
|
|
40
|
+
const htmlLang = document.documentElement.lang?.trim();
|
|
41
|
+
if (htmlLang) return htmlLang;
|
|
42
|
+
return typeof navigator !== "undefined" ? navigator.language || void 0 : void 0;
|
|
43
|
+
}
|
|
44
|
+
async function createLiveChatSession(wire, opts) {
|
|
45
|
+
if (typeof window === "undefined") {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"liveChat.createSession() is browser-only \u2014 build the transcript UI client-side; fetch the bootstrap with getConfig() on the server."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const publishableKey = opts.config.enabled ? opts.config.publishableKey : void 0;
|
|
51
|
+
const locale = resolvePageLocale(opts.locale);
|
|
52
|
+
const pollMs = Math.max(1e3, opts.pollIntervalMs ?? 5e3);
|
|
53
|
+
let snapshot = {
|
|
54
|
+
messages: [],
|
|
55
|
+
status: "CONNECTING",
|
|
56
|
+
online: false,
|
|
57
|
+
greeting: null,
|
|
58
|
+
offlineMessage: null,
|
|
59
|
+
sending: false,
|
|
60
|
+
error: null
|
|
61
|
+
};
|
|
62
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
let cursor;
|
|
65
|
+
let token;
|
|
66
|
+
let timer;
|
|
67
|
+
let closed = false;
|
|
68
|
+
let ticking = false;
|
|
69
|
+
const aborter = new AbortController();
|
|
70
|
+
function emit(patch) {
|
|
71
|
+
snapshot = { ...snapshot, ...patch };
|
|
72
|
+
for (const listener of listeners) listener(snapshot);
|
|
73
|
+
}
|
|
74
|
+
function appendMessages(incoming) {
|
|
75
|
+
const fresh = incoming.filter((m) => !seen.has(m.id));
|
|
76
|
+
if (fresh.length === 0) return;
|
|
77
|
+
for (const m of fresh) seen.add(m.id);
|
|
78
|
+
const messages = [...snapshot.messages, ...fresh];
|
|
79
|
+
cursor = messages[messages.length - 1].createdAt;
|
|
80
|
+
emit({ messages });
|
|
81
|
+
}
|
|
82
|
+
async function tick() {
|
|
83
|
+
if (ticking || closed || !token) return;
|
|
84
|
+
ticking = true;
|
|
85
|
+
try {
|
|
86
|
+
const result = await wire.history(token, { after: cursor });
|
|
87
|
+
if (closed) return;
|
|
88
|
+
appendMessages(result.messages);
|
|
89
|
+
if (result.status === "CLOSED" && snapshot.status !== "CLOSED") {
|
|
90
|
+
emit({ status: "CLOSED" });
|
|
91
|
+
stopPolling();
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
} finally {
|
|
95
|
+
ticking = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function startPolling() {
|
|
99
|
+
stopPolling();
|
|
100
|
+
timer = setInterval(() => void tick(), pollMs);
|
|
101
|
+
}
|
|
102
|
+
function stopPolling() {
|
|
103
|
+
if (timer !== void 0) clearInterval(timer);
|
|
104
|
+
timer = void 0;
|
|
105
|
+
}
|
|
106
|
+
async function connect(resumeToken) {
|
|
107
|
+
if (!publishableKey) {
|
|
108
|
+
emit({ status: "CLOSED" });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const cfg = await wire.runtimeConfig(locale);
|
|
113
|
+
if (closed) return;
|
|
114
|
+
if (!cfg.enabled) {
|
|
115
|
+
emit({ status: "CLOSED" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
emit({
|
|
119
|
+
greeting: cfg.greeting,
|
|
120
|
+
offlineMessage: cfg.offlineMessage,
|
|
121
|
+
online: cfg.online !== false
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (closed) return;
|
|
125
|
+
emit({ status: "ERROR", error });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const started = await wire.startConversation({
|
|
130
|
+
visitorSessionId: getVisitorSessionId(),
|
|
131
|
+
conversationToken: resumeToken,
|
|
132
|
+
pageUrl: opts.pageUrl ?? (typeof location !== "undefined" ? location.href.slice(0, 2048) : void 0),
|
|
133
|
+
referrer: opts.referrer,
|
|
134
|
+
locale,
|
|
135
|
+
visitor: opts.visitor
|
|
136
|
+
});
|
|
137
|
+
if (closed) return;
|
|
138
|
+
if (!started.enabled) {
|
|
139
|
+
emit({ status: "CLOSED" });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
token = started.conversationToken;
|
|
143
|
+
writeStoredToken(publishableKey, token);
|
|
144
|
+
if (started.online !== void 0) emit({ online: started.online });
|
|
145
|
+
emit({ status: "OPEN" });
|
|
146
|
+
await tick();
|
|
147
|
+
if (!closed) startPolling();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (closed) return;
|
|
150
|
+
emit({ status: "ERROR", error });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const session = {
|
|
154
|
+
get messages() {
|
|
155
|
+
return snapshot.messages;
|
|
156
|
+
},
|
|
157
|
+
get state() {
|
|
158
|
+
return snapshot.status;
|
|
159
|
+
},
|
|
160
|
+
getSnapshot: () => snapshot,
|
|
161
|
+
subscribe(listener) {
|
|
162
|
+
listeners.add(listener);
|
|
163
|
+
return () => listeners.delete(listener);
|
|
164
|
+
},
|
|
165
|
+
async send(body) {
|
|
166
|
+
const trimmed = body.trim();
|
|
167
|
+
if (!trimmed) throw new Error("send(): message body is empty");
|
|
168
|
+
if (!token) throw new Error("send(): session is not connected");
|
|
169
|
+
if (snapshot.status === "CLOSED") {
|
|
170
|
+
throw new Error("send(): conversation is closed \u2014 call reset()");
|
|
171
|
+
}
|
|
172
|
+
emit({ sending: true });
|
|
173
|
+
try {
|
|
174
|
+
const message = await wire.sendMessage(token, { body: trimmed });
|
|
175
|
+
appendMessages([message]);
|
|
176
|
+
return message;
|
|
177
|
+
} finally {
|
|
178
|
+
emit({ sending: false });
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
close() {
|
|
182
|
+
if (closed) return;
|
|
183
|
+
closed = true;
|
|
184
|
+
stopPolling();
|
|
185
|
+
aborter.abort();
|
|
186
|
+
listeners.clear();
|
|
187
|
+
},
|
|
188
|
+
async reset() {
|
|
189
|
+
if (publishableKey) writeStoredToken(publishableKey, null);
|
|
190
|
+
token = void 0;
|
|
191
|
+
cursor = void 0;
|
|
192
|
+
seen.clear();
|
|
193
|
+
stopPolling();
|
|
194
|
+
emit({ messages: [], status: "CONNECTING", error: null });
|
|
195
|
+
await connect(void 0);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
await connect(publishableKey ? readStoredToken(publishableKey) : void 0);
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
201
|
+
|
|
7
202
|
// src/client.ts
|
|
8
203
|
var BrandfineApiError = class extends Error {
|
|
9
204
|
name = "BrandfineApiError";
|
|
@@ -23,6 +218,23 @@ var BrandfineApiError = class extends Error {
|
|
|
23
218
|
};
|
|
24
219
|
var INSTALLED_MARKER = "data-brandfine-analytics";
|
|
25
220
|
var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
|
|
221
|
+
var LIVE_CHAT_IDENTITY_CONTEXT = "brandfine:live-chat:identity:v1";
|
|
222
|
+
async function hmacHex(key, message) {
|
|
223
|
+
const enc = new TextEncoder();
|
|
224
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey(
|
|
225
|
+
"raw",
|
|
226
|
+
enc.encode(key),
|
|
227
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
228
|
+
false,
|
|
229
|
+
["sign"]
|
|
230
|
+
);
|
|
231
|
+
const sig = await globalThis.crypto.subtle.sign(
|
|
232
|
+
"HMAC",
|
|
233
|
+
cryptoKey,
|
|
234
|
+
enc.encode(message)
|
|
235
|
+
);
|
|
236
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
237
|
+
}
|
|
26
238
|
var GTAG_MARKER = "data-brandfine-gtag";
|
|
27
239
|
function injectGoogleTag(measurementId) {
|
|
28
240
|
if (typeof document === "undefined") return;
|
|
@@ -44,12 +256,18 @@ function injectGoogleTag(measurementId) {
|
|
|
44
256
|
gtag("config", measurementId);
|
|
45
257
|
}
|
|
46
258
|
var DEFAULT_USER_AGENT = "@brandfine/client";
|
|
259
|
+
var DEFAULT_BASE_URL = "https://api.brandfine.co";
|
|
260
|
+
function resolveBaseUrl(explicit) {
|
|
261
|
+
if (explicit) return explicit;
|
|
262
|
+
if (typeof process !== "undefined" && process.env?.BRANDFINE_API_URL) {
|
|
263
|
+
return process.env.BRANDFINE_API_URL;
|
|
264
|
+
}
|
|
265
|
+
return DEFAULT_BASE_URL;
|
|
266
|
+
}
|
|
47
267
|
function createBrandfineClient(config) {
|
|
48
|
-
if (!config.baseUrl)
|
|
49
|
-
throw new Error("createBrandfineClient: `baseUrl` is required");
|
|
50
268
|
if (!config.apiKey)
|
|
51
269
|
throw new Error("createBrandfineClient: `apiKey` is required");
|
|
52
|
-
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
270
|
+
const baseUrl = resolveBaseUrl(config.baseUrl).replace(/\/$/, "");
|
|
53
271
|
const apiKey = config.apiKey;
|
|
54
272
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
55
273
|
const userAgent = config.userAgent ?? DEFAULT_USER_AGENT;
|
|
@@ -164,6 +382,28 @@ function createBrandfineClient(config) {
|
|
|
164
382
|
return { installed: true, websiteId: cfg.websiteId };
|
|
165
383
|
}
|
|
166
384
|
};
|
|
385
|
+
async function liveChatWireRequest(key, method, path, body) {
|
|
386
|
+
const url = `${baseUrl}${path}`;
|
|
387
|
+
const res = await fetchImpl(url, {
|
|
388
|
+
method,
|
|
389
|
+
headers: {
|
|
390
|
+
Accept: "application/json",
|
|
391
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
392
|
+
...key ? { "X-Api-Key": key } : {}
|
|
393
|
+
},
|
|
394
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok) {
|
|
397
|
+
const text = await res.text().catch(() => "");
|
|
398
|
+
throw new BrandfineApiError({
|
|
399
|
+
status: res.status,
|
|
400
|
+
statusText: res.statusText,
|
|
401
|
+
body: text,
|
|
402
|
+
url
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return await res.json();
|
|
406
|
+
}
|
|
167
407
|
const liveChat = {
|
|
168
408
|
getConfig() {
|
|
169
409
|
return get("/external/live-chat/bootstrap");
|
|
@@ -188,6 +428,9 @@ function createBrandfineClient(config) {
|
|
|
188
428
|
if (opts.visitor?.externalId && opts.visitor.identityToken) {
|
|
189
429
|
host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
|
|
190
430
|
}
|
|
431
|
+
if (opts.locale) {
|
|
432
|
+
host.setAttribute("data-locale", opts.locale);
|
|
433
|
+
}
|
|
191
434
|
if (cfg.theme) {
|
|
192
435
|
for (const [key, value] of Object.entries(cfg.theme)) {
|
|
193
436
|
if (key.startsWith("--bf-chat-")) {
|
|
@@ -203,35 +446,66 @@ function createBrandfineClient(config) {
|
|
|
203
446
|
document.head.appendChild(script);
|
|
204
447
|
return { installed: true };
|
|
205
448
|
},
|
|
449
|
+
createSession(opts) {
|
|
450
|
+
const pk = opts.config.enabled ? opts.config.publishableKey : void 0;
|
|
451
|
+
const wire = {
|
|
452
|
+
runtimeConfig: (locale) => liveChatWireRequest(
|
|
453
|
+
pk,
|
|
454
|
+
"GET",
|
|
455
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
456
|
+
),
|
|
457
|
+
startConversation: (input) => liveChatWireRequest(pk, "POST", "/external/live-chat/conversations", input),
|
|
458
|
+
sendMessage: (conversationToken, input) => liveChatWireRequest(
|
|
459
|
+
void 0,
|
|
460
|
+
"POST",
|
|
461
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
462
|
+
input
|
|
463
|
+
),
|
|
464
|
+
history: (conversationToken, o = {}) => liveChatWireRequest(
|
|
465
|
+
void 0,
|
|
466
|
+
"GET",
|
|
467
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
|
|
468
|
+
)
|
|
469
|
+
};
|
|
470
|
+
return createLiveChatSession(wire, opts);
|
|
471
|
+
},
|
|
472
|
+
runtimeConfig(locale) {
|
|
473
|
+
return liveChatWireRequest(
|
|
474
|
+
apiKey,
|
|
475
|
+
"GET",
|
|
476
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
startConversation(input) {
|
|
480
|
+
return liveChatWireRequest(apiKey, "POST", "/external/live-chat/conversations", input);
|
|
481
|
+
},
|
|
482
|
+
sendMessage(conversationToken, input) {
|
|
483
|
+
return liveChatWireRequest(
|
|
484
|
+
void 0,
|
|
485
|
+
"POST",
|
|
486
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
487
|
+
input
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
history(conversationToken, o = {}) {
|
|
491
|
+
return liveChatWireRequest(
|
|
492
|
+
void 0,
|
|
493
|
+
"GET",
|
|
494
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
|
|
495
|
+
);
|
|
496
|
+
},
|
|
206
497
|
async identityToken(externalId, opts = {}) {
|
|
207
498
|
if (typeof document !== "undefined" || typeof window !== "undefined") {
|
|
208
499
|
throw new Error(
|
|
209
500
|
"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 })."
|
|
210
501
|
);
|
|
211
502
|
}
|
|
212
|
-
const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
213
|
-
if (!secret) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
"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."
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
503
|
if (!externalId) {
|
|
219
504
|
throw new Error("liveChat.identityToken(): externalId is required.");
|
|
220
505
|
}
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
enc.encode(secret),
|
|
225
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
226
|
-
false,
|
|
227
|
-
["sign"]
|
|
228
|
-
);
|
|
229
|
-
const sig = await globalThis.crypto.subtle.sign(
|
|
230
|
-
"HMAC",
|
|
231
|
-
key,
|
|
232
|
-
enc.encode(externalId)
|
|
233
|
-
);
|
|
234
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
506
|
+
const explicit = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
507
|
+
const secret = explicit ?? await hmacHex(apiKey, LIVE_CHAT_IDENTITY_CONTEXT);
|
|
508
|
+
return hmacHex(secret, externalId);
|
|
235
509
|
}
|
|
236
510
|
};
|
|
237
511
|
const submissions = {
|