@burdenoff/website-sdk 2026.1101.4 → 2026.1102.1

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/README.md CHANGED
@@ -298,8 +298,9 @@ publishes audio, video or data, and it sends the avatar nothing to say.
298
298
  live: {
299
299
  site: 'healthybowl',
300
300
  load: () => import('@burdenoff/website-sdk/avatar-live'),
301
- turnstileSiteKey: TURNSTILE_SITE_KEY,
302
301
  manifestVersion: 'hb-en-1', // the version of `suggestedQuestions`
302
+ // Optional: used only where the server checks for bots (see "The bot check is the server's setting").
303
+ turnstileSiteKey: TURNSTILE_SITE_KEY,
303
304
  },
304
305
  }}
305
306
  />
@@ -339,7 +340,24 @@ publishes audio, video or data, and it sends the avatar nothing to say.
339
340
  - **Needs the panel under an `ExploreChatProvider`.** The live avatar reads the
340
341
  provider's conversation. Without a provider, on a shared (read-only)
341
342
  conversation, in a locale whose strings have not been reviewed yet (English
342
- only, for now), or with an incomplete `live`, the Avatar view stays as it was.
343
+ only, for now), or with an incomplete `live` (no `site`, `load` or
344
+ `manifestVersion`, or a `turnstileSiteKey` or `allowTestSiteKey` of the wrong
345
+ shape), the Avatar view stays as it was.
346
+ - **The bot check is the server's setting (web API contract v1.2, additive).**
347
+ The availability answer says whether the server checks for bots when a
348
+ session is created: `"botCheck": {"mode": "off"}` or
349
+ `{"mode": "turnstile"}`. With `off`, Start loads the room module and creates
350
+ the session, and nothing else: no Cloudflare script, no widget, no WebCrypto
351
+ needed and no `x-web-avatar-bot-token` header; the server's own guards and
352
+ caps decide. With `turnstile`, Start runs the Cloudflare Turnstile check with
353
+ the page's own `turnstileSiteKey`, as before; without a key the page may use
354
+ (none, or one of Cloudflare's test keys where `allowTestSiteKey` is `false`)
355
+ the Avatar view stays as it was. A server older than v1.2 sends no `botCheck`
356
+ and always checks, so it is read as `turnstile`. Any other `botCheck` (an
357
+ unknown mode, another field, another shape) leaves the view as it was. So
358
+ turning the bot check on or off is a server config change: a site needs no
359
+ new build for it, as long as its build carries a site key for `turnstile` and
360
+ its CSP allows Cloudflare (below).
343
361
  - **One answer: the first, to a suggested question.** The avatar service
344
362
  accepts a conversation only when it holds exactly two messages: the first user
345
363
  message (a suggested question) and the first assistant answer, completed. The
@@ -356,8 +374,8 @@ publishes audio, video or data, and it sends the avatar nothing to say.
356
374
  the conversation has its first answer, a consent card explains what happens
357
375
  and links to `/privacy#live-avatar`; with no answer yet, the view asks the
358
376
  visitor to choose a suggested question in Chat first. Start runs a
359
- Cloudflare Turnstile check (action `avatar_start`), creates the session and
360
- joins its room: the video, the captions, an "AI avatar · not a real person"
377
+ Cloudflare Turnstile check (action `avatar_start`) where the server checks
378
+ for bots, creates the session and joins its room: the video, the captions, an "AI avatar · not a real person"
361
379
  badge, a countdown, Stop and Continue in Chat. Where a browser holds sound
362
380
  back (Safari, iOS), a "Tap to turn on sound" button appears. A refusal (busy,
363
381
  limits, region, language, paused, a question the avatar cannot read) is one
@@ -379,6 +397,14 @@ publishes audio, video or data, and it sends the avatar nothing to say.
379
397
  digest for all of them (the card's title, intro, processing, storage and
380
398
  privacy-link lines, each ending with a line feed: 760 bytes, SHA-256
381
399
  `2e02b051ceb1163c14bc58fa63a954a12c0d1f2695029a6d4805922d74c793f7`).
400
+ `web-consent-2026-11-03` is the same card without "When you start,
401
+ Cloudflare checks for bots.", for a server whose bot check is `off` (716
402
+ bytes, SHA-256
403
+ `bb7516df3cf3a910c6ec1623391f859e6c48e7e7d305d4fb333f18d4115da203`).
404
+ Each version says which bot check its words describe, and the card is shown
405
+ only where the server's `botCheck` is the same: a server that runs no bot
406
+ check must name `web-consent-2026-11-03`, and one that checks must name a
407
+ version that says so, or the view stays as it was.
382
408
  `web-consent-2026-10-31` (which names the product, "a suggested Healthy Bowl
383
409
  question") and `web-consent-2026-11` stay, unchanged, for servers that still
384
410
  name them.
@@ -389,22 +415,24 @@ publishes audio, video or data, and it sends the avatar nothing to say.
389
415
  apps-first hand-over, so it does not say which.
390
416
  - **Requests.** All three go to the gateway without credentials. The create
391
417
  request carries `content-type: application/json`, `idempotency-key` (a UUID
392
- v4) and `x-web-avatar-bot-token` (the Turnstile token, which the gateway
393
- verifies and does not forward); the availability request and the end request
394
- carry no header of the SDK's own. The conversation token is sent in the create
418
+ v4) and, only where the server checks for bots, `x-web-avatar-bot-token` (the
419
+ Turnstile token, which the gateway verifies and does not forward); the
420
+ availability request and the end request carry no header of the SDK's own. The conversation token is sent in the create
395
421
  body only. Neither it, nor the session's end secret, nor the room token, nor
