@brandfine/client 0.11.0 → 0.14.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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @brandfine/client
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 966879a: Appointments grows the authenticated half it was missing — book as a known person, read your own status back, and cancel, mirroring Live Chat's identity model:
8
+ - **Verified visitor identity on `createRequest`** — optional `visitor: { externalId, name?, email?, attributes?, identityToken }`, signed with the SAME `liveChat.identityToken(externalId)` you already use for chat (one signature, both plugins). Valid → the booking is attributed (verified name/email take precedence, `identityVerified: true` in the response, and the booking links to the person's chat threads via the shared `externalId`). Invalid or absent → the booking proceeds anonymously; it is never rejected.
9
+ - **Identity-scoped read-back** — `appointments.list({ externalId, identityToken })` returns the verified person's appointments (status, requestedAt, `scheduledAt` once confirmed, `declineReason` on rejection, timezone), newest first; `appointments.get(id, identity)` fetches one, 404ing identically for missing and foreign ids. POST under the hood so the identityToken never lands in URLs or logs. Bad signature → hard 401 (reads require proof).
10
+ - **`appointments.cancel(cancellationToken)`** — spends the one-time token `createRequest` already returned; no more hand-rolled HTTP call.
11
+ - **Publishable-key availability** — the workspace's scoped `brandfine_pk_…` key (minted/upgraded on Appointments activation, shown in the config) can now call `getAvailability` read-only, so fully-static sites render slots without a proxy. Booking, read-back and cancel stay on the broad workspace key.
12
+ - **Export fix** — `AppointmentAvailability`, `AppointmentSlot`, `CreateAppointmentRequestInput`, `CreatedAppointmentRequest` (plus the new `Appointment`, `AppointmentStatus`, `AppointmentIdentity`, `VerifiedVisitor`) are now exported from the package root; previously they were declared but unexported.
13
+
14
+ - 966879a: Live Chat gets a realtime transport — messages arrive in ~ms instead of on the 5-second tick, with **zero consumer code changes**:
15
+ - `createSession()` upgrades itself to a WebSocket when the API advertises `realtimeUrl` (new optional field on `LiveChatBootstrap` and the runtime config — older SDKs ignore it; this SDK against an older API simply polls). Same `subscribe`/`getSnapshot`/`send`/`reset`/`close` surface; a consumer never has to ask which transport is live.
16
+ - **Polling is the automatic, invisible fallback**: no `realtimeUrl`, no `WebSocket` in the runtime, a socket that won't open, or 5 consecutive reconnect failures → the session polls exactly as before. During reconnect backoff (1s→30s with jitter) polling covers the gap from the first millisecond.
17
+ - **Resume by `eventId`**: every message now carries a monotonic per-conversation `eventId`; reconnects replay everything after the last one seen — a laptop waking from sleep gets the missed replies, no gaps and no duplicates (also fixes a rare equal-millisecond drop window in the polling cursor via the new `afterEvent` history param).
18
+ - **Idempotent sends**: `send()` attaches a generated `clientId`; a retry after a dropped response returns the original message instead of duplicating.
19
+ - Socket auth mirrors REST exactly (publishable key + conversation token, sent in the first frame — never in the URL) and grants nothing REST wouldn't. `reset()` tears the socket down before switching threads (shared-browser guarantee); unknown frame types are ignored by both ends so typing/presence/read receipts can ship later without a breaking change.
20
+ - Workspaces can optionally restrict socket origins via the new `allowedOrigins` Live Chat setting (unset = any origin, matching the REST posture).
21
+
22
+ ## 0.13.0
23
+
24
+ ### Minor Changes
25
+
26
+ - 6c8f190: Headless Live Chat — build your own chat UI:
27
+ - `liveChat.createSession({ config, visitor, locale, pollIntervalMs })` exposes the conversation as typed, subscribable state (`messages`, `status`, `online`, localized `greeting`/`offlineMessage`, `sending`, `error`) with `send()`, `subscribe()` (shaped for `useSyncExternalStore`), `close()` and `reset()`. Shares the floating widget's storage, threading and identity semantics — same conversation everywhere; the widget remains unchanged.
28
+ - Raw wire methods for full control: `liveChat.runtimeConfig()`, `startConversation()`, `sendMessage()`, `history()`. In browsers, construct the client with the publishable key.
29
+ - New root exports: `ChatMessage`, `ChatMessageSender`, `ConversationStatus`, `LiveChatSession`, `LiveChatSessionSnapshot`, `LiveChatSessionState`, `LiveChatCreateSessionOptions`, `LiveChatRuntimeConfig`, `StartConversationResult` — with a root-export test so gaps can't regress again.
30
+ - Transport is polling (default 5000 ms, `pollIntervalMs` option); a future realtime upgrade keeps this API with polling as fallback.
31
+
32
+ ## 0.12.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 0e6c7c9: Live Chat localization + zero-config identity signing (code already on main, changeset restored after a merge dropped it):
37
+ - `liveChat.install({ config, locale })` pins the widget's language (falls back to the page's `<html lang>`, then the browser). Greeting/away-message translations are configured per locale in the CMS and resolved server-side; the visitor's page language is stored on the conversation.
38
+ - `liveChat.identityToken()` no longer requires a configured secret: by default it derives the signing secret from the client's API key (domain-separated HMAC; the API verifies against the same derivation). `{ secret }` / `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET` still override for decoupled signers and legacy workspaces. The missing-secret throw is gone — only the in-browser guard remains.
39
+
3
40
  ## 0.11.0
