@kywi-software/js 0.14.0 → 0.15.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/dist/kywi.js CHANGED
@@ -21,14 +21,24 @@ var Kywi = (() => {
21
21
  // src/index.ts
22
22
  var src_exports = {};
23
23
  __export(src_exports, {
24
+ AUDIENCE_PIN_COOKIE: () => AUDIENCE_PIN_COOKIE,
25
+ CONSENT_COOKIE: () => CONSENT_COOKIE,
26
+ COOKIE_CONSENT_CATEGORY: () => COOKIE_CONSENT_CATEGORY,
24
27
  Kywi: () => Kywi,
25
28
  NAV_BREAKPOINT: () => NAV_BREAKPOINT,
29
+ SWITCHER_PIN_PREFIX: () => SWITCHER_PIN_PREFIX,
26
30
  bootAudienceEngine: () => bootAudienceEngine,
27
31
  bootExperiments: () => bootExperiments,
32
+ clearDeniedCookies: () => clearDeniedCookies,
33
+ clearSelfId: () => clearSelfId,
28
34
  enhanceNav: () => enhanceNav,
35
+ getConsentState: () => getConsentState,
36
+ hasConsent: () => hasConsent,
29
37
  initNavMenus: () => initNavMenus,
30
38
  openTransparencyPanel: () => openTransparencyPanel,
31
- renderTransparencyPanel: () => renderTransparencyPanel
39
+ renderTransparencyPanel: () => renderTransparencyPanel,
40
+ setConsent: () => setConsent,
41
+ setSelfId: () => setSelfId
32
42
  });
33
43
 
34
44
  // src/context.ts
@@ -569,6 +579,98 @@ var Kywi = (() => {
569
579
  return runtime;
570
580
  }
571
581
 
582
+ // src/audience/selfid-collector.ts
583
+ var SELFID_KEY = "kywi_selfid";
584
+ function collectSelfId() {
585
+ const raw = readStorage(SELFID_KEY);
586
+ if (!raw) return {};
587
+ try {
588
+ const parsed = JSON.parse(raw);
589
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
590
+ return parsed;
591
+ } catch {
592
+ return {};
593
+ }
594
+ }
595
+ function storeSelfId(responses) {
596
+ try {
597
+ localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
598
+ } catch {
599
+ }
600
+ }
601
+ function clearStoredSelfId() {
602
+ try {
603
+ localStorage.removeItem(SELFID_KEY);
604
+ } catch {
605
+ }
606
+ }
607
+ function readStorage(key) {
608
+ try {
609
+ return localStorage.getItem(key);
610
+ } catch {
611
+ return null;
612
+ }
613
+ }
614
+
615
+ // src/audience/selfid-api.ts
616
+ var DEFAULT_API_BASE = "/api/v1";
617
+ function resolveApiBase(explicit) {
618
+ if (explicit) return explicit;
619
+ const basePath = globalThis.Kywi?.context?.basePath;
620
+ if (!basePath || basePath === "/") return DEFAULT_API_BASE;
621
+ return `${basePath}/api/v1`;
622
+ }
623
+ function resolveCsrfToken(explicit) {
624
+ if (explicit !== void 0) return explicit;
625
+ return globalThis.Kywi?.context?.csrfToken ?? null;
626
+ }
627
+ function reloadPage() {
628
+ try {
629
+ window.location.reload();
630
+ } catch {
631
+ }
632
+ }
633
+ async function setSelfId(fields, options = {}) {
634
+ const merged = { ...collectSelfId() };
635
+ for (const [key, value] of Object.entries(fields)) {
636
+ if (value === "") delete merged[key];
637
+ else merged[key] = value;
638
+ }
639
+ if (Object.keys(merged).length > 0) storeSelfId(merged);
640
+ else clearStoredSelfId();
641
+ const result = await submit(merged, options);
642
+ if (options.reload !== false && result !== null) reloadPage();
643
+ return result ?? { audience: null, persisted: false, fields: merged };
644
+ }
645
+ async function clearSelfId(options = {}) {
646
+ clearStoredSelfId();
647
+ const result = await submit({}, options);
648
+ if (options.reload !== false && result !== null) reloadPage();
649
+ return result ?? { audience: null, persisted: false, fields: {} };
650
+ }
651
+ async function submit(fields, options) {
652
+ const csrfToken = resolveCsrfToken(options.csrfToken);
653
+ try {
654
+ const res = await fetch(`${resolveApiBase(options.apiBase)}/kywi/self-id`, {
655
+ method: "POST",
656
+ headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
657
+ body: JSON.stringify({
658
+ fields,
659
+ ...options.audienceId ? { audienceId: options.audienceId } : {}
660
+ })
661
+ });
662
+ if (!res.ok) return null;
663
+ const body = await res.json().catch(() => null);
664
+ return {
665
+ audience: body?.data?.audience ?? null,
666
+ persisted: body?.data?.persisted === true,
667
+ fields
668
+ };
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+
572
674
  // ../core/dist/audiences/types.js
573
675
  function createEmptySignals() {
574
676
  return {
@@ -576,6 +678,7 @@ var Kywi = (() => {
576
678
  referrer: { raw: null, domain: null, sourceType: "direct" },
577
679
  session: { pageViewCount: 0, totalPageViews: 0, sessionDuration: 0, entryPage: "/", pagesVisited: [] },
578
680
  behavioral: { categoryScores: {}, totalScore: 0 },
681
+ query: {},
579
682
  selfId: {},
580
683
  identity: { visitorId: "", isKnown: false, maLeadId: null },
581
684
  adapters: {},
@@ -636,6 +739,10 @@ var Kywi = (() => {
636
739
  return signals.session.entryPage;
637
740
  if (ref === "session.pagesVisited")
638
741
  return signals.session.pagesVisited.join(",");
742
+ if (ref.startsWith("query.")) {
743
+ const key = ref.slice(6);
744
+ return signals.query[key] ?? null;
745
+ }
639
746
  if (ref.startsWith("selfId.")) {
640
747
  const key = ref.slice(7);
641
748
  return signals.selfId[key] ?? null;
@@ -803,6 +910,121 @@ var Kywi = (() => {
803
910
  return { winningAudienceId: allMatchedIds[0] ?? null, allMatchedIds, traces };
804
911
  }
805
912
 
913
+ // src/audience/query-collector.ts
914
+ function collectQuery() {
915
+ const out = {};
916
+ if (typeof location === "undefined") return out;
917
+ new URLSearchParams(location.search).forEach((value, key) => {
918
+ if (key !== "" && !(key in out)) out[key] = value;
919
+ });
920
+ return out;
921
+ }
922
+ function entryPageFor(currentPath) {
923
+ if (currentPath.includes("?")) return currentPath;
924
+ const search = typeof location === "undefined" ? "" : location.search;
925
+ return search ? `${currentPath}${search}` : currentPath;
926
+ }
927
+
928
+ // src/cookies.ts
929
+ var COOKIE_NAMES = {
930
+ SIGNALS: "kywi_signals",
931
+ VISITOR: "kywi_visitor",
932
+ UTM: "kywi_utm",
933
+ AUDIENCE: "kywi_audience",
934
+ OPTOUT: "kywi_optout",
935
+ KNOWN: "kywi_known",
936
+ PREVIEW_INIT: "kywi_preview_init"
937
+ };
938
+ function readCookies() {
939
+ const map = /* @__PURE__ */ new Map();
940
+ if (typeof document === "undefined") return map;
941
+ for (const pair of document.cookie.split(";")) {
942
+ const trimmed = pair.trim();
943
+ const eqIdx = trimmed.indexOf("=");
944
+ if (eqIdx < 0) continue;
945
+ map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
946
+ }
947
+ return map;
948
+ }
949
+ function setCookie(name, value, maxAge) {
950
+ let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
951
+ if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
952
+ document.cookie = str;
953
+ }
954
+ function deleteCookie(name) {
955
+ document.cookie = `${name}=; Path=/; Max-Age=0`;
956
+ }
957
+
958
+ // src/consent.ts
959
+ var CONSENT_COOKIE = "kywi_consent";
960
+ var AUDIENCE_PIN_COOKIE = "kywi_audience";
961
+ var SWITCHER_PIN_PREFIX = "switcher:";
962
+ var COOKIE_CONSENT_CATEGORY = {
963
+ kywi_consent: "essential",
964
+ kywi_optout: "essential",
965
+ kywi_preview_init: "essential",
966
+ kywi_signals: "personalization",
967
+ kywi_visitor: "personalization",
968
+ kywi_utm: "personalization",
969
+ kywi_audience: "personalization",
970
+ kywi_known: "personalization",
971
+ kywi_entry: "personalization",
972
+ kywi_selfid: "personalization"
973
+ };
974
+ function getConsentState() {
975
+ const cookies = readCookies();
976
+ const raw = cookies.get(CONSENT_COOKIE);
977
+ if (!raw) return null;
978
+ let parsed;
979
+ try {
980
+ parsed = JSON.parse(raw);
981
+ } catch {
982
+ return null;
983
+ }
984
+ if (typeof parsed !== "object" || parsed === null) return null;
985
+ const obj = parsed;
986
+ return {
987
+ essential: true,
988
+ analytics: obj.analytics === true,
989
+ marketing: obj.marketing === true,
990
+ personalization: obj.personalization === true
991
+ };
992
+ }
993
+ function setConsent(prefs) {
994
+ const state = {
995
+ essential: true,
996
+ analytics: prefs.analytics ?? false,
997
+ marketing: prefs.marketing ?? false,
998
+ personalization: prefs.personalization ?? false
999
+ };
1000
+ setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
1001
+ clearDeniedCookies(state);
1002
+ }
1003
+ function clearDeniedCookies(state) {
1004
+ if (typeof document === "undefined") return [];
1005
+ const cookies = readCookies();
1006
+ const deleted = [];
1007
+ for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
1008
+ if (category === "essential") continue;
1009
+ if (state && state[category] === true) continue;
1010
+ if (isDeliberatePreference(name, cookies.get(name))) continue;
1011
+ deleteCookie(name);
1012
+ deleted.push(name);
1013
+ }
1014
+ return deleted;
1015
+ }
1016
+ function isDeliberatePreference(name, value) {
1017
+ if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
1018
+ return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
1019
+ }
1020
+ function hasConsent(category, options = {}) {
1021
+ if (options.requireConsent === false) return true;
1022
+ if (category === "essential") return true;
1023
+ const state = getConsentState();
1024
+ if (!state) return false;
1025
+ return state[category] === true;
1026
+ }
1027
+
806
1028
  // src/audience/behavioral-collector.ts
807
1029
  var SESSION_KEY = "kywi_session";
808
1030
  var TOTAL_VIEWS_KEY = "kywi_total_views";
@@ -835,10 +1057,11 @@ var Kywi = (() => {
835
1057
  }
836
1058
  return {};
837
1059
  }
838
- function collectBehavioral(currentPath, category) {
1060
+ function collectBehavioral(currentPath, category, gate = {}) {
839
1061
  const session = getSession();
840
1062
  session.pageViewCount += 1;
841
- if (!session.entryPage) session.entryPage = currentPath;
1063
+ const entryPage = session.entryPage || entryPageFor(currentPath);
1064
+ if (!session.entryPage && hasConsent("personalization", gate)) session.entryPage = entryPage;
842
1065
  session.pagesVisited.push(currentPath);
843
1066
  sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
844
1067
  const totalViews = (Number(localStorage.getItem(TOTAL_VIEWS_KEY)) || 0) + 1;
@@ -850,26 +1073,11 @@ var Kywi = (() => {
850
1073
  localStorage.setItem(CATEGORY_SCORES_KEY, JSON.stringify(categoryScores));
851
1074
  }
852
1075
  return {
853
- session: { pageViewCount: session.pageViewCount, totalPageViews: totalViews, sessionDuration: Math.floor((Date.now() - sessionStart) / 1e3), entryPage: session.entryPage, pagesVisited: session.pagesVisited },
1076
+ session: { pageViewCount: session.pageViewCount, totalPageViews: totalViews, sessionDuration: Math.floor((Date.now() - sessionStart) / 1e3), entryPage, pagesVisited: session.pagesVisited },
854
1077
  behavioral: { categoryScores, totalScore: 0 }
855
1078
  };
856
1079
  }
857
1080
 
858
- // src/audience/selfid-collector.ts
859
- var SELFID_KEY = "kywi_selfid";
860
- function collectSelfId() {
861
- const raw = localStorage.getItem(SELFID_KEY);
862
- if (!raw) return {};
863
- try {
864
- return JSON.parse(raw);
865
- } catch {
866
- return {};
867
- }
868
- }
869
- function storeSelfId(responses) {
870
- localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
871
- }
872
-
873
1081
  // src/audience/evaluate.ts
874
1082
  function merge(base, partial) {
875
1083
  return {
@@ -877,17 +1085,18 @@ var Kywi = (() => {
877
1085
  referrer: partial.referrer ?? base.referrer,
878
1086
  session: partial.session ?? base.session,
879
1087
  behavioral: partial.behavioral ?? base.behavioral,
1088
+ query: { ...base.query, ...partial.query },
880
1089
  selfId: { ...base.selfId, ...partial.selfId },
881
1090
  identity: partial.identity ?? base.identity,
882
1091
  adapters: { ...base.adapters, ...partial.adapters },
883
1092
  meta: { ...base.meta, ...partial.meta }
884
1093
  };
885
1094
  }
886
- function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory) {
1095
+ function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory, gate = {}) {
887
1096
  let signals = merge(createEmptySignals(), serverSignals);
888
- const client = collectBehavioral(currentPath, pageCategory);
1097
+ const client = collectBehavioral(currentPath, pageCategory, gate);
889
1098
  const selfId = collectSelfId();
890
- signals = merge(signals, { session: client.session, behavioral: client.behavioral, selfId, meta: { ...signals.meta, resolvedAt: "client" } });
1099
+ signals = merge(signals, { session: client.session, behavioral: client.behavioral, query: collectQuery(), selfId, meta: { ...signals.meta, resolvedAt: "client" } });
891
1100
  const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
892
1101
  const isOptedOut2 = signals.meta.isOptedOut;
893
1102
  return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
@@ -904,7 +1113,9 @@ var Kywi = (() => {
904
1113
  el.removeAttribute("aria-busy");
905
1114
  const skeleton = el.querySelector(".kywi-variant-skeleton");
906
1115
  if (skeleton) skeleton.remove();
907
- const target = winningAudienceId ?? "default";
1116
+ const requested = winningAudienceId ?? "default";
1117
+ const hasMatch = arms.some((child) => child.getAttribute("data-kywi-variant") === requested);
1118
+ const target = hasMatch ? requested : "default";
908
1119
  el.setAttribute("data-variant", target);
909
1120
  for (const child of arms) {
910
1121
  const h = child;
@@ -913,36 +1124,6 @@ var Kywi = (() => {
913
1124
  }
914
1125
  }
915
1126
 
916
- // src/cookies.ts
917
- var COOKIE_NAMES = {
918
- SIGNALS: "kywi_signals",
919
- VISITOR: "kywi_visitor",
920
- UTM: "kywi_utm",
921
- AUDIENCE: "kywi_audience",
922
- OPTOUT: "kywi_optout",
923
- KNOWN: "kywi_known",
924
- PREVIEW_INIT: "kywi_preview_init"
925
- };
926
- function readCookies() {
927
- const map = /* @__PURE__ */ new Map();
928
- if (typeof document === "undefined") return map;
929
- for (const pair of document.cookie.split(";")) {
930
- const trimmed = pair.trim();
931
- const eqIdx = trimmed.indexOf("=");
932
- if (eqIdx < 0) continue;
933
- map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
934
- }
935
- return map;
936
- }
937
- function setCookie(name, value, maxAge) {
938
- let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
939
- if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
940
- document.cookie = str;
941
- }
942
- function deleteCookie(name) {
943
- document.cookie = `${name}=; Path=/; Max-Age=0`;
944
- }
945
-
946
1127
  // src/audience/transparency.ts
947
1128
  var COLLAPSED_KEY = "kywi_transparency_collapsed";
948
1129
  var LEGACY_DISMISSED_KEY = "kywi_transparency_dismissed";
@@ -950,10 +1131,10 @@ var Kywi = (() => {
950
1131
  var PANEL_ID = "kywi-transparency-panel";
951
1132
  var TITLE_ID = "kywi-transparency-title";
952
1133
  var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
953
- var DEFAULT_API_BASE = "/api/v1";
1134
+ var DEFAULT_API_BASE2 = "/api/v1";
954
1135
  var lastState = null;
955
1136
  var forcedOpen = false;
956
- function readStorage(key) {
1137
+ function readStorage2(key) {
957
1138
  try {
958
1139
  return localStorage.getItem(key);
959
1140
  } catch {
@@ -973,12 +1154,12 @@ var Kywi = (() => {
973
1154
  }
974
1155
  }
975
1156
  function isCollapsed() {
976
- if (readStorage(LEGACY_DISMISSED_KEY) === "1") {
1157
+ if (readStorage2(LEGACY_DISMISSED_KEY) === "1") {
977
1158
  removeStorage(LEGACY_DISMISSED_KEY);
978
1159
  writeStorage(COLLAPSED_KEY, "1");
979
1160
  return true;
980
1161
  }
981
- return readStorage(COLLAPSED_KEY) !== "0";
1162
+ return readStorage2(COLLAPSED_KEY) !== "0";
982
1163
  }
983
1164
  function setCollapsed(collapsed) {
984
1165
  writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
@@ -997,16 +1178,28 @@ var Kywi = (() => {
997
1178
  return readCookies().has(COOKIE_NAMES.AUDIENCE);
998
1179
  }
999
1180
  async function pinAudience(audienceId, opts = {}) {
1000
- const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
1181
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE2;
1001
1182
  try {
1002
- await fetch(`${apiBase}/kywi/audience`, {
1183
+ const res = await fetch(`${apiBase}/kywi/audience`, {
1003
1184
  method: "POST",
1004
1185
  headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
1005
- body: JSON.stringify({ audienceId })
1186
+ body: JSON.stringify({ audienceId, source: "switcher" })
1006
1187
  });
1188
+ if (!res.ok) return false;
1189
+ const body = await res.json().catch(() => null);
1190
+ return body?.data?.persisted !== false;
1007
1191
  } catch {
1192
+ return false;
1008
1193
  }
1009
1194
  }
1195
+ function showSwitcherError(group) {
1196
+ group.parentElement?.querySelector(".kywi-transparency-switcher-error")?.remove();
1197
+ const note = document.createElement("p");
1198
+ note.className = "kywi-transparency-switcher-error";
1199
+ note.setAttribute("role", "status");
1200
+ note.textContent = "Couldn't save that choice. It won't be remembered on the next page.";
1201
+ group.parentElement?.appendChild(note);
1202
+ }
1010
1203
  function reload() {
1011
1204
  try {
1012
1205
  window.location.reload();
@@ -1111,7 +1304,12 @@ var Kywi = (() => {
1111
1304
  const isCurrent = option.id === state.audienceId;
1112
1305
  const optionBtn = button("kywi-transparency-option", option.name, () => {
1113
1306
  if (isCurrent) return;
1114
- void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(reload);
1307
+ void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(
1308
+ (persisted) => {
1309
+ if (persisted) reload();
1310
+ else showSwitcherError(group);
1311
+ }
1312
+ );
1115
1313
  });
1116
1314
  optionBtn.dataset.kywiAudienceId = option.id;
1117
1315
  optionBtn.setAttribute("aria-pressed", String(isCurrent));
@@ -1564,7 +1762,7 @@ var Kywi = (() => {
1564
1762
  }
1565
1763
 
1566
1764
  // src/audience/index.ts
1567
- function resolveApiBase(explicit) {
1765
+ function resolveApiBase2(explicit) {
1568
1766
  if (explicit) return explicit;
1569
1767
  const basePath = Kywi.context?.basePath;
1570
1768
  if (!basePath || basePath === "/") return "/api/v1";
@@ -1572,7 +1770,7 @@ var Kywi = (() => {
1572
1770
  }
1573
1771
  async function bootAudienceEngine(config) {
1574
1772
  const { audiences, currentPath, pageCategory, serverSignals } = config;
1575
- const apiBase = resolveApiBase(config.apiBase);
1773
+ const apiBase = resolveApiBase2(config.apiBase);
1576
1774
  checkPreviewInit();
1577
1775
  const serverMeta = readServerMeta();
1578
1776
  const previewId = getPreviewAudienceId();
@@ -1587,17 +1785,21 @@ var Kywi = (() => {
1587
1785
  return;
1588
1786
  }
1589
1787
  const cookies = readCookies();
1788
+ const consentGate = config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
1789
+ const mayPersonalize = hasConsent("personalization", consentGate);
1790
+ const storedVisitorId = mayPersonalize ? cookies.get(COOKIE_NAMES.VISITOR) : void 0;
1590
1791
  let merged = {
1591
1792
  ...serverSignals,
1592
- identity: { visitorId: serverMeta.visitorId || cookies.get(COOKIE_NAMES.VISITOR) || "", isKnown: cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1793
+ identity: { visitorId: serverMeta.visitorId || storedVisitorId || "", isKnown: mayPersonalize && cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1593
1794
  meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
1594
1795
  };
1595
1796
  if (config.adapters && config.adapters.length > 0) {
1596
1797
  const adapterResults = await runClientAdapters(config.adapters, merged, apiBase);
1597
1798
  merged.adapters = { ...merged.adapters, ...adapterResults };
1598
1799
  }
1599
- const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1600
- const pinnedId = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1800
+ const result = evaluateClientSide(audiences, merged, currentPath, pageCategory, consentGate);
1801
+ const rawPin = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1802
+ const pinnedId = rawPin?.startsWith("switcher:") ? rawPin.slice("switcher:".length) || null : rawPin;
1601
1803
  const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
1602
1804
  const effectiveAudienceId = isPinned ? pinnedId : result.winningAudienceId;
1603
1805
  patchVariantContainers(effectiveAudienceId);
@@ -1614,11 +1816,14 @@ var Kywi = (() => {
1614
1816
  renderPersonalizationBadge({ audienceName: nameOf(effectiveAudienceId) });
1615
1817
  populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1616
1818
  fireKywiReady();
1617
- if (!isPinned && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1819
+ if (!isPinned && mayPersonalize && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1618
1820
  fetch(`${apiBase}/kywi/audience`, {
1619
1821
  method: "POST",
1620
1822
  headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
1621
- body: JSON.stringify({ audienceId: result.winningAudienceId })
1823
+ // Explicitly the AUTOMATIC write, so the server applies the
1824
+ // personalization gate to it (kywi-cms#91). The transparency panel's
1825
+ // hand-operated switcher sends source:'switcher' and is exempt.
1826
+ body: JSON.stringify({ audienceId: result.winningAudienceId, source: "boot" })
1622
1827
  }).catch(() => {
1623
1828
  });
1624
1829
  }
@@ -1696,6 +1901,19 @@ var Kywi = (() => {
1696
1901
  }
1697
1902
  }
1698
1903
  },
1904
+ /**
1905
+ * Record who the visitor says they are, and resolve their audience
1906
+ * (kywi-cms#200). Writes the browser-side answers, posts them to the
1907
+ * consent-gated runtime endpoint, and reloads so the SERVER can re-render
1908
+ * with the resolved audience pinned — which is the only way to reach a
1909
+ * variant arm, since every losing arm is pruned server-side (#167).
1910
+ *
1911
+ * On the global because a site's own "What brings you here?" control needs
1912
+ * it: the built-in widget's write path is module-private.
1913
+ */
1914
+ setSelfId,
1915
+ /** Forget the visitor's self-identification and un-pin their audience. */
1916
+ clearSelfId,
1699
1917
  getEntity: null,
1700
1918
  getFeed: null,
1701
1919
  renderFeed: null,