396
422
  the bot check's token is ever written into the page, the console or storage by
397
423
  the live avatar (the chat keeps its own copy of the conversation in
398
424
  `localStorage`, as it always has).
399
- - **Bot-check binding (web API contract v1.1, additive).** The Turnstile widget
425
+ - **Bot-check binding (web API contract v1.1, additive; `turnstile` mode
426
+ only).** The Turnstile widget
400
427
  is rendered with `cData` set to
401
428
  `base64url_nopad(SHA-256(UTF-8("boff-web-avatar-cdata/v1\n" + site + "\n" + locale + "\n" + conversationToken + "\n" + messageId)))`,
402
429
  made with WebCrypto from the exact values the create body then carries, before
403
430
  the widget is rendered, so the gateway can check that the token was minted for
404
431
  this request. The action stays `avatar_start`, and nothing else in v1
405
- changes. A browser without WebCrypto (a page that is not a secure context)
406
- cannot start the live avatar: Start loads neither the room module nor the bot
407
- check's script there, and shows the view as it was. Pinned vector: site
432
+ changes. Where the server checks for bots, a browser without WebCrypto (a
433
+ page that is not a secure context) cannot start the live avatar: Start loads
434
+ neither the room module nor the bot check's script there, and shows the view
435
+ as it was. Pinned vector: site
408
436
  `healthybowl`, locale `en`, conversation token
409
437
  `ct_vector_0123456789abcdefghijklmnopqrstuvwx`, message id
410
438
  `11111111-2222-4333-8444-555555555555` give
@@ -418,7 +446,9 @@ publishes audio, video or data, and it sends the avatar nothing to say.
418
446
  preflight.
419
447
  - **Your page's headers.** A site with a Content Security Policy must allow:
420
448
  - `script-src` and `frame-src`: `https://challenges.cloudflare.com` (the
421
- Turnstile bot check);
449
+ Turnstile bot check). Only needed where the server's bot check is
450
+ `turnstile`; keep it anyway, so that turning the bot check on later is a
451
+ server change and not a new release of the site;
422
452
  - `connect-src`, the gateway origin (the three requests);
423
453
  - `connect-src`, the media room: `wss://<project>.livekit.cloud`. For a
424
454
  LiveKit Cloud room the media library also asks
@@ -587,14 +617,23 @@ A site adds these lines.
587
617
  usual place whenever the banner is not on screen.
