@clivly/chat-widget 0.16.0 → 0.17.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/browser.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRoot } from "react-dom/client";
2
2
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/theme.ts
4
+ // src/theme.ts
5
5
  const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
6
6
  /** The attribute set by next-themes, daisyUI, and most `data-*` theme setups. */
7
7
  const THEME_ATTRIBUTE = "data-theme";
@@ -109,8 +109,51 @@ function parseTheme(value) {
109
109
  if (value === "auto" || value === "dark" || value === "light") return value;
110
110
  return "auto";
111
111
  }
112
- //#endregion
113
- //#region src/chat-widget.tsx
112
+ // src/visitor-token-storage.ts
113
+ /** Normalised the way the server matches contacts: trimmed, lower-case. */
114
+ function normalizeVisitorEmail(email) {
115
+ return email.trim().toLowerCase();
116
+ }
117
+ const HASH_MODULUS = 4294967291;
118
+ /**
119
+ * A polynomial string hash, reduced mod a prime below 2^32: stable,
120
+ * synchronous, and exact in doubles (every intermediate stays under 2^53).
121
+ * Not a security primitive.
122
+ */
123
+ function shortHash(value) {
124
+ let hash = 5381;
125
+ for (const char of value) hash = (hash * 33 + (char.codePointAt(0) ?? 0)) % HASH_MODULUS;
126
+ return hash.toString(16).padStart(8, "0");
127
+ }
128
+ function visitorTokenKey(widgetId, email) {
129
+ return `clivly-chat-widget:${widgetId}:visitor-token:${shortHash(normalizeVisitorEmail(email))}`;
130
+ }
131
+ /** The single per-widget key every widget before this change wrote. */
132
+ function legacyVisitorTokenKey(widgetId) {
133
+ return `clivly-chat-widget:${widgetId}:visitor-token`;
134
+ }
135
+ /**
136
+ * The token to offer for this person. Falls back to the legacy per-widget
137
+ * token once, so visitors who chatted before this change still resume — the
138
+ * server only honours it if they are the identity that thread belongs to.
139
+ */
140
+ function readVisitorToken(storage, widgetId, email) {
141
+ if (!storage) return null;
142
+ try {
143
+ return storage.getItem(visitorTokenKey(widgetId, email)) ?? storage.getItem(legacyVisitorTokenKey(widgetId));
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ /** Store this person's token and retire the legacy shared one. */
149
+ function writeVisitorToken(storage, widgetId, email, token) {
150
+ if (!storage) return;
151
+ try {
152
+ storage.setItem(visitorTokenKey(widgetId, email), token);
153
+ storage.removeItem?.(legacyVisitorTokenKey(widgetId));
154
+ } catch {}
155
+ }
156
+ // src/chat-widget.tsx
114
157
  const DELIVERY_LABELS = {
115
158
  failed: "Failed to send",
116
159
  pending: "Sending…",
@@ -532,9 +575,6 @@ function createLocalMessage(message, delivery = "sent") {
532
575
  delivery
533
576
  };
534
577
  }
535
- function createStorageKey(widgetId) {
536
- return `clivly-chat-widget:${widgetId}:visitor-token`;
537
- }
538
578
  function formatTimestamp(value) {
539
579
  const date = new Date(value);
540
580
  if (Number.isNaN(date.getTime())) return "";
@@ -960,6 +1000,33 @@ function useLiveConnection({ actions, createTransport, refreshSession, session,
960
1000
  }, [createTransport, session]);
961
1001
  return reconnecting;
962
1002
  }
1003
+ /** Who the host says is signed in: their normalised email, or null. */
1004
+ function hostIdentityOf(defaultVisitor) {
1005
+ return isCompleteVisitor(defaultVisitor) ? normalizeVisitorEmail(defaultVisitor.email) : null;
1006
+ }
1007
+ /**
1008
+ * The host's signed-in visitor changed under an open session — a sign-out, or
1009
+ * a different account signing in without a page reload. The session on
1010
+ * screen is the previous person's: drop it, its transcript and its socket, so
1011
+ * the next open starts as whoever is signed in now (chat widget session
1012
+ * review 2026-10-28, S1). A session the visitor started from the pre-chat
1013
+ * form is theirs and is left alone.
1014
+ */
1015
+ function useResetOnHostIdentityChange({ generationRef, hostIdentity, identityRef, reset, sessionSourceRef }) {
1016
+ const resetRef = useRef(reset);
1017
+ useEffect(() => {
1018
+ resetRef.current = reset;
1019
+ });
1020
+ useEffect(() => {
1021
+ const current = identityRef.current;
1022
+ if (!(current && sessionSourceRef.current === "host")) return;
1023
+ if (hostIdentity === normalizeVisitorEmail(current.email)) return;
1024
+ generationRef.current += 1;
1025
+ identityRef.current = null;
1026
+ sessionSourceRef.current = null;
1027
+ resetRef.current();
1028
+ }, [hostIdentity]);
1029
+ }
963
1030
  function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcher = "auto", launcherLabel = "Chat with us", placement = "bottom-right", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
964
1031
  const [isOpen, setIsOpen] = useState(initiallyOpen);
965
1032
  const panelId = useId();
@@ -978,6 +1045,11 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
978
1045
  const transportRef = useRef(null);
979
1046
  const listRef = useRef(null);
980
1047
  const identityRef = useRef(null);
1048
+ const sessionSourceRef = useRef(null);
1049
+ const generationRef = useRef(0);
1050
+ const hostIdentity = hostIdentityOf(defaultVisitor);
1051
+ const latestHostIdentityRef = useRef(hostIdentity);
1052
+ latestHostIdentityRef.current = hostIdentity;
981
1053
  useEffect(() => {
982
1054
  if (createTransport) return;
983
1055
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
@@ -1004,12 +1076,14 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1004
1076
  refreshSession: async (current) => {
1005
1077
  const identity = identityRef.current;
1006
1078
  if (!identity) throw new Error("No visitor to reconnect as.");
1079
+ const generation = generationRef.current;
1007
1080
  const next = await getSession({
1008
1081
  visitor: identity,
1009
1082
  visitorToken: current.visitorToken,
1010
1083
  widgetId
1011
1084
  });
1012
- storage?.setItem(createStorageKey(widgetId), next.visitorToken);
1085
+ if (generation !== generationRef.current) return;
1086
+ writeVisitorToken(storage, widgetId, identity.email, next.visitorToken);
1013
1087
  setSupportState(next.support?.state ?? null);
1014
1088
  setSession(next);
1015
1089
  },
@@ -1034,31 +1108,53 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1034
1108
  isReady,
1035
1109
  messages
1036
1110
  ]);
1037
- const startSession = useCallback(async (identity) => {
1111
+ const startSession = useCallback(async (identity, source = "form") => {
1038
1112
  setError(null);
1039
1113
  setIsConnecting(true);
1114
+ const generation = generationRef.current;
1115
+ const isStale = () => generation !== generationRef.current || source === "host" && normalizeVisitorEmail(identity.email) !== latestHostIdentityRef.current;
1040
1116
  try {
1041
1117
  const nextSession = await getSession({
1042
1118
  visitor: identity,
1043
- visitorToken: storage?.getItem(createStorageKey(widgetId)) ?? null,
1119
+ visitorToken: readVisitorToken(storage, widgetId, identity.email),
1044
1120
  widgetId
1045
1121
  });
1046
- storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
1122
+ if (isStale()) return;
1123
+ writeVisitorToken(storage, widgetId, identity.email, nextSession.visitorToken);
1047
1124
  identityRef.current = identity;
1125
+ sessionSourceRef.current = source;
1048
1126
  setSupportState(nextSession.support?.state ?? null);
1049
1127
  setSession(nextSession);
1050
1128
  setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
1051
1129
  } catch (sessionError) {
1130
+ if (isStale()) return;
1052
1131
  const resolved = sessionError instanceof Error ? sessionError : /* @__PURE__ */ new Error("Failed to start the conversation.");
1053
1132
  setError(resolved.message);
1054
1133
  } finally {
1055
- setIsConnecting(false);
1134
+ if (generation === generationRef.current) setIsConnecting(false);
1056
1135
  }
1057
1136
  }, [
1058
1137
  getSession,
1059
1138
  storage,
1060
1139
  widgetId
1061
1140
  ]);
1141
+ useResetOnHostIdentityChange({
1142
+ generationRef,
1143
+ hostIdentity,
1144
+ identityRef,
1145
+ reset: () => {
1146
+ setSession(null);
1147
+ setMessages([]);
1148
+ setSupportState(null);
1149
+ setError(null);
1150
+ setIsConnecting(false);
1151
+ setVisitor({
1152
+ email: defaultVisitor?.email ?? "",
1153
+ name: defaultVisitor?.name ?? ""
1154
+ });
1155
+ },
1156
+ sessionSourceRef
1157
+ });
1062
1158
  const submitVisitor = async (event) => {
1063
1159
  event.preventDefault();
1064
1160
  await startSession(visitor);
@@ -1069,7 +1165,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1069
1165
  startSession({
1070
1166
  email: defaultVisitor.email.trim(),
1071
1167
  name: defaultVisitor.name.trim()
1072
- }).catch(() => {});
1168
+ }, "host").catch(() => {});
1073
1169
  }, [
1074
1170
  defaultVisitor,
1075
1171
  error,
@@ -1298,8 +1394,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1298
1394
  ]
1299
1395
  });
