@syntrologie/adapt-chatbot 2.26.0 → 2.27.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.
@@ -11,13 +11,21 @@
11
11
  * `window.__SYNTRO_CONFIG__.token` (set by runtime-config.js) — we read
12
12
  * it at mount time and forward it via `Authorization: Bearer` header.
13
13
  *
14
- * 2. Cloudflare Turnstile bot-check token. When `TURNSTILE_SITEKEY` is set
15
- * (hardcoded at the top of this file — operator updates after running
16
- * terraform apply on cloudflare/turnstile in the infra repo), the
17
- * widget dynamically loads Cloudflare's Turnstile JS, renders an
18
- * invisible widget, and acquires a token before the first message.
19
- * The token is forwarded via `CF-Turnstile-Token` header.
20
- * If sitekey is empty (default), this step is skipped entirely.
14
+ * 2. Cloudflare Turnstile bot-check token (two-phase). When
15
+ * `TURNSTILE_SITEKEY` is set:
16
+ * a. Try invisible first passive fingerprints, no UI. Clean
17
+ * traffic resolves in milliseconds. Token forwarded via
18
+ * `CF-Turnstile-Token`.
19
+ * b. If CF declines to grant silently (datacenter IP, automation
20
+ * signals, strict-privacy browser), render a visible managed
21
+ * widget inside the chat container so CF can present the
22
+ * "I am human" checkbox. On solve, swap to the chat UI.
23
+ * c. If both phases fail, fall through with no token; backend
24
+ * enforcement returns 403 and the existing fallback card
25
+ * renders. The widget mode must be `managed` on the CF side
26
+ * for the visible escalation to work — see
27
+ * cloudflare/turnstile/main.tf in syntro-infra.
28
+ * If sitekey is empty, this step is skipped entirely.
21
29
  *
22
30
  * 3. Fallback card. If the first agent run fails (Cloudflare Turnstile
23
31
  * bot-check failure, CORS / network error, or a server-side rejection),
@@ -37,6 +45,28 @@ export interface ChatAssistantLitProps {
37
45
  runtime: ChatbotWidgetRuntime;
38
46
  tileId?: string;
39
47
  }
48
+ export interface AcquireTurnstileOptions {
49
+ /**
50
+ * Widget size. `invisible` for the silent first attempt; `flexible`
51
+ * (or `normal`) for the visible managed checkbox after silent failure.
52
+ */
53
+ size: 'invisible' | 'flexible' | 'normal' | 'compact';
54
+ /**
55
+ * Where to mount the widget. For `invisible` this is an off-screen
56
+ * host; for the visible sizes it must be a container that's actually
57
+ * in the layout so CF can render the challenge UI. When omitted, an
58
+ * off-screen div is created and removed automatically (suitable for
59
+ * `invisible` only — visible sizes need a real host).
60
+ */
61
+ host?: HTMLElement;
62
+ /**
63
+ * Cancels an in-flight acquisition. On abort, the promise resolves to
64
+ * `null`, the CF widget is removed from the registry, and any owned
65
+ * host is torn down. Required for cleanup-during-challenge — without
66
+ * it, a user dismissing the tile mid-checkbox leaks the widget.
67
+ */
68
+ signal?: AbortSignal;
69
+ }
40
70
  /**
41
71
  * Acquire a Cloudflare Turnstile token. Returns null when:
42
72
  * • TURNSTILE_SITEKEY is empty (Turnstile disabled at build time)
@@ -44,14 +74,14 @@ export interface ChatAssistantLitProps {
44
74
  * • the render callback doesn't fire within TURNSTILE_ACQUIRE_TIMEOUT_MS
45
75
  * • Cloudflare returns an error-callback (e.g., rate-limited, bad sitekey)
46
76
  *
47
- * When null is returned, the widget proceeds WITHOUT a Turnstile token.
48
- * If backend enforcement is on, the request 403s and the fallback card
49
- * renders via the existing failure-detection path. If enforcement is off,
50
- * the request succeeds.
77
+ * Calls `turnstile.remove(widgetId)` on settle so CF's internal widget
78
+ * registry doesn't leak across re-mounts (CF will otherwise log
79
+ * `Cannot find Widget ...` when subsequent renders look up the old
80
+ * widget by id and find the host gone).
51
81
  *
52
82
  * Exported for tests; the production code path goes through `mount()`.
53
83
  */