588
618
 
589
619
  4. **The build env var** `VITE_AVATAR_TURNSTILE_SITE_KEY`: the site's
590
- Cloudflare Turnstile site key. `websiteAvatarLive` returns `undefined` (the
591
- still picture and "Live avatar coming soon", no live avatar) when it is
592
- unset or empty; on alpha (`VITE_DEPLOY_ENV=alpha`) it accepts Cloudflare's
593
- always-pass test key `1x00000000000000000000AA` (the alpha gateway runs
594
- Turnstile in dummy mode) or a real key; everywhere else, production
595
- included, only a real-looking key (`0x…`, letters, digits, `_` and `-`, 18
596
- to 64 characters) that is not one of Cloudflare's test keys (`1x…AA`,
597
- `2x…AB`, `1x…BB`, `2x…BB`, `3x…FF`, or anything of their shape). Install
620
+ Cloudflare Turnstile site key, used only where the server checks for bots
621
+ (its availability's `botCheck` is `turnstile`). The server decides whether
622
+ the live avatar is offered on the site (its reviewed site list, its pause,
623
+ its caps) and whether it checks for bots, so the key is no longer the
624
+ opt-in: when it is unset or empty, `websiteAvatarLive` returns the live
625
+ config without a key, which runs where the server's bot check is `off` and
626
+ stays the still picture where it is `turnstile`. On alpha
627
+ (`VITE_DEPLOY_ENV=alpha`) it accepts Cloudflare's always-pass test key
628
+ `1x00000000000000000000AA` (the alpha gateway runs Turnstile in dummy mode)
629
+ or a real key; everywhere else, production included, only a real-looking key
630
+ (`0x…`, letters, digits, `_` and `-`, 18 to 64 characters) that is not one of
631
+ Cloudflare's test keys (`1x…AA`, `2x…AB`, `1x…BB`, `2x…BB`, `3x…FF`, or
632
+ anything of their shape). A key that is set but refused returns `undefined`
633
+ (the still picture and "Live avatar coming soon"), never a live avatar
634
+ without it. It also sets `allowTestSiteKey` (true on alpha only), so the
635
+ view itself refuses a test key on any other build. Pass `enabled: false` to
636
+ keep the still picture on a build whatever the server offers. Install
598
637
  `livekit-client` on the site (see [Live avatar](#live-avatar-opt-in-receive-only)),
599
638
  and declare the variable in `vite-env.d.ts`.
600
639
  `websiteAvatarLive` trusts `VITE_DEPLOY_ENV`, so a production build must
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-BGsGwbcq.mjs';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-D-hO90-A.mjs';
4
4
  import '../types-CndbmBK-.mjs';
5
5
  import '../use-explore-chat-zRuoAin6.mjs';
6
6
  import '../explore-types-Bb2Ih3d8.mjs';
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-B171o1Yo.js';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-C1ILEcY8.js';
4
4
  import '../types-CndbmBK-.js';
5
5
  import '../use-explore-chat-DunjG-3p.js';
6
6
  import '../explore-types-Bb2Ih3d8.js';
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk23H4JUL4_js = require('../chunk-23H4JUL4.js');
3
+ var chunkOIQHIGGU_js = require('../chunk-OIQHIGGU.js');
4
4
  var chunkO4GS3XPZ_js = require('../chunk-O4GS3XPZ.js');
5
5
  var chunkC6OJIHNO_js = require('../chunk-C6OJIHNO.js');
6
6
  require('../chunk-ZSTFX7FV.js');
@@ -35,6 +35,20 @@ var AVAILABILITY_REASONS = /* @__PURE__ */ new Map([
35
35
  ["busy", "busy"],
36
36
  ["limits", "limits"]
37
37
  ]);
38
+ var BOT_CHECK_MODES = /* @__PURE__ */ new Set([
39
+ "off",
40
+ "turnstile"
41
+ ]);
42
+ function readBotCheck(value) {
43
+ if (value === void 0) return "turnstile";
44
+ if (!isRecord(value)) return null;
45
+ const keys = Object.keys(value);
46
+ const { mode } = value;
47
+ if (keys.length !== 1 || keys[0] !== "mode" || typeof mode !== "string" || !BOT_CHECK_MODES.has(mode)) {
48
+ return null;
49
+ }
50
+ return mode === "off" ? "off" : "turnstile";
51
+ }
38
52
  function readAvailability(status, body) {
39
53
  if (status !== 200 || !isRecord(body)) return INERT;
40
54
  const { available, reason } = body;
@@ -43,10 +57,14 @@ function readAvailability(status, body) {
43
57
  }
44
58
  if (reason === "ok") {
45
59
  const { consentVersion, maxSessionSeconds } = body;
46
- if (available !== true || typeof consentVersion !== "string" || !chunk23H4JUL4_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1) {
60
+ const botCheck = readBotCheck(body.botCheck);
61
+ if (available !== true || typeof consentVersion !== "string" || !chunkOIQHIGGU_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null) {
47
62
  return INERT;
48
63
  }
49
- return { kind: "available", offer: { consentVersion, maxSessionSeconds } };
64
+ return {
65
+ kind: "available",
66
+ offer: { consentVersion, maxSessionSeconds, botCheck }
67
+ };
50
68
  }
51
69
  const meaning = available ? void 0 : AVAILABILITY_REASONS.get(reason);
52
70
  return meaning === void 0 || meaning === "inert" ? INERT : { kind: "refused", reason: meaning };
@@ -128,7 +146,7 @@ function deadline(timeoutMs, outer) {
128
146
  };
129
147
  }
130
148
  async function requestAvailability(target) {
131
- if (!chunk23H4JUL4_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
149
+ if (!chunkOIQHIGGU_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
132
150
  const query = new URLSearchParams({
133
151
  site: target.site,
134
152
  locale: target.locale
@@ -169,7 +187,7 @@ async function requestCreate(target) {
169
187
  ...target.headers,
170
188
  "Content-Type": "application/json",
171
189
  "Idempotency-Key": target.idempotencyKey,
172
- "x-web-avatar-bot-token": target.botToken
190
+ ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
173
191
  },
174
192
  body: payload,
175
193
  signal: timer.signal
@@ -457,7 +475,8 @@ var CONSENT = /* @__PURE__ */ new Map([
457
475
  intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
458
476
  processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Your browser connects to LiveKit, which sees your IP address. LiveKit fetches our temporary audio file and passes the voice to Protoface, which makes the avatar video streamed through LiveKit.",
459
477
  storage: "Your microphone and camera are not used. We do not record the live session. We temporarily store the generated voice file and keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
460
- privacyLink: "How we handle this"
478
+ privacyLink: "How we handle this",
479
+ botCheck: "turnstile"
461
480
  }
462
481
  }
463
482
  ],
@@ -471,7 +490,8 @@ var CONSENT = /* @__PURE__ */ new Map([
471
490
  intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
472
491
  processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address.",
473
492
  storage: "Your microphone and camera are not used. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
474
- privacyLink: "How we handle this"
493
+ privacyLink: "How we handle this",
494
+ botCheck: "turnstile"
475
495
  }
476
496
  }
477
497
  ],
@@ -486,13 +506,30 @@ var CONSENT = /* @__PURE__ */ new Map([
486
506
  intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
487
507
  processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address.",
488
508
  storage: "Your microphone and camera are not used. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
489
- privacyLink: "How we handle this"
509
+ privacyLink: "How we handle this",
510
+ botCheck: "turnstile"
511
+ }
512
+ }
513
+ ],
514
+ [
515
+ // The words of web-consent-2026-11-02 without "When you start, Cloudflare checks for bots.", for a server that runs
516
+ // no bot check (its availability's botCheck is `off`). Product-neutral too: one notice and one digest for every site.
517
+ "web-consent-2026-11-03",
518
+ {
519
+ en: {
520
+ title: "Live AI avatar (beta)",
521
+ introProduct: null,
522
+ intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
523
+ processing: "The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address.",
524
+ storage: "Your microphone and camera are not used. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
525
+ privacyLink: "How we handle this",
526
+ botCheck: "off"
490
527
  }
491
528
  }
492
529
  ]
493
530
  ]);
494
531
  function avatarConsentCopy(version, locale) {
495
- if (!chunk23H4JUL4_js.isAvatarLiveLocaleReviewed(locale)) return null;
532
+ if (!chunkOIQHIGGU_js.isAvatarLiveLocaleReviewed(locale)) return null;
496
533
  return CONSENT.get(version)?.[locale] ?? null;
497
534
  }
498
535
 
@@ -757,7 +794,11 @@ function AvatarLiveView({
757
794
  return;
758
795
  }
759
796
  const copy = avatarConsentCopy(result.offer.consentVersion, locale2);
760
- if (copy === null) {
797
+ if (copy === null || copy.botCheck !== result.offer.botCheck) {
798
+ go(INERT2);
799
+ return;
800
+ }
801
+ if (result.offer.botCheck === "turnstile" && chunkOIQHIGGU_js.turnstileSiteKeyOf(live2) === null) {
761
802
  go(INERT2);
762
803
  return;
763
804
  }
@@ -853,11 +894,12 @@ function AvatarLiveView({
853
894
  if (current.name !== "consent") return;
854
895
  const { live: live2, base: base2, headers: headers2 } = latest.current;
855
896
  if (base2 === null) return;
856
- if (!canBindBotCheck()) {
897
+ const { offer, messageId } = current;
898
+ const siteKey = offer.botCheck === "turnstile" ? chunkOIQHIGGU_js.turnstileSiteKeyOf(live2) : null;
899
+ if (offer.botCheck === "turnstile" && (siteKey === null || !canBindBotCheck())) {
857
900
  go(INERT2);
858
901
  return;
859
902
  }
860
- const { offer, messageId } = current;
861
903
  const conversationToken = conversationRef.current?.getConversationToken() ?? null;
862
904
  const idempotencyKey = newIdempotencyKey();
863
905
  if (conversationToken === null || !isAvatarConversationToken(conversationToken) || idempotencyKey === null) {
@@ -874,31 +916,36 @@ function AvatarLiveView({
874
916
  };
875
917
  const run = { cancelled: null, turnstile: null };
876
918
  startRef.current = run;
877
- go({ name: "bot-check", offer, messageId });
878
919
  const module = loadModule(live2.load);
879
- const turnstile = runTurnstile(
880
- botBoxRef.current,
881
- live2.turnstileSiteKey,
882
- "avatar_start",
883
- avatarBotBinding(body2)
884
- );
885
- run.turnstile = turnstile;
920
+ let turnstile = null;
921
+ if (siteKey === null) {
922
+ go({ name: "creating" });
923
+ } else {
924
+ go({ name: "bot-check", offer, messageId });
925
+ turnstile = runTurnstile(
926
+ botBoxRef.current,
927
+ siteKey,
928
+ "avatar_start",
929
+ avatarBotBinding(body2)
930
+ );
931
+ run.turnstile = turnstile;
932
+ }
886
933
  void (async () => {
887
934
  let loaded;
888
935
  let botToken;
889
936
  try {
890
- [loaded, botToken] = await Promise.all([module, turnstile.token]);
937
+ [loaded, botToken] = await Promise.all([module, turnstile?.token]);
891
938
  } catch {
892
- turnstile.cancel();
939
+ turnstile?.cancel();
893
940
  if (run.cancelled === null) {
894
941
  startRef.current = null;
895
942
  go(INERT2);
896
943
  }
897
944
  return;
898
945
  }
899
- turnstile.cancel();
946
+ turnstile?.cancel();
900
947
  if (run.cancelled !== null) return;
901
- go({ name: "creating" });
948
+ if (turnstile !== null) go({ name: "creating" });
902
949
  const result = await requestCreate({
903
950
  base: base2,
904
951
  headers: headers2,