@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/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. No trailing slash the client
23
- * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */
24
- baseUrl: string;
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(identitySecret, externalId)). SERVER-ONLY — it
440
- * throws in a browser context and throws when no secret is
441
- * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
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
- * Get the secret from the CMS: Plugins Live Chat Manage
445
- * settings Integrate Identity secret. Keep it in server env;
446
- * shipping it to a browser lets anyone impersonate any visitor.
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 enc = new TextEncoder();
220
- const key = await globalThis.crypto.subtle.importKey(
221
- "raw",
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 = {