54
- export declare function acquireTurnstileToken(): Promise<string | null>;
84
+ export declare function acquireTurnstileToken(opts?: AcquireTurnstileOptions): Promise<string | null>;
55
85
  export declare const ChatAssistantLitMountable: {
56
86
  mount(container: HTMLElement, mountConfig?: Record<string, unknown>): () => void;
57
87
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ChatAssistantLit.d.ts","sourceRoot":"","sources":["../src/ChatAssistantLit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAE,aAAa,EAAmB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvF,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAqGD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkDpE;AAiGD,eAAO,MAAM,yBAAyB;qBACnB,WAAW,gBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CA2LpE,CAAC"}
1
+ {"version":3,"file":"ChatAssistantLit.d.ts","sourceRoot":"","sources":["../src/ChatAssistantLit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAE,aAAa,EAAmB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvF,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAqGD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;IACtD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,GAAE,uBAA+C,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkFxB;AAqGD,eAAO,MAAM,yBAAyB;qBACnB,WAAW,gBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAwPpE,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ChatAssistantLitMountable,
3
3
  acquireTurnstileToken
4
- } from "./chunk-O7RWNUVU.js";
4
+ } from "./chunk-W457NMGD.js";
5
5
  import "./chunk-UVKRO5ER.js";
6
6
  export {
7
7
  ChatAssistantLitMountable,
@@ -10741,7 +10741,7 @@ function loadTurnstileScript() {
10741
10741
  document.head.appendChild(script);
10742
10742
  });
10743
10743
  }