1300
1396
  }
1301
- //#endregion
1302
- //#region src/transport.ts
1397
+ // src/transport.ts
1303
1398
  function isServerEvent(value) {
1304
1399
  if (typeof value !== "object" || value === null) return false;
1305
1400
  return typeof value.type === "string";
@@ -1344,8 +1439,7 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
1344
1439
  };
1345
1440
  };
1346
1441
  }
1347
- //#endregion
1348
- //#region src/clivly-transport.ts
1442
+ // src/clivly-transport.ts
1349
1443
  /**
1350
1444
  * The transport a host app should use against a real Clivly backend.
1351
1445
  *
@@ -1362,8 +1456,7 @@ function createClivlyTransport(options = {}) {
1362
1456
  }
1363
1457
  });
1364
1458
  }
1365
- //#endregion
1366
- //#region src/layout.ts
1459
+ // src/layout.ts
1367
1460
  const PLACEMENTS = [
1368
1461
  "bottom-left",
1369
1462
  "bottom-right",
@@ -1382,8 +1475,7 @@ function parsePlacement(value) {
1382
1475
  function parseLauncher(value) {
1383
1476
  return LAUNCHERS.find((launcher) => launcher === value) ?? "auto";
1384
1477
  }
1385
- //#endregion
1386
- //#region src/session.ts
1478
+ // src/session.ts
1387
1479
  function isWidgetSessionPayload(value) {
1388
1480
  return typeof value === "object" && value !== null;
1389
1481
  }
@@ -1405,8 +1497,7 @@ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
1405
1497
  return data;
1406
1498
  };
1407
1499
  }
1408
- //#endregion
1409
- //#region src/browser-mount.tsx
1500
+ // src/browser-mount.tsx
1410
1501
  const MOUNTED_DATA_ATTR = "clivlyMounted";
1411
1502
  /**
1412
1503
  * The identity a script-tag host already knows, read off the tag.
@@ -1498,11 +1589,9 @@ function autoMountChatWidgetScript$1(script = document.currentScript) {
1498
1589
  if (!script || scriptAlreadyMounted(script)) return null;
1499
1590
  return mountChatWidgetScript$1(script);
1500
1591
  }
1501
- //#endregion
1502
- //#region src/browser.tsx
1592
+ // src/browser.tsx
1503
1593
  const mountChatWidget = mountChatWidget$1;
1504
1594
  const mountChatWidgetScript = mountChatWidgetScript$1;
1505
1595
  const autoMountChatWidgetScript = autoMountChatWidgetScript$1;
1506
1596
  if (typeof document !== "undefined") autoMountChatWidgetScript();
1507
- //#endregion
1508
1597
  export { autoMountChatWidgetScript, mountChatWidget, mountChatWidgetScript };
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react_dom_client = require("react-dom/client");
3
3
  let react = require("react");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
- //#region src/theme.ts
5
+ // src/theme.ts
6
6
  const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
7
7
  /** The attribute set by next-themes, daisyUI, and most `data-*` theme setups. */
