@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/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(identitySecret, externalId)). SERVER-ONLY — it
443
- * throws in a browser context and throws when no secret is
444
- * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
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
- * Get the secret from the CMS: Plugins Live Chat Manage
448
- * settings Integrate Identity secret. Keep it in server env;
449
- * 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).
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 enc = new TextEncoder();
226
- const key = await globalThis.crypto.subtle.importKey(
227
- "raw",
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 = {