@kywi-software/js 0.15.0 → 0.15.2

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.esm.js CHANGED
@@ -536,6 +536,98 @@ function bootExperiments(config = {}) {
536
536
  return runtime;
537
537
  }
538
538
 
539
+ // src/audience/selfid-collector.ts
540
+ var SELFID_KEY = "kywi_selfid";
541
+ function collectSelfId() {
542
+ const raw = readStorage(SELFID_KEY);
543
+ if (!raw) return {};
544
+ try {
545
+ const parsed = JSON.parse(raw);
546
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
547
+ return parsed;
548
+ } catch {
549
+ return {};
550
+ }
551
+ }
552
+ function storeSelfId(responses) {
553
+ try {
554
+ localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
555
+ } catch {
556
+ }
557
+ }
558
+ function clearStoredSelfId() {
559
+ try {
560
+ localStorage.removeItem(SELFID_KEY);
561
+ } catch {
562
+ }
563
+ }
564
+ function readStorage(key) {
565
+ try {
566
+ return localStorage.getItem(key);
567
+ } catch {
568
+ return null;
569
+ }
570
+ }
571
+
572
+ // src/audience/selfid-api.ts
573
+ var DEFAULT_API_BASE = "/api/v1";
574
+ function resolveApiBase(explicit) {
575
+ if (explicit) return explicit;
576
+ const basePath = globalThis.Kywi?.context?.basePath;
577
+ if (!basePath || basePath === "/") return DEFAULT_API_BASE;
578
+ return `${basePath}/api/v1`;
579
+ }
580
+ function resolveCsrfToken(explicit) {
581
+ if (explicit !== void 0) return explicit;
582
+ return globalThis.Kywi?.context?.csrfToken ?? null;
583
+ }
584
+ function reloadPage() {
585
+ try {
586
+ window.location.reload();
587
+ } catch {
588
+ }
589
+ }
590
+ async function setSelfId(fields, options = {}) {
591
+ const merged = { ...collectSelfId() };
592
+ for (const [key, value] of Object.entries(fields)) {
593
+ if (value === "") delete merged[key];
594
+ else merged[key] = value;
595
+ }
596
+ if (Object.keys(merged).length > 0) storeSelfId(merged);
597
+ else clearStoredSelfId();
598
+ const result = await submit(merged, options);
599
+ if (options.reload !== false && result !== null) reloadPage();
600
+ return result ?? { audience: null, persisted: false, fields: merged };
601
+ }
602
+ async function clearSelfId(options = {}) {
603
+ clearStoredSelfId();
604
+ const result = await submit({}, options);
605
+ if (options.reload !== false && result !== null) reloadPage();
606
+ return result ?? { audience: null, persisted: false, fields: {} };
607
+ }
608
+ async function submit(fields, options) {
609
+ const csrfToken = resolveCsrfToken(options.csrfToken);
610
+ try {
611
+ const res = await fetch(`${resolveApiBase(options.apiBase)}/kywi/self-id`, {
612
+ method: "POST",
613
+ headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
614
+ body: JSON.stringify({
615
+ fields,
616
+ ...options.audienceId ? { audienceId: options.audienceId } : {}
617
+ })
618
+ });
619
+ if (!res.ok) return null;
620
+ const body = await res.json().catch(() => null);
621
+ return {
622
+ audience: body?.data?.audience ?? null,
623
+ persisted: body?.data?.persisted === true,
624
+ fields
625
+ };
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
539
631
  // ../core/dist/audiences/types.js
540
632
  function createEmptySignals() {
541
633
  return {
@@ -543,6 +635,7 @@ function createEmptySignals() {
543
635
  referrer: { raw: null, domain: null, sourceType: "direct" },
544
636
  session: { pageViewCount: 0, totalPageViews: 0, sessionDuration: 0, entryPage: "/", pagesVisited: [] },
545
637
  behavioral: { categoryScores: {}, totalScore: 0 },
638
+ query: {},
546
639
  selfId: {},
547
640
  identity: { visitorId: "", isKnown: false, maLeadId: null },
548
641
  adapters: {},
@@ -603,6 +696,10 @@ function resolveSignalValue(signals, ref) {
603
696
  return signals.session.entryPage;
604
697
  if (ref === "session.pagesVisited")
605
698
  return signals.session.pagesVisited.join(",");
699
+ if (ref.startsWith("query.")) {
700
+ const key = ref.slice(6);
701
+ return signals.query[key] ?? null;
702
+ }
606
703
  if (ref.startsWith("selfId.")) {
607
704
  const key = ref.slice(7);
608
705
  return signals.selfId[key] ?? null;
@@ -770,6 +867,121 @@ function resolveAudiences(audiences, signals) {
770
867
  return { winningAudienceId: allMatchedIds[0] ?? null, allMatchedIds, traces };
771
868
  }
772
869
 
870
+ // src/audience/query-collector.ts
871
+ function collectQuery() {
872
+ const out = {};
873
+ if (typeof location === "undefined") return out;
874
+ new URLSearchParams(location.search).forEach((value, key) => {
875
+ if (key !== "" && !(key in out)) out[key] = value;
876
+ });
877
+ return out;
878
+ }
879
+ function entryPageFor(currentPath) {
880
+ if (currentPath.includes("?")) return currentPath;
881
+ const search = typeof location === "undefined" ? "" : location.search;
882
+ return search ? `${currentPath}${search}` : currentPath;
883
+ }
884
+
885
+ // src/cookies.ts
886
+ var COOKIE_NAMES = {
887
+ SIGNALS: "kywi_signals",
888
+ VISITOR: "kywi_visitor",
889
+ UTM: "kywi_utm",
890
+ AUDIENCE: "kywi_audience",
891
+ OPTOUT: "kywi_optout",
892
+ KNOWN: "kywi_known",
893
+ PREVIEW_INIT: "kywi_preview_init"
894
+ };
895
+ function readCookies() {
896
+ const map = /* @__PURE__ */ new Map();
897
+ if (typeof document === "undefined") return map;
898
+ for (const pair of document.cookie.split(";")) {
899
+ const trimmed = pair.trim();
900
+ const eqIdx = trimmed.indexOf("=");
901
+ if (eqIdx < 0) continue;
902
+ map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
903
+ }
904
+ return map;
905
+ }
906
+ function setCookie(name, value, maxAge) {
907
+ let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
908
+ if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
909
+ document.cookie = str;
910
+ }
911
+ function deleteCookie(name) {
912
+ document.cookie = `${name}=; Path=/; Max-Age=0`;
913
+ }
914
+
915
+ // src/consent.ts
916
+ var CONSENT_COOKIE = "kywi_consent";
917
+ var AUDIENCE_PIN_COOKIE = "kywi_audience";
918
+ var SWITCHER_PIN_PREFIX = "switcher:";
919
+ var COOKIE_CONSENT_CATEGORY = {
920
+ kywi_consent: "essential",
921
+ kywi_optout: "essential",
922
+ kywi_preview_init: "essential",
923
+ kywi_signals: "personalization",
924
+ kywi_visitor: "personalization",
925
+ kywi_utm: "personalization",
926
+ kywi_audience: "personalization",
927
+ kywi_known: "personalization",
928
+ kywi_entry: "personalization",
929
+ kywi_selfid: "personalization"
930
+ };
931
+ function getConsentState() {
932
+ const cookies = readCookies();
933
+ const raw = cookies.get(CONSENT_COOKIE);
934
+ if (!raw) return null;
935
+ let parsed;
936
+ try {
937
+ parsed = JSON.parse(raw);
938
+ } catch {
939
+ return null;
940
+ }
941
+ if (typeof parsed !== "object" || parsed === null) return null;
942
+ const obj = parsed;
943
+ return {
944
+ essential: true,
945
+ analytics: obj.analytics === true,
946
+ marketing: obj.marketing === true,
947
+ personalization: obj.personalization === true
948
+ };
949
+ }
950
+ function setConsent(prefs) {
951
+ const state = {
952
+ essential: true,
953
+ analytics: prefs.analytics ?? false,
954
+ marketing: prefs.marketing ?? false,
955
+ personalization: prefs.personalization ?? false
956
+ };
957
+ setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
958
+ clearDeniedCookies(state);
959
+ }
960
+ function clearDeniedCookies(state) {
961
+ if (typeof document === "undefined") return [];
962
+ const cookies = readCookies();
963
+ const deleted = [];
964
+ for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
965
+ if (category === "essential") continue;
966
+ if (state && state[category] === true) continue;
967
+ if (isDeliberatePreference(name, cookies.get(name))) continue;
968
+ deleteCookie(name);
969
+ deleted.push(name);
970
+ }
971
+ return deleted;
972
+ }
973
+ function isDeliberatePreference(name, value) {
974
+ if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
975
+ return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
976
+ }
977
+ function hasConsent(category, options = {}) {
978
+ if (options.requireConsent === false) return true;
979
+ if (category === "essential") return true;
980
+ const state = getConsentState();
981
+ if (!state) return false;
982
+ return state[category] === true;
983
+ }
984
+
773
985
  // src/audience/behavioral-collector.ts
774
986
  var SESSION_KEY = "kywi_session";
775
987
  var TOTAL_VIEWS_KEY = "kywi_total_views";
@@ -802,10 +1014,11 @@ function getCategoryScores() {
802
1014
  }
803
1015
  return {};
804
1016
  }
805
- function collectBehavioral(currentPath, category) {
1017
+ function collectBehavioral(currentPath, category, gate = {}) {
806
1018
  const session = getSession();
807
1019
  session.pageViewCount += 1;
808
- if (!session.entryPage) session.entryPage = currentPath;
1020
+ const entryPage = session.entryPage || entryPageFor(currentPath);
1021
+ if (!session.entryPage && hasConsent("personalization", gate)) session.entryPage = entryPage;
809
1022
  session.pagesVisited.push(currentPath);
810
1023
  sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
811
1024
  const totalViews = (Number(localStorage.getItem(TOTAL_VIEWS_KEY)) || 0) + 1;
@@ -817,26 +1030,11 @@ function collectBehavioral(currentPath, category) {
817
1030
  localStorage.setItem(CATEGORY_SCORES_KEY, JSON.stringify(categoryScores));
818
1031
  }
819
1032
  return {
820
- session: { pageViewCount: session.pageViewCount, totalPageViews: totalViews, sessionDuration: Math.floor((Date.now() - sessionStart) / 1e3), entryPage: session.entryPage, pagesVisited: session.pagesVisited },
1033
+ session: { pageViewCount: session.pageViewCount, totalPageViews: totalViews, sessionDuration: Math.floor((Date.now() - sessionStart) / 1e3), entryPage, pagesVisited: session.pagesVisited },
821
1034
  behavioral: { categoryScores, totalScore: 0 }
822
1035
  };
823
1036
  }
824
1037
 
825
- // src/audience/selfid-collector.ts
826
- var SELFID_KEY = "kywi_selfid";
827
- function collectSelfId() {
828
- const raw = localStorage.getItem(SELFID_KEY);
829
- if (!raw) return {};
830
- try {
831
- return JSON.parse(raw);
832
- } catch {
833
- return {};
834
- }
835
- }
836
- function storeSelfId(responses) {
837
- localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
838
- }
839
-
840
1038
  // src/audience/evaluate.ts
841
1039
  function merge(base, partial) {
842
1040
  return {
@@ -844,17 +1042,18 @@ function merge(base, partial) {
844
1042
  referrer: partial.referrer ?? base.referrer,
845
1043
  session: partial.session ?? base.session,
846
1044
  behavioral: partial.behavioral ?? base.behavioral,
1045
+ query: { ...base.query, ...partial.query },
847
1046
  selfId: { ...base.selfId, ...partial.selfId },
848
1047
  identity: partial.identity ?? base.identity,
849
1048
  adapters: { ...base.adapters, ...partial.adapters },
850
1049
  meta: { ...base.meta, ...partial.meta }
851
1050
  };
852
1051
  }
853
- function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory) {
1052
+ function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory, gate = {}) {
854
1053
  let signals = merge(createEmptySignals(), serverSignals);
855
- const client = collectBehavioral(currentPath, pageCategory);
1054
+ const client = collectBehavioral(currentPath, pageCategory, gate);
856
1055
  const selfId = collectSelfId();
857
- signals = merge(signals, { session: client.session, behavioral: client.behavioral, selfId, meta: { ...signals.meta, resolvedAt: "client" } });
1056
+ signals = merge(signals, { session: client.session, behavioral: client.behavioral, query: collectQuery(), selfId, meta: { ...signals.meta, resolvedAt: "client" } });
858
1057
  const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
859
1058
  const isOptedOut2 = signals.meta.isOptedOut;
860
1059
  return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
@@ -882,36 +1081,6 @@ function patchVariantContainers(winningAudienceId) {
882
1081
  }
883
1082
  }
884
1083
 
885
- // src/cookies.ts
886
- var COOKIE_NAMES = {
887
- SIGNALS: "kywi_signals",
888
- VISITOR: "kywi_visitor",
889
- UTM: "kywi_utm",
890
- AUDIENCE: "kywi_audience",
891
- OPTOUT: "kywi_optout",
892
- KNOWN: "kywi_known",
893
- PREVIEW_INIT: "kywi_preview_init"
894
- };
895
- function readCookies() {
896
- const map = /* @__PURE__ */ new Map();
897
- if (typeof document === "undefined") return map;
898
- for (const pair of document.cookie.split(";")) {
899
- const trimmed = pair.trim();
900
- const eqIdx = trimmed.indexOf("=");
901
- if (eqIdx < 0) continue;
902
- map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
903
- }
904
- return map;
905
- }
906
- function setCookie(name, value, maxAge) {
907
- let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
908
- if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
909
- document.cookie = str;
910
- }
911
- function deleteCookie(name) {
912
- document.cookie = `${name}=; Path=/; Max-Age=0`;
913
- }
914
-
915
1084
  // src/audience/transparency.ts
916
1085
  var COLLAPSED_KEY = "kywi_transparency_collapsed";
917
1086
  var LEGACY_DISMISSED_KEY = "kywi_transparency_dismissed";
@@ -919,10 +1088,10 @@ var ROOT_ID = "kywi-transparency";
919
1088
  var PANEL_ID = "kywi-transparency-panel";
920
1089
  var TITLE_ID = "kywi-transparency-title";
921
1090
  var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
922
- var DEFAULT_API_BASE = "/api/v1";
1091
+ var DEFAULT_API_BASE2 = "/api/v1";
923
1092
  var lastState = null;
924
1093
  var forcedOpen = false;
925
- function readStorage(key) {
1094
+ function readStorage2(key) {
926
1095
  try {
927
1096
  return localStorage.getItem(key);
928
1097
  } catch {
@@ -942,12 +1111,12 @@ function removeStorage(key) {
942
1111
  }
943
1112
  }
944
1113
  function isCollapsed() {
945
- if (readStorage(LEGACY_DISMISSED_KEY) === "1") {
1114
+ if (readStorage2(LEGACY_DISMISSED_KEY) === "1") {
946
1115
  removeStorage(LEGACY_DISMISSED_KEY);
947
1116
  writeStorage(COLLAPSED_KEY, "1");
948
1117
  return true;
949
1118
  }
950
- return readStorage(COLLAPSED_KEY) !== "0";
1119
+ return readStorage2(COLLAPSED_KEY) !== "0";
951
1120
  }
952
1121
  function setCollapsed(collapsed) {
953
1122
  writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
@@ -966,7 +1135,7 @@ function hasPin() {
966
1135
  return readCookies().has(COOKIE_NAMES.AUDIENCE);
967
1136
  }
968
1137
  async function pinAudience(audienceId, opts = {}) {
969
- const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
1138
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE2;
970
1139
  try {
971
1140
  const res = await fetch(`${apiBase}/kywi/audience`, {
972
1141
  method: "POST",
@@ -1228,74 +1397,6 @@ function readServerMeta() {
1228
1397
  return { audienceId: m("audience-id"), experimentId: m("experiment-id"), variantId: m("variant-id"), visitorId: m("visitor-id") ?? "" };
1229
1398
  }
1230
1399
 
1231
- // src/consent.ts
1232
- var CONSENT_COOKIE = "kywi_consent";
1233
- var AUDIENCE_PIN_COOKIE = "kywi_audience";
1234
- var SWITCHER_PIN_PREFIX = "switcher:";
1235
- var COOKIE_CONSENT_CATEGORY = {
1236
- kywi_consent: "essential",
1237
- kywi_optout: "essential",
1238
- kywi_preview_init: "essential",
1239
- kywi_signals: "personalization",
1240
- kywi_visitor: "personalization",
1241
- kywi_utm: "personalization",
1242
- kywi_audience: "personalization",
1243
- kywi_known: "personalization"
1244
- };
1245
- function getConsentState() {
1246
- const cookies = readCookies();
1247
- const raw = cookies.get(CONSENT_COOKIE);
1248
- if (!raw) return null;
1249
- let parsed;
1250
- try {
1251
- parsed = JSON.parse(raw);
1252
- } catch {
1253
- return null;
1254
- }
1255
- if (typeof parsed !== "object" || parsed === null) return null;
1256
- const obj = parsed;
1257
- return {
1258
- essential: true,
1259
- analytics: obj.analytics === true,
1260
- marketing: obj.marketing === true,
1261
- personalization: obj.personalization === true
1262
- };
1263
- }
1264
- function setConsent(prefs) {
1265
- const state = {
1266
- essential: true,
1267
- analytics: prefs.analytics ?? false,
1268
- marketing: prefs.marketing ?? false,
1269
- personalization: prefs.personalization ?? false
1270
- };
1271
- setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
1272
- clearDeniedCookies(state);
1273
- }
1274
- function clearDeniedCookies(state) {
1275
- if (typeof document === "undefined") return [];
1276
- const cookies = readCookies();
1277
- const deleted = [];
1278
- for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
1279
- if (category === "essential") continue;
1280
- if (state && state[category] === true) continue;
1281
- if (isDeliberatePreference(name, cookies.get(name))) continue;
1282
- deleteCookie(name);
1283
- deleted.push(name);
1284
- }
1285
- return deleted;
1286
- }
1287
- function isDeliberatePreference(name, value) {
1288
- if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
1289
- return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
1290
- }
1291
- function hasConsent(category, options = {}) {
1292
- if (options.requireConsent === false) return true;
1293
- if (category === "essential") return true;
1294
- const state = getConsentState();
1295
- if (!state) return false;
1296
- return state[category] === true;
1297
- }
1298
-
1299
1400
  // src/audience/adapter-runner.ts
1300
1401
  var CACHE_PREFIX = "kywi_adapter_";
1301
1402
  function getCached(adapterId) {
@@ -1618,7 +1719,7 @@ function renderPersonalizationBadge(info = {}) {
1618
1719
  }
1619
1720
 
1620
1721
  // src/audience/index.ts
1621
- function resolveApiBase(explicit) {
1722
+ function resolveApiBase2(explicit) {
1622
1723
  if (explicit) return explicit;
1623
1724
  const basePath = Kywi.context?.basePath;
1624
1725
  if (!basePath || basePath === "/") return "/api/v1";
@@ -1626,7 +1727,7 @@ function resolveApiBase(explicit) {
1626
1727
  }
1627
1728
  async function bootAudienceEngine(config) {
1628
1729
  const { audiences, currentPath, pageCategory, serverSignals } = config;
1629
- const apiBase = resolveApiBase(config.apiBase);
1730
+ const apiBase = resolveApiBase2(config.apiBase);
1630
1731
  checkPreviewInit();
1631
1732
  const serverMeta = readServerMeta();
1632
1733
  const previewId = getPreviewAudienceId();
@@ -1653,7 +1754,7 @@ async function bootAudienceEngine(config) {
1653
1754
  const adapterResults = await runClientAdapters(config.adapters, merged, apiBase);
1654
1755
  merged.adapters = { ...merged.adapters, ...adapterResults };
1655
1756
  }
1656
- const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1757
+ const result = evaluateClientSide(audiences, merged, currentPath, pageCategory, consentGate);
1657
1758
  const rawPin = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1658
1759
  const pinnedId = rawPin?.startsWith("switcher:") ? rawPin.slice("switcher:".length) || null : rawPin;
1659
1760
  const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
@@ -1757,6 +1858,19 @@ var Kywi = {
1757
1858
  }
1758
1859
  }
1759
1860
  },
1861
+ /**
1862
+ * Record who the visitor says they are, and resolve their audience
1863
+ * (kywi-cms#200). Writes the browser-side answers, posts them to the
1864
+ * consent-gated runtime endpoint, and reloads so the SERVER can re-render
1865
+ * with the resolved audience pinned — which is the only way to reach a
1866
+ * variant arm, since every losing arm is pruned server-side (#167).
1867
+ *
1868
+ * On the global because a site's own "What brings you here?" control needs
1869
+ * it: the built-in widget's write path is module-private.
1870
+ */
1871
+ setSelfId,
1872
+ /** Forget the visitor's self-identification and un-pin their audience. */
1873
+ clearSelfId,
1760
1874
  getEntity: null,
1761
1875
  getFeed: null,
1762
1876
  renderFeed: null,
@@ -1797,12 +1911,14 @@ export {
1797
1911
  bootAudienceEngine,
1798
1912
  bootExperiments,
1799
1913
  clearDeniedCookies,
1914
+ clearSelfId,
1800
1915
  enhanceNav,
1801
1916
  getConsentState,
1802
1917
  hasConsent,
1803
1918
  initNavMenus,
1804
1919
  openTransparencyPanel,
1805
1920
  renderTransparencyPanel,
1806
- setConsent
1921
+ setConsent,
1922
+ setSelfId
1807
1923
  };
1808
1924
  //# sourceMappingURL=kywi.esm.js.map