8
8
  const THEME_ATTRIBUTE = "data-theme";
@@ -110,8 +110,51 @@ function parseTheme(value) {
110
110
  if (value === "auto" || value === "dark" || value === "light") return value;
111
111
  return "auto";
112
112
  }
113
- //#endregion
114
- //#region src/chat-widget.tsx
113
+ // src/visitor-token-storage.ts
114
+ /** Normalised the way the server matches contacts: trimmed, lower-case. */
115
+ function normalizeVisitorEmail(email) {
116
+ return email.trim().toLowerCase();
117
+ }
118
+ const HASH_MODULUS = 4294967291;
119
+ /**
120
+ * A polynomial string hash, reduced mod a prime below 2^32: stable,
121
+ * synchronous, and exact in doubles (every intermediate stays under 2^53).
122
+ * Not a security primitive.
123
+ */
124
+ function shortHash(value) {
125
+ let hash = 5381;
126
+ for (const char of value) hash = (hash * 33 + (char.codePointAt(0) ?? 0)) % HASH_MODULUS;
127
+ return hash.toString(16).padStart(8, "0");
128
+ }
129
+ function visitorTokenKey(widgetId, email) {
130
+ return `clivly-chat-widget:${widgetId}:visitor-token:${shortHash(normalizeVisitorEmail(email))}`;
131
+ }
132
+ /** The single per-widget key every widget before this change wrote. */
133
+ function legacyVisitorTokenKey(widgetId) {
134
+ return `clivly-chat-widget:${widgetId}:visitor-token`;
135
+ }
136
+ /**
137
+ * The token to offer for this person. Falls back to the legacy per-widget
138
+ * token once, so visitors who chatted before this change still resume — the
139
+ * server only honours it if they are the identity that thread belongs to.
140
+ */
141
+ function readVisitorToken(storage, widgetId, email) {
142
+ if (!storage) return null;
143
+ try {
144
+ return storage.getItem(visitorTokenKey(widgetId, email)) ?? storage.getItem(legacyVisitorTokenKey(widgetId));
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+ /** Store this person's token and retire the legacy shared one. */
150
+ function writeVisitorToken(storage, widgetId, email, token) {
151
+ if (!storage) return;
152
+ try {
153
+ storage.setItem(visitorTokenKey(widgetId, email), token);
154
+ storage.removeItem?.(legacyVisitorTokenKey(widgetId));
155
+ } catch {}
156
+ }
157
+ // src/chat-widget.tsx
115
158
  const DELIVERY_LABELS = {
116
159
  failed: "Failed to send",
117
160
  pending: "Sending…",
@@ -533,9 +576,6 @@ function createLocalMessage(message, delivery = "sent") {
533
576
  delivery
534
577
  };
535
578
  }
536
- function createStorageKey(widgetId) {
537
- return `clivly-chat-widget:${widgetId}:visitor-token`;
538
- }
539
579
  function formatTimestamp(value) {
540
580
  const date = new Date(value);
541
581
  if (Number.isNaN(date.getTime())) return "";
@@ -961,6 +1001,33 @@ function useLiveConnection({ actions, createTransport, refreshSession, session,
961
1001
  }, [createTransport, session]);
962
1002
  return reconnecting;
963
1003
  }
1004
+ /** Who the host says is signed in: their normalised email, or null. */
1005
+ function hostIdentityOf(defaultVisitor) {
1006
+ return isCompleteVisitor(defaultVisitor) ? normalizeVisitorEmail(defaultVisitor.email) : null;
1007
+ }
1008
+ /**
1009
+ * The host's signed-in visitor changed under an open session — a sign-out, or
1010
+ * a different account signing in without a page reload. The session on
1011
+ * screen is the previous person's: drop it, its transcript and its socket, so
1012
+ * the next open starts as whoever is signed in now (chat widget session
1013
+ * review 2026-10-28, S1). A session the visitor started from the pre-chat
1014
+ * form is theirs and is left alone.
1015
+ */
1016
+ function useResetOnHostIdentityChange({ generationRef, hostIdentity, identityRef, reset, sessionSourceRef }) {
1017
+ const resetRef = (0, react.useRef)(reset);
1018
+ (0, react.useEffect)(() => {
1019
+ resetRef.current = reset;
1020
+ });
1021
+ (0, react.useEffect)(() => {
1022
+ const current = identityRef.current;
1023
+ if (!(current && sessionSourceRef.current === "host")) return;
1024
+ if (hostIdentity === normalizeVisitorEmail(current.email)) return;
1025
+ generationRef.current += 1;
1026
+ identityRef.current = null;
1027
+ sessionSourceRef.current = null;
1028
+ resetRef.current();
1029
+ }, [hostIdentity]);
1030
+ }
964
1031
  function ChatWidget({ className, createTransport, defaultVisitor, getSession, initiallyOpen = false, launcher = "auto", launcherLabel = "Chat with us", placement = "bottom-right", storage = DEFAULT_STORAGE, style, theme = "auto", widgetId }) {
965
1032
  const [isOpen, setIsOpen] = (0, react.useState)(initiallyOpen);
966
1033
  const panelId = (0, react.useId)();
@@ -979,6 +1046,11 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
979
1046
  const transportRef = (0, react.useRef)(null);
980
1047
  const listRef = (0, react.useRef)(null);
981
1048
  const identityRef = (0, react.useRef)(null);
1049
+ const sessionSourceRef = (0, react.useRef)(null);
1050
+ const generationRef = (0, react.useRef)(0);
1051
+ const hostIdentity = hostIdentityOf(defaultVisitor);
1052
+ const latestHostIdentityRef = (0, react.useRef)(hostIdentity);
1053
+ latestHostIdentityRef.current = hostIdentity;
982
1054
  (0, react.useEffect)(() => {
983
1055
  if (createTransport) return;
984
1056
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
@@ -1005,12 +1077,14 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1005
1077
  refreshSession: async (current) => {
1006
1078
  const identity = identityRef.current;
1007
1079
  if (!identity) throw new Error("No visitor to reconnect as.");
1080
+ const generation = generationRef.current;
1008
1081
  const next = await getSession({
1009
1082
  visitor: identity,
1010
1083
  visitorToken: current.visitorToken,
1011
1084
  widgetId
1012
1085
  });
1013
- storage?.setItem(createStorageKey(widgetId), next.visitorToken);
1086
+ if (generation !== generationRef.current) return;
1087
+ writeVisitorToken(storage, widgetId, identity.email, next.visitorToken);
1014
1088
  setSupportState(next.support?.state ?? null);
1015
1089
  setSession(next);
1016
1090
  },
@@ -1035,31 +1109,53 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1035
1109
  isReady,
1036
1110
  messages
1037
1111
  ]);
1038
- const startSession = (0, react.useCallback)(async (identity) => {
1112
+ const startSession = (0, react.useCallback)(async (identity, source = "form") => {
1039
1113
  setError(null);
1040
1114
  setIsConnecting(true);
1115
+ const generation = generationRef.current;
1116
+ const isStale = () => generation !== generationRef.current || source === "host" && normalizeVisitorEmail(identity.email) !== latestHostIdentityRef.current;
1041
1117
  try {
1042
1118
  const nextSession = await getSession({
1043
1119
  visitor: identity,
1044
- visitorToken: storage?.getItem(createStorageKey(widgetId)) ?? null,
1120
+ visitorToken: readVisitorToken(storage, widgetId, identity.email),
1045
1121
  widgetId
1046
1122
  });
1047
- storage?.setItem(createStorageKey(widgetId), nextSession.visitorToken);
1123
+ if (isStale()) return;
1124
+ writeVisitorToken(storage, widgetId, identity.email, nextSession.visitorToken);
1048
1125
  identityRef.current = identity;
1126
+ sessionSourceRef.current = source;
1049
1127
  setSupportState(nextSession.support?.state ?? null);
1050
1128
  setSession(nextSession);
1051
1129
  setMessages(sortMessages(nextSession.messages.map((message) => createLocalMessage(message))));
1052
1130
  } catch (sessionError) {
1131
+ if (isStale()) return;
1053
1132
  const resolved = sessionError instanceof Error ? sessionError : /* @__PURE__ */ new Error("Failed to start the conversation.");
1054
1133
  setError(resolved.message);
1055
1134
  } finally {
1056
- setIsConnecting(false);
1135
+ if (generation === generationRef.current) setIsConnecting(false);
1057
1136
  }
1058
1137
  }, [
1059
1138
  getSession,
1060
1139
  storage,
1061
1140
  widgetId
1062
1141
  ]);
1142
+ useResetOnHostIdentityChange({
1143
+ generationRef,
1144
+ hostIdentity,
1145
+ identityRef,
1146
+ reset: () => {
1147
+ setSession(null);
1148
+ setMessages([]);
1149
+ setSupportState(null);
1150
+ setError(null);
1151
+ setIsConnecting(false);
1152
+ setVisitor({
1153
+ email: defaultVisitor?.email ?? "",
1154
+ name: defaultVisitor?.name ?? ""
1155
+ });
1156
+ },
1157
+ sessionSourceRef
1158
+ });
1063
1159
  const submitVisitor = async (event) => {
1064
1160
  event.preventDefault();
1065
1161
  await startSession(visitor);
@@ -1070,7 +1166,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
1070
1166
  startSession({
1071
1167
  email: defaultVisitor.email.trim(),
1072
1168
  name: defaultVisitor.name.trim()
1073
- }).catch(() => {});
1169
+ }, "host").catch(() => {});
1074
1170
  }, [
1075
1171
  defaultVisitor,
1076
1172
  error,
@@ -1314,8 +1410,7 @@ function PreChatForm() {
1314
1410
  function ConversationView() {
1315
1411
  return null;
1316
1412
  }
1317
- //#endregion
1318
- //#region src/transport.ts
1413
+ // src/transport.ts
1319
1414
  function isServerEvent(value) {
1320
1415
  if (typeof value !== "object" || value === null) return false;
1321
1416
  return typeof value.type === "string";
@@ -1417,8 +1512,7 @@ function createLoopbackTransport({ operatorName = "Clivly", operatorReply = (bod
1417
1512
  };
1418
1513
  };
1419
1514
  }
1420
- //#endregion
1421
- //#region src/clivly-transport.ts
1515
+ // src/clivly-transport.ts
1422
1516
  /**
1423
1517
  * The transport a host app should use against a real Clivly backend.
1424
1518
  *
@@ -1435,8 +1529,7 @@ function createClivlyTransport$1(options = {}) {
1435
1529
  }
1436
1530
  });
1437
1531
  }
1438
- //#endregion
1439
- //#region src/layout.ts
1532
+ // src/layout.ts
1440
1533
  const PLACEMENTS = [
1441
1534
  "bottom-left",
1442
1535
  "bottom-right",
@@ -1455,8 +1548,7 @@ function parsePlacement(value) {
1455
1548
  function parseLauncher(value) {
1456
1549
  return LAUNCHERS.find((launcher) => launcher === value) ?? "auto";
1457
1550
  }
1458
- //#endregion
1459
- //#region src/session.ts
1551
+ // src/session.ts
1460
1552
  function isWidgetSessionPayload(value) {
1461
1553
  return typeof value === "object" && value !== null;
1462
1554
  }
@@ -1478,8 +1570,7 @@ function createSessionFetcher({ fetchImpl = fetch, sessionUrl }) {
1478
1570
  return data;
1479
1571
  };
1480
1572
  }
1481
- //#endregion
1482
- //#region src/browser-mount.tsx
1573
+ // src/browser-mount.tsx
1483
1574
  const MOUNTED_DATA_ATTR = "clivlyMounted";
1484
1575
  /**
1485
1576
  * The identity a script-tag host already knows, read off the tag.
@@ -1571,13 +1662,11 @@ function autoMountChatWidgetScript$1(script = document.currentScript) {
1571
1662
  if (!script || scriptAlreadyMounted(script)) return null;
1572
1663
  return mountChatWidgetScript$1(script);
1573
1664
  }
1574
- //#endregion
1575
- //#region src/index.ts
1665
+ // src/index.ts
1576
1666
  const autoMountChatWidgetScript = autoMountChatWidgetScript$1;
1577
1667
  const createClivlyTransport = createClivlyTransport$1;
1578
1668
  const mountChatWidget = mountChatWidget$1;
1579
1669
  const mountChatWidgetScript = mountChatWidgetScript$1;
1580
- //#endregion
1581
1670
  exports.ChatLauncher = ChatLauncher;
1582
1671
  exports.ChatPanel = ChatPanel;
1583
1672
  exports.ChatWidget = ChatWidget;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Root } from "react-dom/client";
2
2
  import { CSSProperties, ReactElement } from "react";
3
- //#region src/layout.d.ts
4
3
  /**
5
4
  * Where the widget sits on the host page, and what its launcher looks like.
6
5
  *
@@ -20,8 +19,6 @@ type ChatWidgetPlacement = "bottom-left" | "bottom-right" | "inline";
20
19
  * The label stays the button's accessible name in every variant.
21
20
  */
22
21
  type ChatWidgetLauncher = "auto" | "icon" | "pill";
23
- //#endregion
24
- //#region src/theme.d.ts
25
22
  /**
26
23
  * Theme resolution for the widget.
27
24
  *
@@ -46,8 +43,6 @@ interface ResolveThemeOptions {
46
43
  * the majority of pages.
47
44
  */
48
45
  export declare function resolveTheme(theme: ChatWidgetTheme, { root, view }?: ResolveThemeOptions): ResolvedTheme;
49
- //#endregion
50
- //#region src/types.d.ts
51
46
  type WidgetSenderType = "visitor" | "operator" | "system";
52
47
  interface WidgetConfig {
53
48
  brandColor?: string | null;
@@ -164,8 +159,6 @@ interface WidgetErrorEvent {
164
159
  type: "error";
165
160
  }
166
161
  type WidgetServerEvent = WidgetAssistantStatusEvent | WidgetConversationSnapshotEvent | WidgetErrorEvent | WidgetMessageAckEvent | WidgetMessageCreatedEvent | WidgetSessionReadyEvent | WidgetSupportStateEvent;
167
- //#endregion
168
- //#region src/transport.d.ts
169
162
  interface ChatTransportHandlers {
170
163
  onClose: () => void;
171
164
  onError: (error: Error) => void;
@@ -204,8 +197,21 @@ interface CreateLoopbackTransportOptions {
204
197
  replyDelayMs?: number;
205
198
  }
206
199
  export declare function createLoopbackTransport({ operatorName, operatorReply, replyDelayMs }?: CreateLoopbackTransportOptions): CreateChatTransport;
207
- //#endregion
208
- //#region src/chat-widget.d.ts
200
+ /**
201
+ * Where the widget keeps the visitor token that resumes a conversation.
202
+ *
203
+ * Keyed by widget AND by who the token was issued to. One key per widget used
204
+ * to hand the token to whoever opened the widget next on that browser — after
205
+ * a sign-out, or between two people who don't sign in at all — and the server
206
+ * resumed the previous person's thread for them (chat widget session review
207
+ * 2026-10-28, S1). The server now refuses that resume on its own; this keeps
208
+ * each person's token to themselves so a returning visitor still resumes.
209
+ *
210
+ * The identity part is a short hash of the normalised email, so the address
211
+ * itself isn't written into storage keys. It only partitions storage: the
212
+ * server's identity check is the security boundary, not this.
213
+ */
214
+ type VisitorTokenStorage = Pick<Storage, "getItem" | "setItem"> & Partial<Pick<Storage, "removeItem">>;
209
215
  interface ChatWidgetProps {
210
216
  className?: string;
211
217
  createTransport?: CreateChatTransport;
@@ -240,7 +246,12 @@ interface ChatWidgetProps {
240
246
  * `style` to sit beneath the host's own dialogs.
241
247
  */
242
248
  placement?: ChatWidgetPlacement;
243
- storage?: Pick<Storage, "getItem" | "setItem">;
249
+ /**
250
+ * Where the visitor token is kept, per widget and per visitor (see
251
+ * visitor-token-storage.ts). `removeItem` is optional; with it, the
252
+ * pre-0.16 shared token is retired after its first use.
253
+ */
254
+ storage?: VisitorTokenStorage;
244
255
  /**
245
256
  * Inline styles on the widget root. The intended use is overriding palette
246
257
  * tokens — `{ "--clivly-chat-panel": "#0b0f19" }` — which works because
@@ -261,8 +272,6 @@ export declare function ChatPanel(): null;
261
272
  export declare function MessageComposer(): null;
262
273
  export declare function PreChatForm(): null;
263
274
  export declare function ConversationView(): null;
264
- //#endregion
265
- //#region src/browser-mount.d.ts
266
275
  interface MountChatWidgetOptions$1 extends Omit<ChatWidgetProps, "getSession" | "widgetId"> {
267
276
  container: Element | ShadowRoot;
268
277
  sessionUrl: string;
@@ -287,8 +296,6 @@ interface ScriptMountResult$1 extends ChatWidgetMount$1 {
287
296
  }
288
297
  declare function mountChatWidgetScript$1(script?: HTMLScriptElement): ScriptMountResult$1;
289
298
  declare function autoMountChatWidgetScript$1(script?: HTMLScriptElement | null): ScriptMountResult$1 | null;
290
- //#endregion
291
- //#region src/clivly-transport.d.ts
292
299
  interface CreateClivlyTransportOptions$1 {
293
300
  createSocket?: (url: string, protocols?: string | string[]) => WebSocket;
294
301
  }
@@ -300,15 +307,11 @@ interface CreateClivlyTransportOptions$1 {
300
307
  * produce a silently dead socket.
301
308
  */
302
309
  declare function createClivlyTransport$1(options?: CreateClivlyTransportOptions$1): CreateChatTransport;
303
- //#endregion
304
- //#region src/session.d.ts
305
310
  interface CreateSessionFetcherOptions {
306
311
  fetchImpl?: typeof fetch;
307
312
  sessionUrl: string;
308
313
  }
309
314
  export declare function createSessionFetcher({ fetchImpl, sessionUrl }: CreateSessionFetcherOptions): GetSession;
310
- //#endregion
311
- //#region src/index.d.ts
312
315
  export type ChatWidgetMount = ChatWidgetMount$1;
313
316
  export type MountChatWidgetOptions = MountChatWidgetOptions$1;
314
317
  export type ScriptMountResult = ScriptMountResult$1;
@@ -317,5 +320,4 @@ export type CreateClivlyTransportOptions = CreateClivlyTransportOptions$1;
317
320
  export declare const createClivlyTransport: typeof createClivlyTransport$1;
318
321
  export declare const mountChatWidget: typeof mountChatWidget$1;
319
322
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
320
- //#endregion
321
323
  export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetLauncher, ChatWidgetPlacement, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetAssistantStatusEvent, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, WidgetClientTopicEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary, WidgetSupport, WidgetSupportState, WidgetSupportStateEvent, WidgetSupportTopic };