@brandfine/client 0.11.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 +18 -0
- package/README.md +11 -12
- package/dist/index.cjs +288 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -8
- package/dist/index.d.ts +152 -8
- package/dist/index.js +288 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,101 @@ export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost
|
|
|
3
3
|
export { Cache, CacheOptions, KeyedCache, KeyedCacheOptions, createCache, createKeyedCache } from './cache/index.js';
|
|
4
4
|
export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhookPayload, createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './webhook/index.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Headless Live Chat session — the conversation WITHOUT the widget.
|
|
8
|
+
*
|
|
9
|
+
* `bf.liveChat.createSession()` gives a consumer everything needed to
|
|
10
|
+
* render chat inline in their own design system: transcript state, a
|
|
11
|
+
* send method, and a subscribe API shaped for
|
|
12
|
+
* `useSyncExternalStore` / Svelte stores / Vue refs. Brandfine keeps
|
|
13
|
+
* owning transport, identity, threading and storage; the consumer
|
|
14
|
+
* owns pixels.
|
|
15
|
+
*
|
|
16
|
+
* Storage keys are IDENTICAL to the floating widget's, so a visitor
|
|
17
|
+
* who talks through the widget on one page and an inline panel on
|
|
18
|
+
* another continues the same thread.
|
|
19
|
+
*/
|
|
20
|
+
type ChatMessageSender = 'VISITOR' | 'AGENT' | 'SYSTEM';
|
|
21
|
+
type ChatMessage = {
|
|
22
|
+
id: string;
|
|
23
|
+
body: string;
|
|
24
|
+
sender: ChatMessageSender;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
};
|
|
27
|
+
type ConversationStatus = 'OPEN' | 'CLOSED';
|
|
28
|
+
type LiveChatSessionState = 'CONNECTING' | 'OPEN' | 'CLOSED' | 'ERROR';
|
|
29
|
+
/** The `/external/live-chat/config` shape — the RUNTIME config the
|
|
30
|
+
* widget fetches per page load. Distinct from the server-side
|
|
31
|
+
* `LiveChatBootstrap` (which additionally carries the publishable
|
|
32
|
+
* key + script path). */
|
|
33
|
+
type LiveChatRuntimeConfig = {
|
|
34
|
+
enabled: false;
|
|
35
|
+
} | {
|
|
36
|
+
enabled: true;
|
|
37
|
+
greeting: string | null;
|
|
38
|
+
offlineMessage: string | null;
|
|
39
|
+
theme: Record<string, string> | null;
|
|
40
|
+
online?: boolean;
|
|
41
|
+
};
|
|
42
|
+
type LiveChatSessionSnapshot = {
|
|
43
|
+
messages: ChatMessage[];
|
|
44
|
+
status: LiveChatSessionState;
|
|
45
|
+
/** Agents available right now (business-hours based today; agent
|
|
46
|
+
* presence once the realtime transport lands). */
|
|
47
|
+
online: boolean;
|
|
48
|
+
/** Localized via the session's `locale`. */
|
|
49
|
+
greeting: string | null;
|
|
50
|
+
offlineMessage: string | null;
|
|
51
|
+
/** An outbound `send()` is in flight. */
|
|
52
|
+
sending: boolean;
|
|
53
|
+
error: Error | null;
|
|
54
|
+
};
|
|
55
|
+
type StartConversationResult = {
|
|
56
|
+
enabled: false;
|
|
57
|
+
} | {
|
|
58
|
+
enabled: true;
|
|
59
|
+
conversationId: string;
|
|
60
|
+
conversationToken: string;
|
|
61
|
+
greeting: string | null;
|
|
62
|
+
resumed: boolean;
|
|
63
|
+
online?: boolean;
|
|
64
|
+
};
|
|
65
|
+
type LiveChatCreateSessionOptions = {
|
|
66
|
+
/** The server-half bootstrap (same object `install()` takes) —
|
|
67
|
+
* supplies the publishable key. */
|
|
68
|
+
config: {
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
publishableKey?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Signed identity — SAME shape and rules as `install()`. A bad
|
|
73
|
+
* signature downgrades to anonymous server-side; it is never a
|
|
74
|
+
* second identity path. */
|
|
75
|
+
visitor?: Record<string, unknown>;
|
|
76
|
+
/** BCP-47 tag for greeting/away localization. Defaults to
|
|
77
|
+
* `<html lang>` then browser language, like the widget. */
|
|
78
|
+
locale?: string;
|
|
79
|
+
/** Defaults to `location.href`. */
|
|
80
|
+
pageUrl?: string;
|
|
81
|
+
referrer?: string;
|
|
82
|
+
/** Transcript refresh cadence. Polling is the transport today; a
|
|
83
|
+
* realtime upgrade will keep this as the fallback. */
|
|
84
|
+
pollIntervalMs?: number;
|
|
85
|
+
};
|
|
86
|
+
type LiveChatSession = {
|
|
87
|
+
/** Current transcript (same array identity as the latest snapshot). */
|
|
88
|
+
readonly messages: readonly ChatMessage[];
|
|
89
|
+
readonly state: LiveChatSessionState;
|
|
90
|
+
send(body: string): Promise<ChatMessage>;
|
|
91
|
+
/** Listener fires on every snapshot change. Returns unsubscribe. */
|
|
92
|
+
subscribe(listener: (s: LiveChatSessionSnapshot) => void): () => void;
|
|
93
|
+
getSnapshot(): LiveChatSessionSnapshot;
|
|
94
|
+
/** Stop polling, abort in-flight work, drop listeners. Idempotent. */
|
|
95
|
+
close(): void;
|
|
96
|
+
/** Drop the stored conversation token and start a fresh thread —
|
|
97
|
+
* identity switch on a shared browser. */
|
|
98
|
+
reset(): Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
|
|
6
101
|
/**
|
|
7
102
|
* `createBrandfineClient` — the SDK's entry point.
|
|
8
103
|
*
|
|
@@ -415,6 +510,14 @@ type LiveChatInstallOptions = {
|
|
|
415
510
|
* onto the widget host, it performs no crypto.
|
|
416
511
|
*/
|
|
417
512
|
visitor?: LiveChatVisitor;
|
|
513
|
+
/**
|
|
514
|
+
* Locale for the widget's visitor-facing strings (greeting, away
|
|
515
|
+
* message), e.g. your i18n router's active locale. Optional — the
|
|
516
|
+
* widget falls back to the page's `<html lang>` and then the
|
|
517
|
+
* browser language. Resolved server-side against the workspace's
|
|
518
|
+
* configured translations; unknown locales get the default text.
|
|
519
|
+
*/
|
|
520
|
+
locale?: string;
|
|
418
521
|
};
|
|
419
522
|
type LiveChatApi = {
|
|
420
523
|
/**
|
|
@@ -439,18 +542,59 @@ type LiveChatApi = {
|
|
|
439
542
|
install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
|
|
440
543
|
/**
|
|
441
544
|
* Computes the visitor identity token:
|
|
442
|
-
* hex(HMAC_SHA256(
|
|
443
|
-
* throws in a browser context
|
|
444
|
-
*
|
|
445
|
-
* rather than ever emitting an unsigned/mis-signed payload.
|
|
545
|
+
* hex(HMAC_SHA256(signingSecret, externalId)). SERVER-ONLY — it
|
|
546
|
+
* throws in a browser context rather than ever computing next to
|
|
547
|
+
* the DOM.
|
|
446
548
|
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
549
|
+
* Signing secret, in order: `{ secret }` option →
|
|
550
|
+
* `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET` env → **derived from the
|
|
551
|
+
* client's API key** (HMAC with a fixed domain-separation
|
|
552
|
+
* constant; the Brandfine API derives the same value from its
|
|
553
|
+
* stored copy). The derived path needs ZERO extra configuration —
|
|
554
|
+
* an explicit secret is only for signers that shouldn't hold the
|
|
555
|
+
* broad key, or legacy workspaces without a revealable key
|
|
556
|
+
* (generate one in the CMS: Plugins → Live Chat → Integrate).
|
|
450
557
|
*/
|
|
451
558
|
identityToken: (externalId: string, opts?: {
|
|
452
559
|
secret?: string;
|
|
453
560
|
}) => Promise<string>;
|
|
561
|
+
/**
|
|
562
|
+
* Headless session — the conversation without the widget, for
|
|
563
|
+
* building your own inline chat UI. Browser-only. Takes the same
|
|
564
|
+
* `config` and signed `visitor` as `install()` (identical identity
|
|
565
|
+
* rules and thread continuity — it IS the same conversation the
|
|
566
|
+
* widget would join, sharing its storage keys). Transport today is
|
|
567
|
+
* polling (`pollIntervalMs`, default 5000); a realtime upgrade
|
|
568
|
+
* will keep this API and the poll as fallback.
|
|
569
|
+
*/
|
|
570
|
+
createSession: (opts: LiveChatCreateSessionOptions) => Promise<LiveChatSession>;
|
|
571
|
+
/**
|
|
572
|
+
* Raw, stateless wire methods for full control. NOTE on auth: the
|
|
573
|
+
* config + start endpoints authenticate with this client's
|
|
574
|
+
* `apiKey` header — in a browser, construct the client with the
|
|
575
|
+
* PUBLISHABLE key (`createBrandfineClient({ apiKey: config.publishableKey })`),
|
|
576
|
+
* never the broad key. Message endpoints authenticate with the
|
|
577
|
+
* conversation token alone — treat it as a bearer credential:
|
|
578
|
+
* don't log it, don't put it in URLs you share.
|
|
579
|
+
*/
|
|
580
|
+
runtimeConfig: (locale?: string) => Promise<LiveChatRuntimeConfig>;
|
|
581
|
+
startConversation: (input: {
|
|
582
|
+
visitorSessionId: string;
|
|
583
|
+
conversationToken?: string;
|
|
584
|
+
pageUrl?: string;
|
|
585
|
+
referrer?: string;
|
|
586
|
+
locale?: string;
|
|
587
|
+
visitor?: Record<string, unknown>;
|
|
588
|
+
}) => Promise<StartConversationResult>;
|
|
589
|
+
sendMessage: (conversationToken: string, input: {
|
|
590
|
+
body: string;
|
|
591
|
+
}) => Promise<ChatMessage>;
|
|
592
|
+
history: (conversationToken: string, opts?: {
|
|
593
|
+
after?: string;
|
|
594
|
+
}) => Promise<{
|
|
595
|
+
messages: ChatMessage[];
|
|
596
|
+
status: ConversationStatus;
|
|
597
|
+
}>;
|
|
454
598
|
};
|
|
455
599
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
456
600
|
|
|
@@ -468,4 +612,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
468
612
|
*/
|
|
469
613
|
declare const SDK_VERSION: "0.0.0";
|
|
470
614
|
|
|
471
|
-
export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatVisitor, SDK_VERSION, type Submission, createBrandfineClient };
|
|
615
|
+
export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type ChatMessage, type ChatMessageSender, type ConversationStatus, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, type LiveChatBootstrap, type LiveChatCreateSessionOptions, type LiveChatInstallOptions, type LiveChatInstallResult, type LiveChatRuntimeConfig, type LiveChatSession, type LiveChatSessionSnapshot, type LiveChatSessionState, type LiveChatVisitor, SDK_VERSION, type StartConversationResult, type Submission, createBrandfineClient };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,201 @@ 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 token;
|
|
64
|
+
let timer;
|
|
65
|
+
let closed = false;
|
|
66
|
+
let ticking = false;
|
|
67
|
+
const aborter = new AbortController();
|
|
68
|
+
function emit(patch) {
|
|
69
|
+
snapshot = { ...snapshot, ...patch };
|
|
70
|
+
for (const listener of listeners) listener(snapshot);
|
|
71
|
+
}
|
|
72
|
+
function appendMessages(incoming) {
|
|
73
|
+
const fresh = incoming.filter((m) => !seen.has(m.id));
|
|
74
|
+
if (fresh.length === 0) return;
|
|
75
|
+
for (const m of fresh) seen.add(m.id);
|
|
76
|
+
const messages = [...snapshot.messages, ...fresh];
|
|
77
|
+
cursor = messages[messages.length - 1].createdAt;
|
|
78
|
+
emit({ messages });
|
|
79
|
+
}
|
|
80
|
+
async function tick() {
|
|
81
|
+
if (ticking || closed || !token) return;
|
|
82
|
+
ticking = true;
|
|
83
|
+
try {
|
|
84
|
+
const result = await wire.history(token, { after: cursor });
|
|
85
|
+
if (closed) return;
|
|
86
|
+
appendMessages(result.messages);
|
|
87
|
+
if (result.status === "CLOSED" && snapshot.status !== "CLOSED") {
|
|
88
|
+
emit({ status: "CLOSED" });
|
|
89
|
+
stopPolling();
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
} finally {
|
|
93
|
+
ticking = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function startPolling() {
|
|
97
|
+
stopPolling();
|
|
98
|
+
timer = setInterval(() => void tick(), pollMs);
|
|
99
|
+
}
|
|
100
|
+
function stopPolling() {
|
|
101
|
+
if (timer !== void 0) clearInterval(timer);
|
|
102
|
+
timer = void 0;
|
|
103
|
+
}
|
|
104
|
+
async function connect(resumeToken) {
|
|
105
|
+
if (!publishableKey) {
|
|
106
|
+
emit({ status: "CLOSED" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const cfg = await wire.runtimeConfig(locale);
|
|
111
|
+
if (closed) return;
|
|
112
|
+
if (!cfg.enabled) {
|
|
113
|
+
emit({ status: "CLOSED" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
emit({
|
|
117
|
+
greeting: cfg.greeting,
|
|
118
|
+
offlineMessage: cfg.offlineMessage,
|
|
119
|
+
online: cfg.online !== false
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (closed) return;
|
|
123
|
+
emit({ status: "ERROR", error });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const started = await wire.startConversation({
|
|
128
|
+
visitorSessionId: getVisitorSessionId(),
|
|
129
|
+
conversationToken: resumeToken,
|
|
130
|
+
pageUrl: opts.pageUrl ?? (typeof location !== "undefined" ? location.href.slice(0, 2048) : void 0),
|
|
131
|
+
referrer: opts.referrer,
|
|
132
|
+
locale,
|
|
133
|
+
visitor: opts.visitor
|
|
134
|
+
});
|
|
135
|
+
if (closed) return;
|
|
136
|
+
if (!started.enabled) {
|
|
137
|
+
emit({ status: "CLOSED" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
token = started.conversationToken;
|
|
141
|
+
writeStoredToken(publishableKey, token);
|
|
142
|
+
if (started.online !== void 0) emit({ online: started.online });
|
|
143
|
+
emit({ status: "OPEN" });
|
|
144
|
+
await tick();
|
|
145
|
+
if (!closed) startPolling();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (closed) return;
|
|
148
|
+
emit({ status: "ERROR", error });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const session = {
|
|
152
|
+
get messages() {
|
|
153
|
+
return snapshot.messages;
|
|
154
|
+
},
|
|
155
|
+
get state() {
|
|
156
|
+
return snapshot.status;
|
|
157
|
+
},
|
|
158
|
+
getSnapshot: () => snapshot,
|
|
159
|
+
subscribe(listener) {
|
|
160
|
+
listeners.add(listener);
|
|
161
|
+
return () => listeners.delete(listener);
|
|
162
|
+
},
|
|
163
|
+
async send(body) {
|
|
164
|
+
const trimmed = body.trim();
|
|
165
|
+
if (!trimmed) throw new Error("send(): message body is empty");
|
|
166
|
+
if (!token) throw new Error("send(): session is not connected");
|
|
167
|
+
if (snapshot.status === "CLOSED") {
|
|
168
|
+
throw new Error("send(): conversation is closed \u2014 call reset()");
|
|
169
|
+
}
|
|
170
|
+
emit({ sending: true });
|
|
171
|
+
try {
|
|
172
|
+
const message = await wire.sendMessage(token, { body: trimmed });
|
|
173
|
+
appendMessages([message]);
|
|
174
|
+
return message;
|
|
175
|
+
} finally {
|
|
176
|
+
emit({ sending: false });
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
close() {
|
|
180
|
+
if (closed) return;
|
|
181
|
+
closed = true;
|
|
182
|
+
stopPolling();
|
|
183
|
+
aborter.abort();
|
|
184
|
+
listeners.clear();
|
|
185
|
+
},
|
|
186
|
+
async reset() {
|
|
187
|
+
if (publishableKey) writeStoredToken(publishableKey, null);
|
|
188
|
+
token = void 0;
|
|
189
|
+
cursor = void 0;
|
|
190
|
+
seen.clear();
|
|
191
|
+
stopPolling();
|
|
192
|
+
emit({ messages: [], status: "CONNECTING", error: null });
|
|
193
|
+
await connect(void 0);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await connect(publishableKey ? readStoredToken(publishableKey) : void 0);
|
|
197
|
+
return session;
|
|
198
|
+
}
|
|
199
|
+
|
|
5
200
|
// src/client.ts
|
|
6
201
|
var BrandfineApiError = class extends Error {
|
|
7
202
|
name = "BrandfineApiError";
|
|
@@ -21,6 +216,23 @@ var BrandfineApiError = class extends Error {
|
|
|
21
216
|
};
|
|
22
217
|
var INSTALLED_MARKER = "data-brandfine-analytics";
|
|
23
218
|
var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
|
|
219
|
+
var LIVE_CHAT_IDENTITY_CONTEXT = "brandfine:live-chat:identity:v1";
|
|
220
|
+
async function hmacHex(key, message) {
|
|
221
|
+
const enc = new TextEncoder();
|
|
222
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey(
|
|
223
|
+
"raw",
|
|
224
|
+
enc.encode(key),
|
|
225
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
226
|
+
false,
|
|
227
|
+
["sign"]
|
|
228
|
+
);
|
|
229
|
+
const sig = await globalThis.crypto.subtle.sign(
|
|
230
|
+
"HMAC",
|
|
231
|
+
cryptoKey,
|
|
232
|
+
enc.encode(message)
|
|
233
|
+
);
|
|
234
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
235
|
+
}
|
|
24
236
|
var GTAG_MARKER = "data-brandfine-gtag";
|
|
25
237
|
function injectGoogleTag(measurementId) {
|
|
26
238
|
if (typeof document === "undefined") return;
|
|
@@ -168,6 +380,28 @@ function createBrandfineClient(config) {
|
|
|
168
380
|
return { installed: true, websiteId: cfg.websiteId };
|
|
169
381
|
}
|
|
170
382
|
};
|
|
383
|
+
async function liveChatWireRequest(key, method, path, body) {
|
|
384
|
+
const url = `${baseUrl}${path}`;
|
|
385
|
+
const res = await fetchImpl(url, {
|
|
386
|
+
method,
|
|
387
|
+
headers: {
|
|
388
|
+
Accept: "application/json",
|
|
389
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
390
|
+
...key ? { "X-Api-Key": key } : {}
|
|
391
|
+
},
|
|
392
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
393
|
+
});
|
|
394
|
+
if (!res.ok) {
|
|
395
|
+
const text = await res.text().catch(() => "");
|
|
396
|
+
throw new BrandfineApiError({
|
|
397
|
+
status: res.status,
|
|
398
|
+
statusText: res.statusText,
|
|
399
|
+
body: text,
|
|
400
|
+
url
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return await res.json();
|
|
404
|
+
}
|
|
171
405
|
const liveChat = {
|
|
172
406
|
getConfig() {
|
|
173
407
|
return get("/external/live-chat/bootstrap");
|
|
@@ -192,6 +426,9 @@ function createBrandfineClient(config) {
|
|
|
192
426
|
if (opts.visitor?.externalId && opts.visitor.identityToken) {
|
|
193
427
|
host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
|
|
194
428
|
}
|
|
429
|
+
if (opts.locale) {
|
|
430
|
+
host.setAttribute("data-locale", opts.locale);
|
|
431
|
+
}
|
|
195
432
|
if (cfg.theme) {
|
|
196
433
|
for (const [key, value] of Object.entries(cfg.theme)) {
|
|
197
434
|
if (key.startsWith("--bf-chat-")) {
|
|
@@ -207,35 +444,66 @@ function createBrandfineClient(config) {
|
|
|
207
444
|
document.head.appendChild(script);
|
|
208
445
|
return { installed: true };
|
|
209
446
|
},
|
|
447
|
+
createSession(opts) {
|
|
448
|
+
const pk = opts.config.enabled ? opts.config.publishableKey : void 0;
|
|
449
|
+
const wire = {
|
|
450
|
+
runtimeConfig: (locale) => liveChatWireRequest(
|
|
451
|
+
pk,
|
|
452
|
+
"GET",
|
|
453
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
454
|
+
),
|
|
455
|
+
startConversation: (input) => liveChatWireRequest(pk, "POST", "/external/live-chat/conversations", input),
|
|
456
|
+
sendMessage: (conversationToken, input) => liveChatWireRequest(
|
|
457
|
+
void 0,
|
|
458
|
+
"POST",
|
|
459
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
460
|
+
input
|
|
461
|
+
),
|
|
462
|
+
history: (conversationToken, o = {}) => liveChatWireRequest(
|
|
463
|
+
void 0,
|
|
464
|
+
"GET",
|
|
465
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
|
|
466
|
+
)
|
|
467
|
+
};
|
|
468
|
+
return createLiveChatSession(wire, opts);
|
|
469
|
+
},
|
|
470
|
+
runtimeConfig(locale) {
|
|
471
|
+
return liveChatWireRequest(
|
|
472
|
+
apiKey,
|
|
473
|
+
"GET",
|
|
474
|
+
`/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
|
|
475
|
+
);
|
|
476
|
+
},
|
|
477
|
+
startConversation(input) {
|
|
478
|
+
return liveChatWireRequest(apiKey, "POST", "/external/live-chat/conversations", input);
|
|
479
|
+
},
|
|
480
|
+
sendMessage(conversationToken, input) {
|
|
481
|
+
return liveChatWireRequest(
|
|
482
|
+
void 0,
|
|
483
|
+
"POST",
|
|
484
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
|
|
485
|
+
input
|
|
486
|
+
);
|
|
487
|
+
},
|
|
488
|
+
history(conversationToken, o = {}) {
|
|
489
|
+
return liveChatWireRequest(
|
|
490
|
+
void 0,
|
|
491
|
+
"GET",
|
|
492
|
+
`/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
|
|
493
|
+
);
|
|
494
|
+
},
|
|
210
495
|
async identityToken(externalId, opts = {}) {
|
|
211
496
|
if (typeof document !== "undefined" || typeof window !== "undefined") {
|
|
212
497
|
throw new Error(
|
|
213
498
|
"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
499
|
);
|
|
215
500
|
}
|
|
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
501
|
if (!externalId) {
|
|
223
502
|
throw new Error("liveChat.identityToken(): externalId is required.");
|
|
224
503
|
}
|
|
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("");
|
|
504
|
+
const explicit = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
505
|
+
const secret = explicit ?? await hmacHex(apiKey, LIVE_CHAT_IDENTITY_CONTEXT);
|
|
506
|
+
return hmacHex(secret, externalId);
|
|
239
507
|
}
|
|
240
508
|
};
|
|
241
509
|
const submissions = {
|