4
41
 
5
42
  ### Minor Changes
package/README.md CHANGED
@@ -62,6 +62,17 @@ await bf.appointments.createRequest({
62
62
  visitorName: 'Alex',
63
63
  visitorEmail: 'alex@example.com',
64
64
  requestedAt: slots[0].start,
65
+ // Optional: book as a KNOWN person — same signature Live Chat
66
+ // uses, so one identityToken serves both plugins. Unlocks
67
+ // list()/get() read-back for that person.
68
+ visitor: {
69
+ externalId: 'user:42',
70
+ identityToken: await bf.liveChat.identityToken('user:42'),
71
+ },
72
+ })
73
+ const mine = await bf.appointments.list({
74
+ externalId: 'user:42',
75
+ identityToken: await bf.liveChat.identityToken('user:42'),
65
76
  })
66
77
  ```
67
78
 
@@ -106,16 +117,13 @@ bf.liveChat.install({ config })
106
117
 
107
118
  ### Verified visitors
108
119
 
109
- On signed-in pages, tell the inbox **who** is chatting. Sign the
110
- identity on your server with the workspace's identity secret (CMS:
111
- Plugins Live Chat Manage settings Integrate), then pass it to
112
- `install()`:
120
+ On signed-in pages, tell the inbox **who** is chatting. Signing
121
+ needs **no extra configuration** `identityToken()` derives the
122
+ signing secret from the API key the client already holds:
113
123
 
114
124
  ```ts
115
125
  // Server — never in a browser:
116
- const identityToken = await bf.liveChat.identityToken(user.id, {
117
- secret: process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET,
118
- })
126
+ const identityToken = await bf.liveChat.identityToken(user.id)
119
127
  const visitor = {
120
128
  externalId: user.id,
121
129
  name: user.name,
@@ -133,11 +141,13 @@ the Brandfine inbox and continue across devices/sessions (same
133
141
  `externalId` = same person). An invalid or missing token silently
134
142
  downgrades to anonymous chat — never a blocked visitor.
135
143
 
136
- > **Security:** never ship the identity secret to a browser and
137
- > never compute the HMAC client-side either would let anyone
138
- > impersonate any visitor. `identityToken()` enforces this: it
139
- > throws in browser contexts and when the secret is missing. Rotate
140
- > the secret in the CMS if it ever leaks.
144
+ > **Security:** never compute the HMAC client-side the signing
145
+ > secret (derived from your API key, or an explicit one) must never
146
+ > reach a browser, or anyone could impersonate any visitor.
147
+ > `identityToken()` enforces this by throwing in browser contexts.
148
+ > An explicit secret (`{ secret }` / `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET`)
149
+ > overrides the derived default — only needed when the signer
150
+ > shouldn't hold the broad API key.
141
151
 
142
152
  Full reference: [docs.brandfine.co/docs/sdk/live-chat](https://docs.brandfine.co/docs/sdk/live-chat).
143
153
 
package/dist/index.cjs CHANGED
@@ -4,6 +4,356 @@ var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
4
4
  var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
5
5
  var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
6
6
 
7
+ // src/live-chat-session.ts
8
+ var SESSION_STORAGE_KEY = "bf-live-chat-session";
9
+ function tokenStorageKey(publishableKey) {
10
+ return `bf-live-chat-token:${publishableKey.slice(-12)}`;
11
+ }
12
+ function getVisitorSessionId() {
13
+ try {
14
+ const existing = localStorage.getItem(SESSION_STORAGE_KEY);
15
+ if (existing) return existing;
16
+ const generated = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().replace(/-/g, "") : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
17
+ localStorage.setItem(SESSION_STORAGE_KEY, generated);
18
+ return generated;
19
+ } catch {
20
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
21
+ }
22
+ }
23
+ function readStoredToken(publishableKey) {
24
+ try {
25
+ return localStorage.getItem(tokenStorageKey(publishableKey)) ?? void 0;
26
+ } catch {
27
+ return void 0;
28
+ }
29
+ }
30
+ function writeStoredToken(publishableKey, token) {
31
+ try {
32
+ if (token === null) localStorage.removeItem(tokenStorageKey(publishableKey));
33
+ else localStorage.setItem(tokenStorageKey(publishableKey), token);
34
+ } catch {
35
+ }
36
+ }
37
+ function resolvePageLocale(explicit) {
38
+ if (explicit) return explicit;
39
+ if (typeof document === "undefined") return void 0;
40
+ const htmlLang = document.documentElement.lang?.trim();
41
+ if (htmlLang) return htmlLang;
42
+ return typeof navigator !== "undefined" ? navigator.language || void 0 : void 0;
43
+ }
44
+ async function createLiveChatSession(wire, opts) {
45
+ if (typeof window === "undefined") {
46
+ throw new Error(
47
+ "liveChat.createSession() is browser-only \u2014 build the transcript UI client-side; fetch the bootstrap with getConfig() on the server."
48
+ );
49
+ }
50
+ const publishableKey = opts.config.enabled ? opts.config.publishableKey : void 0;
51
+ const locale = resolvePageLocale(opts.locale);
52
+ const pollMs = Math.max(1e3, opts.pollIntervalMs ?? 5e3);
53
+ let snapshot = {
54
+ messages: [],
55
+ status: "CONNECTING",
56
+ online: false,
57
+ greeting: null,
58
+ offlineMessage: null,
59
+ sending: false,
60
+ error: null
61
+ };
62
+ const listeners = /* @__PURE__ */ new Set();
63
+ const seen = /* @__PURE__ */ new Set();
64
+ let cursor;
65
+ let lastEventId = 0;
66
+ let token;
67
+ let timer;
68
+ let closed = false;
69
+ let ticking = false;
70
+ const aborter = new AbortController();
71
+ let realtimeUrl = opts.config.realtimeUrl;
72
+ let ws;
73
+ let wsLive = false;
74
+ let wsFailures = 0;
75
+ let wsGivenUp = false;
76
+ let reconnectTimer;
77
+ let pingTimer;
78
+ function emit(patch) {
79
+ snapshot = { ...snapshot, ...patch };
80
+ for (const listener of listeners) listener(snapshot);
81
+ }
82
+ function appendMessages(incoming) {
83
+ const fresh = incoming.filter((m) => !seen.has(m.id));
84
+ if (fresh.length === 0) {
85
+ for (const m of incoming) {
86
+ if (typeof m.eventId === "number" && m.eventId > lastEventId) {
87
+ lastEventId = m.eventId;
88
+ }
89
+ }
90
+ return;
91
+ }
92
+ for (const m of fresh) {
93
+ seen.add(m.id);
94
+ if (typeof m.eventId === "number" && m.eventId > lastEventId) {
95
+ lastEventId = m.eventId;
96
+ }
97
+ }
98
+ const messages = [...snapshot.messages, ...fresh];
99
+ cursor = messages[messages.length - 1].createdAt;
100
+ emit({ messages });
101
+ }
102
+ function markClosed() {
103
+ if (snapshot.status !== "CLOSED") emit({ status: "CLOSED" });
104
+ stopPolling();
105
+ teardownRealtime();
106
+ wsGivenUp = true;
107
+ }
108
+ async function tick() {
109
+ if (ticking || closed || !token) return;
110
+ ticking = true;
111
+ try {
112
+ const result = await wire.history(
113
+ token,
114
+ // The eventId cursor is exact (no equal-millisecond drop
115
+ // window); the timestamp cursor remains for old APIs that
116
+ // never sent eventIds.
117
+ lastEventId > 0 ? { afterEvent: lastEventId } : { after: cursor }
118
+ );
119
+ if (closed) return;
120
+ appendMessages(result.messages);
121
+ if (result.status === "CLOSED") markClosed();
122
+ } catch {
123
+ } finally {
124
+ ticking = false;
125
+ }
126
+ }
127
+ function startPolling() {
128
+ stopPolling();
129
+ timer = setInterval(() => void tick(), pollMs);
130
+ }
131
+ function stopPolling() {
132
+ if (timer !== void 0) clearInterval(timer);
133
+ timer = void 0;
134
+ }
135
+ function teardownRealtime() {
136
+ if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
137
+ reconnectTimer = void 0;
138
+ if (pingTimer !== void 0) clearInterval(pingTimer);
139
+ pingTimer = void 0;
140
+ wsLive = false;
141
+ if (ws) {
142
+ const socket = ws;
143
+ ws = void 0;
144
+ try {
145
+ socket.onopen = socket.onmessage = socket.onclose = socket.onerror = null;
146
+ socket.close();
147
+ } catch {
148
+ }
149
+ }
150
+ }
151
+ function scheduleReconnect() {
152
+ if (closed || wsGivenUp || reconnectTimer !== void 0) return;
153
+ if (wsFailures >= 5) {
154
+ wsGivenUp = true;
155
+ return;
156
+ }
157
+ const base = Math.min(3e4, 1e3 * 2 ** wsFailures);
158
+ const delay = base / 2 + Math.random() * (base / 2);
159
+ reconnectTimer = setTimeout(() => {
160
+ reconnectTimer = void 0;
161
+ startRealtime();
162
+ }, delay);
163
+ }
164
+ function handleSocketDown() {
165
+ teardownRealtime();
166
+ if (closed || wsGivenUp || snapshot.status === "CLOSED") return;
167
+ startPolling();
168
+ wsFailures += 1;
169
+ scheduleReconnect();
170
+ }
171
+ function startRealtime() {
172
+ if (closed || wsGivenUp || wsLive || ws !== void 0 || !realtimeUrl || !token || !publishableKey || typeof WebSocket === "undefined" || snapshot.status === "CLOSED") {
173
+ return;
174
+ }
175
+ let socket;
176
+ try {
177
+ socket = new WebSocket(realtimeUrl);
178
+ } catch {
179
+ wsFailures += 1;
180
+ scheduleReconnect();
181
+ return;
182
+ }
183
+ ws = socket;
184
+ socket.onopen = () => {
185
+ try {
186
+ socket.send(
187
+ JSON.stringify({
188
+ type: "auth",
189
+ publishableKey,
190
+ conversationToken: token,
191
+ lastEventId
192
+ })
193
+ );
194
+ } catch {
195
+ handleSocketDown();
196
+ }
197
+ };
198
+ socket.onmessage = (event) => {
199
+ let frame;
200
+ try {
201
+ frame = JSON.parse(String(event.data));
202
+ } catch {
203
+ return;
204
+ }
205
+ switch (frame.type) {
206
+ case "ready": {
207
+ wsLive = true;
208
+ wsFailures = 0;
209
+ stopPolling();
210
+ if (pingTimer === void 0) {
211
+ pingTimer = setInterval(() => {
212
+ try {
213
+ socket.send(JSON.stringify({ type: "ping" }));
214
+ } catch {
215
+ }
216
+ }, 3e4);
217
+ }
218
+ break;
219
+ }
220
+ case "message": {
221
+ const m = frame.message;
222
+ if (m && typeof m.id === "string") {
223
+ appendMessages([
224
+ {
225
+ ...m,
226
+ eventId: typeof frame.eventId === "number" ? frame.eventId : m.eventId
227
+ }
228
+ ]);
229
+ }
230
+ break;
231
+ }
232
+ case "status": {
233
+ if (frame.status === "CLOSED") markClosed();
234
+ break;
235
+ }
236
+ case "error": {
237
+ wsGivenUp = true;
238
+ teardownRealtime();
239
+ if (!closed && snapshot.status !== "CLOSED") startPolling();
240
+ break;
241
+ }
242
+ }
243
+ };
244
+ socket.onclose = () => handleSocketDown();
245
+ socket.onerror = () => {
246
+ };
247
+ }
248
+ async function connect(resumeToken) {
249
+ if (!publishableKey) {
250
+ emit({ status: "CLOSED" });
251
+ return;
252
+ }
253
+ try {
254
+ const cfg = await wire.runtimeConfig(locale);
255
+ if (closed) return;
256
+ if (!cfg.enabled) {
257
+ emit({ status: "CLOSED" });
258
+ return;
259
+ }
260
+ emit({
261
+ greeting: cfg.greeting,
262
+ offlineMessage: cfg.offlineMessage,
263
+ online: cfg.online !== false
264
+ });
265
+ if (cfg.realtimeUrl !== void 0) realtimeUrl = cfg.realtimeUrl;
266
+ } catch (error) {
267
+ if (closed) return;
268
+ emit({ status: "ERROR", error });
269
+ return;
270
+ }
271
+ try {
272
+ const started = await wire.startConversation({
273
+ visitorSessionId: getVisitorSessionId(),
274
+ conversationToken: resumeToken,
275
+ pageUrl: opts.pageUrl ?? (typeof location !== "undefined" ? location.href.slice(0, 2048) : void 0),
276
+ referrer: opts.referrer,
277
+ locale,
278
+ visitor: opts.visitor
279
+ });
280
+ if (closed) return;
281
+ if (!started.enabled) {
282
+ emit({ status: "CLOSED" });
283
+ return;
284
+ }
285
+ token = started.conversationToken;
286
+ writeStoredToken(publishableKey, token);
287
+ if (started.online !== void 0) emit({ online: started.online });
288
+ emit({ status: "OPEN" });
289
+ await tick();
290
+ if (!closed) {
291
+ startPolling();
292
+ startRealtime();
293
+ }
294
+ } catch (error) {
295
+ if (closed) return;
296
+ emit({ status: "ERROR", error });
297
+ }
298
+ }
299
+ const session = {
300
+ get messages() {
301
+ return snapshot.messages;
302
+ },
303
+ get state() {
304
+ return snapshot.status;
305
+ },
306
+ getSnapshot: () => snapshot,
307
+ subscribe(listener) {
308
+ listeners.add(listener);
309
+ return () => listeners.delete(listener);
310
+ },
311
+ async send(body) {
312
+ const trimmed = body.trim();
313
+ if (!trimmed) throw new Error("send(): message body is empty");
314
+ if (!token) throw new Error("send(): session is not connected");
315
+ if (snapshot.status === "CLOSED") {
316
+ throw new Error("send(): conversation is closed \u2014 call reset()");
317
+ }
318
+ emit({ sending: true });
319
+ try {
320
+ const clientId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
321
+ const message = await wire.sendMessage(token, {
322
+ body: trimmed,
323
+ clientId
324
+ });
325
+ appendMessages([message]);
326
+ return message;
327
+ } finally {
328
+ emit({ sending: false });
329
+ }
330
+ },
331
+ close() {
332
+ if (closed) return;
333
+ closed = true;
334
+ stopPolling();
335
+ teardownRealtime();
336
+ aborter.abort();
337
+ listeners.clear();
338
+ },
339
+ async reset() {
340
+ teardownRealtime();
341
+ wsFailures = 0;
342
+ wsGivenUp = false;
343
+ if (publishableKey) writeStoredToken(publishableKey, null);
344
+ token = void 0;
345
+ cursor = void 0;
346
+ lastEventId = 0;
347
+ seen.clear();
348
+ stopPolling();
349
+ emit({ messages: [], status: "CONNECTING", error: null });
350
+ await connect(void 0);
351
+ }
352
+ };
353
+ await connect(publishableKey ? readStoredToken(publishableKey) : void 0);
354
+ return session;
355
+ }
356
+
7
357
  // src/client.ts
8
358
  var BrandfineApiError = class extends Error {
9
359
  name = "BrandfineApiError";
@@ -23,6 +373,23 @@ var BrandfineApiError = class extends Error {
23
373
  };
24
374
  var INSTALLED_MARKER = "data-brandfine-analytics";
25
375
  var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
376
+ var LIVE_CHAT_IDENTITY_CONTEXT = "brandfine:live-chat:identity:v1";
377
+ async function hmacHex(key, message) {
378
+ const enc = new TextEncoder();
379
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
380
+ "raw",
381
+ enc.encode(key),
382
+ { name: "HMAC", hash: "SHA-256" },
383
+ false,
384
+ ["sign"]
385
+ );
386
+ const sig = await globalThis.crypto.subtle.sign(
387
+ "HMAC",
388
+ cryptoKey,
389
+ enc.encode(message)
390
+ );
391
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
392
+ }
26
393
  var GTAG_MARKER = "data-brandfine-gtag";
27
394
  function injectGoogleTag(measurementId) {
28
395
  if (typeof document === "undefined") return;
@@ -170,6 +537,28 @@ function createBrandfineClient(config) {
170
537
  return { installed: true, websiteId: cfg.websiteId };
171
538
  }
172
539
  };
540
+ async function liveChatWireRequest(key, method, path, body) {
541
+ const url = `${baseUrl}${path}`;
542
+ const res = await fetchImpl(url, {
543
+ method,
544
+ headers: {
545
+ Accept: "application/json",
546
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {},
547
+ ...key ? { "X-Api-Key": key } : {}
548
+ },
549
+ body: body !== void 0 ? JSON.stringify(body) : void 0
550
+ });
551
+ if (!res.ok) {
552
+ const text = await res.text().catch(() => "");
553
+ throw new BrandfineApiError({
554
+ status: res.status,
555
+ statusText: res.statusText,
556
+ body: text,
557
+ url
558
+ });
559
+ }
560
+ return await res.json();
561
+ }
173
562
  const liveChat = {
174
563
  getConfig() {
175
564
  return get("/external/live-chat/bootstrap");
@@ -194,6 +583,9 @@ function createBrandfineClient(config) {
194
583
  if (opts.visitor?.externalId && opts.visitor.identityToken) {
195
584
  host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
196
585
  }
586
+ if (opts.locale) {
587
+ host.setAttribute("data-locale", opts.locale);
588
+ }
197
589
  if (cfg.theme) {
198
590
  for (const [key, value] of Object.entries(cfg.theme)) {
199
591
  if (key.startsWith("--bf-chat-")) {
@@ -209,35 +601,67 @@ function createBrandfineClient(config) {
209
601
  document.head.appendChild(script);
210
602
  return { installed: true };
211
603
  },
604
+ createSession(opts) {
605
+ const pk = opts.config.enabled ? opts.config.publishableKey : void 0;
606
+ const wire = {
607
+ runtimeConfig: (locale) => liveChatWireRequest(
608
+ pk,
609
+ "GET",
610
+ `/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
611
+ ),
612
+ startConversation: (input) => liveChatWireRequest(pk, "POST", "/external/live-chat/conversations", input),
613
+ sendMessage: (conversationToken, input) => liveChatWireRequest(
614
+ void 0,
615
+ "POST",
616
+ `/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
617
+ input
618
+ ),
619
+ history: (conversationToken, o = {}) => liveChatWireRequest(
620
+ void 0,
621
+ "GET",
622
+ `/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${o.afterEvent !== void 0 ? `?afterEvent=${encodeURIComponent(String(o.afterEvent))}` : o.after ? `?after=${encodeURIComponent(o.after)}` : ""}`
623
+ )
624
+ };
625
+ return createLiveChatSession(wire, opts);
626
+ },
627
+ runtimeConfig(locale) {
628
+ return liveChatWireRequest(
629
+ apiKey,
630
+ "GET",
631
+ `/external/live-chat/config${locale ? `?locale=${encodeURIComponent(locale)}` : ""}`
632
+ );
633
+ },
634
+ startConversation(input) {
635
+ return liveChatWireRequest(apiKey, "POST", "/external/live-chat/conversations", input);
636
+ },
637
+ sendMessage(conversationToken, input) {
638
+ return liveChatWireRequest(
639
+ void 0,
640
+ "POST",
641
+ `/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages`,
642
+ input
643
+ );
644
+ },
645
+ history(conversationToken, o = {}) {
646
+ const qs = o.afterEvent !== void 0 ? `?afterEvent=${encodeURIComponent(String(o.afterEvent))}` : o.after ? `?after=${encodeURIComponent(o.after)}` : "";
647
+ return liveChatWireRequest(
648
+ void 0,
649
+ "GET",
650
+ `/external/live-chat/conversations/${encodeURIComponent(conversationToken)}/messages${qs}`
651
+ );
652
+ },
212
653
  async identityToken(externalId, opts = {}) {
213
654
  if (typeof document !== "undefined" || typeof window !== "undefined") {
214
655
  throw new Error(
215
656
  "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 })."
216
657
  );
217
658
  }
218
- const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
219
- if (!secret) {
220
- throw new Error(
221
- "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."
222
- );
223
- }
224
659
  if (!externalId) {
225
660
  throw new Error("liveChat.identityToken(): externalId is required.");
226
661
  }
227
- const enc = new TextEncoder();
228
- const key = await globalThis.crypto.subtle.importKey(
229
- "raw",
230
- enc.encode(secret),
231
- { name: "HMAC", hash: "SHA-256" },
232
- false,
233
- ["sign"]
234
- );
235
- const sig = await globalThis.crypto.subtle.sign(
236
- "HMAC",
237
- key,
238
- enc.encode(externalId)
239
- );
240
- return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
662
+ const explicit = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
663
+ const secret = explicit ?? await hmacHex(apiKey, LIVE_CHAT_IDENTITY_CONTEXT);
664
+ return hmacHex(secret, externalId);
241
665
  }
242
666
  };
243
667
  const submissions = {
@@ -305,6 +729,35 @@ function createBrandfineClient(config) {
305
729
  "/external/appointments/requests",
306
730
  input
307
731
  );
732
+ },
733
+ async list(identity) {
734
+ const res = await post(
735
+ "/external/appointments/requests/lookup",
736
+ identity
737
+ );
738
+ return res.appointments;
739
+ },
740
+ async get(id, identity) {
741
+ const res = await post(
742
+ "/external/appointments/requests/lookup",
743
+ { ...identity, id }
744
+ );
745
+ const row = res.appointments[0];
746
+ if (!row) {
747
+ throw new BrandfineApiError({
748
+ status: 404,
749
+ statusText: "Not Found",
750
+ body: "Appointment not found.",
751
+ url: "/external/appointments/requests/lookup"
752
+ });
753
+ }
754
+ return row;
755
+ },
756
+ cancel(cancellationToken) {
757
+ return post(
758
+ `/external/appointments/requests/${encodeURIComponent(cancellationToken)}/cancel`,
759
+ {}
760
+ );
308
761
  }
309
762
  };
310
763
  return {