@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.js CHANGED
@@ -30,13 +30,15 @@ var Kywi = (() => {
30
30
  bootAudienceEngine: () => bootAudienceEngine,
31
31
  bootExperiments: () => bootExperiments,
32
32
  clearDeniedCookies: () => clearDeniedCookies,
33
+ clearSelfId: () => clearSelfId,
33
34
  enhanceNav: () => enhanceNav,
34
35
  getConsentState: () => getConsentState,
35
36
  hasConsent: () => hasConsent,
36
37
  initNavMenus: () => initNavMenus,
37
38
  openTransparencyPanel: () => openTransparencyPanel,
38
39
  renderTransparencyPanel: () => renderTransparencyPanel,
39
- setConsent: () => setConsent
40
+ setConsent: () => setConsent,
41
+ setSelfId: () => setSelfId
40
42
  });
41
43
 
42
44
  // src/context.ts
@@ -577,6 +579,98 @@ var Kywi = (() => {
577
579
  return runtime;
578
580
  }
579
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
+
580
674
  // ../core/dist/audiences/types.js
581
675
  function createEmptySignals() {
582
676
  return {
@@ -584,6 +678,7 @@ var Kywi = (() => {
584
678
  referrer: { raw: null, domain: null, sourceType: "direct" },
585
679
  session: { pageViewCount: 0, totalPageViews: 0, sessionDuration: 0, entryPage: "/", pagesVisited: [] },
586
680
  behavioral: { categoryScores: {}, totalScore: 0 },
681
+ query: {},
587
682
  selfId: {},
588
683
  identity: { visitorId: "", isKnown: false, maLeadId: null },
589
684
  adapters: {},
@@ -644,6 +739,10 @@ var Kywi = (() => {
644
739
  return signals.session.entryPage;
645
740
  if (ref === "session.pagesVisited")
646
741
  return signals.session.pagesVisited.join(",");
742
+ if (ref.startsWith("query.")) {
743
+ const key = ref.slice(6);
744
+ return signals.query[key] ?? null;
745
+ }
647
746
  if (ref.startsWith("selfId.")) {
648
747
  const key = ref.slice(7);
649
748
  return signals.selfId[key] ?? null;
@@ -811,6 +910,121 @@ var Kywi = (() => {
811
910
  return { winningAudienceId: allMatchedIds[0] ?? null, allMatchedIds, traces };
812
911
  }
813
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
+
814
1028
  // src/audience/behavioral-collector.ts
815
1029
  var SESSION_KEY = "kywi_session";
816
1030
  var TOTAL_VIEWS_KEY = "kywi_total_views";
@@ -843,10 +1057,11 @@ var Kywi = (() => {
843
1057
  }
844
1058
  return {};
845
1059
  }
846
- function collectBehavioral(currentPath, category) {
1060
+ function collectBehavioral(currentPath, category, gate = {}) {
847
1061
  const session = getSession();
848
1062
  session.pageViewCount += 1;
849
- if (!session.entryPage) session.entryPage = currentPath;
1063
+ const entryPage = session.entryPage || entryPageFor(currentPath);
1064
+ if (!session.entryPage && hasConsent("personalization", gate)) session.entryPage = entryPage;
850
1065
  session.pagesVisited.push(currentPath);
851
1066
  sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
852
1067
  const totalViews = (Number(localStorage.getItem(TOTAL_VIEWS_KEY)) || 0) + 1;
@@ -858,26 +1073,11 @@ var Kywi = (() => {
858
1073
  localStorage.setItem(CATEGORY_SCORES_KEY, JSON.stringify(categoryScores));
859
1074
  }
860
1075
  return {
861
- 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 },
862
1077
  behavioral: { categoryScores, totalScore: 0 }
863
1078
  };
864
1079
  }
865
1080
 
866
- // src/audience/selfid-collector.ts
867
- var SELFID_KEY = "kywi_selfid";
868
- function collectSelfId() {
869
- const raw = localStorage.getItem(SELFID_KEY);
870
- if (!raw) return {};
871
- try {
872
- return JSON.parse(raw);
873
- } catch {
874
- return {};
875
- }
876
- }
877
- function storeSelfId(responses) {
878
- localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
879
- }
880
-
881
1081
  // src/audience/evaluate.ts
882
1082
  function merge(base, partial) {
883
1083
  return {
@@ -885,17 +1085,18 @@ var Kywi = (() => {
885
1085
  referrer: partial.referrer ?? base.referrer,
886
1086
  session: partial.session ?? base.session,
887
1087
  behavioral: partial.behavioral ?? base.behavioral,
1088
+ query: { ...base.query, ...partial.query },
888
1089
  selfId: { ...base.selfId, ...partial.selfId },
889
1090
  identity: partial.identity ?? base.identity,
890
1091
  adapters: { ...base.adapters, ...partial.adapters },
891
1092
  meta: { ...base.meta, ...partial.meta }
892
1093
  };
893
1094
  }
894
- function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory) {
1095
+ function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory, gate = {}) {
895
1096
  let signals = merge(createEmptySignals(), serverSignals);
896
- const client = collectBehavioral(currentPath, pageCategory);
1097
+ const client = collectBehavioral(currentPath, pageCategory, gate);
897
1098
  const selfId = collectSelfId();
898
- 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" } });
899
1100
  const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
900
1101
  const isOptedOut2 = signals.meta.isOptedOut;
901
1102
  return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
@@ -923,36 +1124,6 @@ var Kywi = (() => {
923
1124
  }
924
1125
  }
925
1126
 
926
- // src/cookies.ts
927
- var COOKIE_NAMES = {
928
- SIGNALS: "kywi_signals",
929
- VISITOR: "kywi_visitor",
930
- UTM: "kywi_utm",
931
- AUDIENCE: "kywi_audience",
932
- OPTOUT: "kywi_optout",
933
- KNOWN: "kywi_known",
934
- PREVIEW_INIT: "kywi_preview_init"
935
- };
936
- function readCookies() {
937
- const map = /* @__PURE__ */ new Map();
938
- if (typeof document === "undefined") return map;
939
- for (const pair of document.cookie.split(";")) {
940
- const trimmed = pair.trim();
941
- const eqIdx = trimmed.indexOf("=");
942
- if (eqIdx < 0) continue;
943
- map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
944
- }
945
- return map;
946
- }
947
- function setCookie(name, value, maxAge) {
948
- let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
949
- if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
950
- document.cookie = str;
951
- }
952
- function deleteCookie(name) {
953
- document.cookie = `${name}=; Path=/; Max-Age=0`;
954
- }
955
-
956
1127
  // src/audience/transparency.ts
957
1128
  var COLLAPSED_KEY = "kywi_transparency_collapsed";
958
1129
  var LEGACY_DISMISSED_KEY = "kywi_transparency_dismissed";
@@ -960,10 +1131,10 @@ var Kywi = (() => {
960
1131
  var PANEL_ID = "kywi-transparency-panel";
961
1132
  var TITLE_ID = "kywi-transparency-title";
962
1133
  var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
963
- var DEFAULT_API_BASE = "/api/v1";
1134
+ var DEFAULT_API_BASE2 = "/api/v1";
964
1135
  var lastState = null;
965
1136
  var forcedOpen = false;
966
- function readStorage(key) {
1137
+ function readStorage2(key) {
967
1138
  try {
968
1139
  return localStorage.getItem(key);
969
1140
  } catch {
@@ -983,12 +1154,12 @@ var Kywi = (() => {
983
1154
  }
984
1155
  }
985
1156
  function isCollapsed() {
986
- if (readStorage(LEGACY_DISMISSED_KEY) === "1") {
1157
+ if (readStorage2(LEGACY_DISMISSED_KEY) === "1") {
987
1158
  removeStorage(LEGACY_DISMISSED_KEY);
988
1159
  writeStorage(COLLAPSED_KEY, "1");
989
1160
  return true;
990
1161
  }
991
- return readStorage(COLLAPSED_KEY) !== "0";
1162
+ return readStorage2(COLLAPSED_KEY) !== "0";
992
1163
  }
993
1164
  function setCollapsed(collapsed) {
994
1165
  writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
@@ -1007,7 +1178,7 @@ var Kywi = (() => {
1007
1178
  return readCookies().has(COOKIE_NAMES.AUDIENCE);
1008
1179
  }
1009
1180
  async function pinAudience(audienceId, opts = {}) {
1010
- const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
1181
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE2;
1011
1182
  try {
1012
1183
  const res = await fetch(`${apiBase}/kywi/audience`, {
1013
1184
  method: "POST",
@@ -1269,74 +1440,6 @@ var Kywi = (() => {
1269
1440
  return { audienceId: m("audience-id"), experimentId: m("experiment-id"), variantId: m("variant-id"), visitorId: m("visitor-id") ?? "" };
1270
1441
  }
1271
1442
 
1272
- // src/consent.ts
1273
- var CONSENT_COOKIE = "kywi_consent";
1274
- var AUDIENCE_PIN_COOKIE = "kywi_audience";
1275
- var SWITCHER_PIN_PREFIX = "switcher:";
1276
- var COOKIE_CONSENT_CATEGORY = {
1277
- kywi_consent: "essential",
1278
- kywi_optout: "essential",
1279
- kywi_preview_init: "essential",
1280
- kywi_signals: "personalization",
1281
- kywi_visitor: "personalization",
1282
- kywi_utm: "personalization",
1283
- kywi_audience: "personalization",
1284
- kywi_known: "personalization"
1285
- };
1286
- function getConsentState() {
1287
- const cookies = readCookies();
1288
- const raw = cookies.get(CONSENT_COOKIE);
1289
- if (!raw) return null;
1290
- let parsed;
1291
- try {
1292
- parsed = JSON.parse(raw);
1293
- } catch {
1294
- return null;
1295
- }
1296
- if (typeof parsed !== "object" || parsed === null) return null;
1297
- const obj = parsed;
1298
- return {
1299
- essential: true,
1300
- analytics: obj.analytics === true,
1301
- marketing: obj.marketing === true,
1302
- personalization: obj.personalization === true
1303
- };
1304
- }
1305
- function setConsent(prefs) {
1306
- const state = {
1307
- essential: true,
1308
- analytics: prefs.analytics ?? false,
1309
- marketing: prefs.marketing ?? false,
1310
- personalization: prefs.personalization ?? false
1311
- };
1312
- setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
1313
- clearDeniedCookies(state);
1314
- }
1315
- function clearDeniedCookies(state) {
1316
- if (typeof document === "undefined") return [];
1317
- const cookies = readCookies();
1318
- const deleted = [];
1319
- for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
1320
- if (category === "essential") continue;
1321
- if (state && state[category] === true) continue;
1322
- if (isDeliberatePreference(name, cookies.get(name))) continue;
1323
- deleteCookie(name);
1324
- deleted.push(name);
1325
- }
1326
- return deleted;
1327
- }
1328
- function isDeliberatePreference(name, value) {
1329
- if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
1330
- return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
1331
- }
1332
- function hasConsent(category, options = {}) {
1333
- if (options.requireConsent === false) return true;
1334
- if (category === "essential") return true;
1335
- const state = getConsentState();
1336
- if (!state) return false;
1337
- return state[category] === true;
1338
- }
1339
-
1340
1443
  // src/audience/adapter-runner.ts
1341
1444
  var CACHE_PREFIX = "kywi_adapter_";
1342
1445
  function getCached(adapterId) {
@@ -1659,7 +1762,7 @@ var Kywi = (() => {
1659
1762
  }
1660
1763
 
1661
1764
  // src/audience/index.ts
1662
- function resolveApiBase(explicit) {
1765
+ function resolveApiBase2(explicit) {
1663
1766
  if (explicit) return explicit;
1664
1767
  const basePath = Kywi.context?.basePath;
1665
1768
  if (!basePath || basePath === "/") return "/api/v1";
@@ -1667,7 +1770,7 @@ var Kywi = (() => {
1667
1770
  }
1668
1771
  async function bootAudienceEngine(config) {
1669
1772
  const { audiences, currentPath, pageCategory, serverSignals } = config;
1670
- const apiBase = resolveApiBase(config.apiBase);
1773
+ const apiBase = resolveApiBase2(config.apiBase);
1671
1774
  checkPreviewInit();
1672
1775
  const serverMeta = readServerMeta();
1673
1776
  const previewId = getPreviewAudienceId();
@@ -1694,7 +1797,7 @@ var Kywi = (() => {
1694
1797
  const adapterResults = await runClientAdapters(config.adapters, merged, apiBase);
1695
1798
  merged.adapters = { ...merged.adapters, ...adapterResults };
1696
1799
  }
1697
- const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1800
+ const result = evaluateClientSide(audiences, merged, currentPath, pageCategory, consentGate);
1698
1801
  const rawPin = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1699
1802
  const pinnedId = rawPin?.startsWith("switcher:") ? rawPin.slice("switcher:".length) || null : rawPin;
1700
1803
  const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
@@ -1798,6 +1901,19 @@ var Kywi = (() => {
1798
1901
  }
1799
1902
  }
1800
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,
1801
1917
  getEntity: null,
1802
1918
  getFeed: null,
1803
1919
  renderFeed: null,