@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/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
|
*
|
|
@@ -19,9 +114,12 @@ export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhook
|
|
|
19
114
|
*/
|
|
20
115
|
|
|
21
116
|
type BrandfineClientConfig = {
|
|
22
|
-
/** Base URL of the Brandfine API.
|
|
23
|
-
*
|
|
24
|
-
|
|
117
|
+
/** Base URL of the Brandfine API. OPTIONAL — defaults to the
|
|
118
|
+
* production API (`https://api.brandfine.co`); on the server the
|
|
119
|
+
* `BRANDFINE_API_URL` env var overrides the default (set it for
|
|
120
|
+
* local dev / staging). Explicit option wins over both. No
|
|
121
|
+
* trailing slash — the client trims one if you pass it anyway. */
|
|
122
|
+
baseUrl?: string;
|
|
25
123
|
/** Workspace-scoped API key. Generated from the cms's Workspace
|
|
26
124
|
* settings; identifies which workspace the client talks to. */
|
|
27
125
|
apiKey: string;
|
|
@@ -412,6 +510,14 @@ type LiveChatInstallOptions = {
|
|
|
412
510
|
* onto the widget host, it performs no crypto.
|
|
413
511
|
*/
|
|
414
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;
|
|
415
521
|
};
|
|
416
522
|
type LiveChatApi = {
|
|
417
523
|
/**
|
|
@@ -436,18 +542,59 @@ type LiveChatApi = {
|
|
|
436
542
|
install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
|
|
437
543
|
/**
|
|
438
544
|
* Computes the visitor identity token:
|
|
439
|
-
* hex(HMAC_SHA256(
|
|
440
|
-
* throws in a browser context
|
|
441
|
-
*
|
|
442
|
-
* 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.
|
|
443
548
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
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).
|
|
447
557
|
*/
|
|
448
558
|
identityToken: (externalId: string, opts?: {
|
|
449
559
|
secret?: string;
|
|
450
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
|
+
}>;
|
|
451
598
|
};
|
|
452
599
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
453
600
|
|
|
@@ -465,4 +612,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
465
612
|
*/
|
|
466
613
|
declare const SDK_VERSION: "0.0.0";
|
|
467
614
|
|
|
468
|
-
export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, 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;
|
|
@@ -42,12 +254,18 @@ function injectGoogleTag(measurementId) {
|
|
|
42
254
|
gtag("config", measurementId);
|
|
43
255
|
}
|
|
44
256
|
var DEFAULT_USER_AGENT = "@brandfine/client";
|
|
257
|
+
var DEFAULT_BASE_URL = "https://api.brandfine.co";
|
|
258
|
+
function resolveBaseUrl(explicit) {
|
|
259
|
+
if (explicit) return explicit;
|
|
260
|
+
if (typeof process !== "undefined" && process.env?.BRANDFINE_API_URL) {
|
|
261
|
+
return process.env.BRANDFINE_API_URL;
|
|
262
|
+
}
|
|
263
|
+
return DEFAULT_BASE_URL;
|
|
264
|
+
}
|
|
45
265
|
function createBrandfineClient(config) {
|
|
46
|
-
if (!config.baseUrl)
|
|
47
|
-
throw new Error("createBrandfineClient: `baseUrl` is required");
|
|
48
266
|
if (!config.apiKey)
|
|
49
267
|
throw new Error("createBrandfineClient: `apiKey` is required");
|
|
50
|
-
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
268
|
+
const baseUrl = resolveBaseUrl(config.baseUrl).replace(/\/$/, "");
|
|
51
269
|
const apiKey = config.apiKey;
|
|
52
270
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
53
271
|
const userAgent = config.userAgent ?? DEFAULT_USER_AGENT;
|
|
@@ -162,6 +380,28 @@ function createBrandfineClient(config) {
|
|
|
162
380
|
return { installed: true, websiteId: cfg.websiteId };
|
|
163
381
|
}
|
|
164
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
|
+
}
|
|
165
405
|
const liveChat = {
|
|
166
406
|
getConfig() {
|
|
167
407
|
return get("/external/live-chat/bootstrap");
|
|
@@ -186,6 +426,9 @@ function createBrandfineClient(config) {
|
|
|
186
426
|
if (opts.visitor?.externalId && opts.visitor.identityToken) {
|
|
187
427
|
host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
|
|
188
428
|
}
|
|
429
|
+
if (opts.locale) {
|
|
430
|
+
host.setAttribute("data-locale", opts.locale);
|
|
431
|
+
}
|
|
189
432
|
if (cfg.theme) {
|
|
190
433
|
for (const [key, value] of Object.entries(cfg.theme)) {
|
|
191
434
|
if (key.startsWith("--bf-chat-")) {
|
|
@@ -201,35 +444,66 @@ function createBrandfineClient(config) {
|
|
|
201
444
|
document.head.appendChild(script);
|
|
202
445
|
return { installed: true };
|
|
203
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
|
+
},
|
|
204
495
|
async identityToken(externalId, opts = {}) {
|
|
205
496
|
if (typeof document !== "undefined" || typeof window !== "undefined") {
|
|
206
497
|
throw new Error(
|
|
207
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 })."
|
|
208
499
|
);
|
|
209
500
|
}
|
|
210
|
-
const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
|
|
211
|
-
if (!secret) {
|
|
212
|
-
throw new Error(
|
|
213
|
-
"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."
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
501
|
if (!externalId) {
|
|
217
502
|
throw new Error("liveChat.identityToken(): externalId is required.");
|
|
218
503
|
}
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
enc.encode(secret),
|
|
223
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
224
|
-
false,
|
|
225
|
-
["sign"]
|
|
226
|
-
);
|
|
227
|
-
const sig = await globalThis.crypto.subtle.sign(
|
|
228
|
-
"HMAC",
|
|
229
|
-
key,
|
|
230
|
-
enc.encode(externalId)
|
|
231
|
-
);
|
|
232
|
-
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);
|
|
233
507
|
}
|
|
234
508
|
};
|
|
235
509
|
const submissions = {
|