10744
- async function acquireTurnstileToken() {
10744
+ async function acquireTurnstileToken(opts = { size: "invisible" }) {
10745
10745
  if (!TURNSTILE_SITEKEY) return null;
10746
10746
  if (typeof window === "undefined" || typeof document === "undefined") return null;
10747
10747
  let turnstile;
@@ -10755,24 +10755,44 @@ async function acquireTurnstileToken() {
10755
10755
  } catch {
10756
10756
  return null;
10757
10757
  }
10758
+ const ownsHost = !opts.host;
10759
+ const host = opts.host ?? (() => {
10760
+ const h = document.createElement("div");
10761
+ h.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:0;height:0;overflow:hidden;";
10762
+ h.setAttribute("data-adaptive-chatbot-turnstile-host", "true");
10763
+ document.body.appendChild(h);
10764
+ return h;
10765
+ })();
10758
10766
  return new Promise((resolve) => {
10759
- const host = document.createElement("div");
10760
- host.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:0;height:0;overflow:hidden;";
10761
- host.setAttribute("data-adaptive-chatbot-turnstile-host", "true");
10762
- document.body.appendChild(host);
10763
10767
  let settled = false;
10768
+ let widgetId = null;
10764
10769
  const settle = (value) => {
10765
10770
  if (settled) return;
10766
10771
  settled = true;
10767
10772
  clearTimeout(timer);
10768
- setTimeout(() => host.remove(), 0);
10773
+ if (typeof widgetId === "string") {
10774
+ try {
10775
+ turnstile.remove?.(widgetId);
10776
+ } catch {
10777
+ }
10778
+ }
10779
+ if (ownsHost) {
10780
+ setTimeout(() => host.remove(), 0);
10781
+ }
10769
10782
  resolve(value);
10770
10783
  };
10771
10784
  const timer = setTimeout(() => settle(null), TURNSTILE_ACQUIRE_TIMEOUT_MS);
10785
+ if (opts.signal) {
10786
+ if (opts.signal.aborted) {
10787
+ settle(null);
10788
+ return;
10789
+ }
10790
+ opts.signal.addEventListener("abort", () => settle(null), { once: true });
10791
+ }
10772
10792
  try {
10773
- turnstile.render(host, {
10793
+ widgetId = turnstile.render(host, {
10774
10794
  sitekey: TURNSTILE_SITEKEY,
10775
- size: "invisible",
10795
+ size: opts.size,
10776
10796
  callback: (token) => settle(token),
10777
10797
  "error-callback": () => settle(null),
10778
10798
  "timeout-callback": () => settle(null),
@@ -10833,8 +10853,10 @@ function renderFallbackHtml(fallback) {
10833
10853
  return "&gt;";
10834
10854
  case '"':
10835
10855
  return "&quot;";
10836
- default:
10856
+ case "'":
10837
10857
  return "&#39;";
10858
+ default:
10859
+ return ch;
10838
10860
  }
10839
10861
  });
10840
10862
  const ctaHtml = ctaHref && /^https?:\/\//.test(ctaHref) ? `<a href="${escapeHtml(ctaHref)}" target="_blank" rel="noopener noreferrer" style="${ctaStyle}">${escapeHtml(ctaLabel)}</a>` : "";
@@ -10893,6 +10915,7 @@ var ChatAssistantLitMountable = {
10893
10915
  }
10894
10916
  };
10895
10917
  let hadTurnstileToken = null;
10918
+ let botCheckOutcome = "disabled";
10896
10919
  const swapToFallback = (reason) => {
10897
10920
  if (isUnmounted || hasSucceeded || fallbackRendered) return;
10898
10921
  fallbackRendered = true;
@@ -10900,7 +10923,8 @@ var ChatAssistantLitMountable = {
10900
10923
  const payload = {
10901
10924
  tileId: resolvedTileId,
10902
10925
  reason,
10903
- hadTurnstileToken
10926
+ hadTurnstileToken,
10927
+ botCheckOutcome
10904
10928
  };
10905
10929
  runtime.events.publish("chatbot.fallback_rendered", payload);
10906
10930
  console.warn("[adaptive-chatbot] fallback rendered", payload);
@@ -10946,14 +10970,45 @@ var ChatAssistantLitMountable = {
10946
10970
  });
10947
10971
  container.appendChild(el);
10948
10972
  };
10973
+ let visiblePanel = null;
10974
+ const acquireAbort = new AbortController();
10975
+ const acquireWithEscalation = async () => {
10976
+ const silent = await acquireTurnstileToken({
10977
+ size: "invisible",
10978
+ signal: acquireAbort.signal
10979
+ });
10980
+ if (isUnmounted) return null;
10981
+ if (silent !== null) {
10982
+ botCheckOutcome = "invisible_succeeded";
10983
+ return silent;
10984
+ }
10985
+ visiblePanel = renderVerifyPanel(container);
10986
+ const widgetHost = visiblePanel.querySelector(
10987
+ '[data-adaptive-chatbot-turnstile-host="true"]'
10988
+ );
10989
+ if (!widgetHost) return null;
10990
+ const interactive = await acquireTurnstileToken({
10991
+ size: "flexible",
10992
+ host: widgetHost,
10993
+ signal: acquireAbort.signal
10994
+ });
10995
+ if (isUnmounted) return null;
10996
+ visiblePanel.remove();
10997
+ visiblePanel = null;
10998
+ if (interactive !== null) {
10999
+ botCheckOutcome = "visible_succeeded";
11000
+ return interactive;
11001
+ }
11002
+ botCheckOutcome = "visible_failed";
11003
+ console.warn(
11004
+ "[adaptive-chatbot] turnstile token acquisition failed (visible challenge also rejected) \u2014 connecting without a bot-check token; backend may 403"
11005
+ );
11006
+ return null;
11007
+ };
10949
11008
  if (TURNSTILE_SITEKEY) {
10950
- void acquireTurnstileToken().then((cft) => {
11009
+ void acquireWithEscalation().then((cft) => {
11010
+ if (isUnmounted) return;
10951
11011
  hadTurnstileToken = cft !== null;
10952
- if (!hadTurnstileToken) {
10953
- console.warn(
10954
- "[adaptive-chatbot] turnstile token acquisition failed \u2014 connecting without a bot-check token; backend may 403"
10955
- );
10956
- }
10957
11012
  setupTransport(cft);
10958
11013
  });
10959
11014
  } else {
@@ -10962,10 +11017,15 @@ var ChatAssistantLitMountable = {
10962
11017
  return () => {
10963
11018
  isUnmounted = true;
10964
11019
  clearTimers();
11020
+ acquireAbort.abort();
10965
11021
  if (unsubscribe) {
10966
11022
  unsubscribe();
10967
11023
  unsubscribe = null;
10968
11024
  }
11025
+ if (visiblePanel) {
11026
+ visiblePanel.remove();
11027
+ visiblePanel = null;
11028
+ }
10969
11029
  if (!fallbackRendered) {
10970
11030
  transport?.disconnect();
10971
11031
  el.remove();
@@ -10973,6 +11033,33 @@ var ChatAssistantLitMountable = {
10973
11033
  };
10974
11034
  }
10975
11035
  };
11036
+ function renderVerifyPanel(container) {
11037
+ const panel = document.createElement("div");
11038
+ panel.setAttribute("data-adaptive-chatbot-turnstile-visible", "true");
11039
+ panel.style.cssText = [
11040
+ "display:flex",
11041
+ "flex-direction:column",
11042
+ "align-items:center",
11043
+ "justify-content:center",
11044
+ "gap:14px",
11045
+ "padding:20px",
11046
+ "height:100%",
11047
+ "width:100%",
11048
+ "background:var(--sc-content-background,'transparent')",
11049
+ "color:var(--sc-content-text-color,'#1a1a1a')",
11050
+ "font-family:var(--sc-font-family,'system-ui')",
11051
+ "text-align:center"
11052
+ ].join(";");
11053
+ const heading = document.createElement("div");
11054
+ heading.style.cssText = "font-size:14px;font-weight:500;line-height:1.4;";
11055
+ heading.textContent = "Quick check before we start chatting";
11056
+ const widgetHost = document.createElement("div");
11057
+ widgetHost.setAttribute("data-adaptive-chatbot-turnstile-host", "true");
11058
+ panel.appendChild(heading);
11059
+ panel.appendChild(widgetHost);
11060
+ container.appendChild(panel);
11061
+ return panel;
11062
+ }
10976
11063
 
10977
11064
  export {
10978
11065
  acquireTurnstileToken,
@@ -10997,4 +11084,4 @@ fast-json-patch/module/duplex.mjs:
10997
11084
  * MIT license
10998
11085
  *)
10999
11086
  */
11000
- //# sourceMappingURL=chunk-O7RWNUVU.js.map
11087
+ //# sourceMappingURL=chunk-W457NMGD.js.map