@kywi-software/js 0.24.0 → 0.26.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.
package/dist/kywi.js CHANGED
@@ -23,6 +23,8 @@ var Kywi = (() => {
23
23
  __export(src_exports, {
24
24
  AUDIENCE_PIN_COOKIE: () => AUDIENCE_PIN_COOKIE,
25
25
  CONSENT_COOKIE: () => CONSENT_COOKIE,
26
+ CONSENT_EVENT: () => CONSENT_EVENT,
27
+ CONSENT_REOPEN_EVENT: () => CONSENT_REOPEN_EVENT,
26
28
  COOKIE_CONSENT_CATEGORY: () => COOKIE_CONSENT_CATEGORY,
27
29
  Kywi: () => Kywi,
28
30
  NAV_BREAKPOINT: () => NAV_BREAKPOINT,
@@ -37,6 +39,7 @@ var Kywi = (() => {
37
39
  initNavMenus: () => initNavMenus,
38
40
  openTransparencyPanel: () => openTransparencyPanel,
39
41
  renderTransparencyPanel: () => renderTransparencyPanel,
42
+ reopenConsent: () => reopenConsent,
40
43
  setConsent: () => setConsent,
41
44
  setSelfId: () => setSelfId
42
45
  });
@@ -58,6 +61,26 @@ var Kywi = (() => {
58
61
  };
59
62
  }
60
63
 
64
+ // src/site-scope.ts
65
+ function currentSiteId() {
66
+ if (typeof document === "undefined") return "";
67
+ return document.querySelector('meta[name="kywi:site-id"]')?.getAttribute("content") || "";
68
+ }
69
+ function withSiteId(url) {
70
+ if (!isSameOrigin(url)) return url;
71
+ const siteId = currentSiteId();
72
+ if (!siteId || /[?&]siteId=/.test(url)) return url;
73
+ const [path, hash] = url.split("#", 2);
74
+ const joined = `${path}${path.includes("?") ? "&" : "?"}siteId=${encodeURIComponent(siteId)}`;
75
+ return hash === void 0 ? joined : `${joined}#${hash}`;
76
+ }
77
+ function isSameOrigin(url) {
78
+ if (url.startsWith("/")) return !url.startsWith("//");
79
+ const origin = typeof window !== "undefined" ? window.location?.origin : void 0;
80
+ if (!origin || origin === "null") return false;
81
+ return url === origin || url.startsWith(`${origin}/`);
82
+ }
83
+
61
84
  // src/entity.ts
62
85
  function createEntity(type, basePath, csrfToken, data) {
63
86
  const fields = { ...data };
@@ -77,7 +100,7 @@ var Kywi = (() => {
77
100
  async save() {
78
101
  const id = fields["id"];
79
102
  const url = id ? `${basePath}/content/${type}/${id}` : `${basePath}/content/${type}`;
80
- const res = await fetch(url, { method: id ? "PUT" : "POST", headers, body: JSON.stringify(Object.fromEntries(dirty)) });
103
+ const res = await fetch(withSiteId(url), { method: id ? "PUT" : "POST", headers, body: JSON.stringify(Object.fromEntries(dirty)) });
81
104
  const json = await res.json();
82
105
  if (!res.ok || json.error) {
83
106
  errors.push(json.error?.message ?? "Save failed");
@@ -89,14 +112,14 @@ var Kywi = (() => {
89
112
  },
90
113
  async delete() {
91
114
  const id = fields["id"];
92
- await fetch(`${basePath}/content/${type}/${id}`, { method: "DELETE", headers });
115
+ await fetch(withSiteId(`${basePath}/content/${type}/${id}`), { method: "DELETE", headers });
93
116
  },
94
117
  getErrors() {
95
118
  return [...errors];
96
119
  },
97
120
  async loadBy(field, value) {
98
121
  const url = field === "id" ? `${basePath}/content/${type}/${value}` : `${basePath}/content/${type}?${field}=${encodeURIComponent(value)}`;
99
- const res = await fetch(url, { headers });
122
+ const res = await fetch(withSiteId(url), { headers });
100
123
  const json = await res.json();
101
124
  if (!res.ok || json.error) {
102
125
  errors.push(json.error?.message ?? "Load failed");
@@ -215,7 +238,7 @@ var Kywi = (() => {
215
238
  const agg = { function: fn };
216
239
  if (field) agg["field"] = field;
217
240
  body["aggregate"] = agg;
218
- const res = await fetch(`${this.basePath}/feeds/aggregate`, {
241
+ const res = await fetch(withSiteId(`${this.basePath}/feeds/aggregate`), {
219
242
  method: "POST",
220
243
  headers: this.buildHeaders(),
221
244
  body: JSON.stringify(body)
@@ -225,7 +248,7 @@ var Kywi = (() => {
225
248
  }
226
249
  async getQuery() {
227
250
  const body = this.buildBody();
228
- const res = await fetch(`${this.basePath}/feeds/query`, {
251
+ const res = await fetch(withSiteId(`${this.basePath}/feeds/query`), {
229
252
  method: "POST",
230
253
  headers: this.buildHeaders(),
231
254
  body: JSON.stringify(body)
@@ -514,6 +537,248 @@ var Kywi = (() => {
514
537
  };
515
538
  }
516
539
 
540
+ // src/cookies.ts
541
+ var COOKIE_NAMES = {
542
+ SIGNALS: "kywi_signals",
543
+ VISITOR: "kywi_visitor",
544
+ UTM: "kywi_utm",
545
+ AUDIENCE: "kywi_audience",
546
+ OPTOUT: "kywi_optout",
547
+ KNOWN: "kywi_known",
548
+ PREVIEW_INIT: "kywi_preview_init",
549
+ BUCKET: "kywi_bucket"
550
+ };
551
+ function readCookies() {
552
+ const map = /* @__PURE__ */ new Map();
553
+ if (typeof document === "undefined") return map;
554
+ for (const pair of document.cookie.split(";")) {
555
+ const trimmed = pair.trim();
556
+ const eqIdx = trimmed.indexOf("=");
557
+ if (eqIdx < 0) continue;
558
+ map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
559
+ }
560
+ return map;
561
+ }
562
+ function setCookie(name, value, maxAge) {
563
+ let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
564
+ if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
565
+ document.cookie = str;
566
+ }
567
+ function deleteCookie(name) {
568
+ document.cookie = `${name}=; Path=/; Max-Age=0`;
569
+ }
570
+
571
+ // src/consent.ts
572
+ var CONSENT_COOKIE = "kywi_consent";
573
+ var CONSENT_EVENT = "kywi:consent";
574
+ var CONSENT_REOPEN_EVENT = "kywi:consent-reopen";
575
+ function reopenConsent() {
576
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
577
+ window.dispatchEvent(new CustomEvent(CONSENT_REOPEN_EVENT));
578
+ }
579
+ var AUDIENCE_PIN_COOKIE = "kywi_audience";
580
+ var SWITCHER_PIN_PREFIX = "switcher:";
581
+ var COOKIE_CONSENT_CATEGORY = {
582
+ kywi_consent: "essential",
583
+ kywi_optout: "essential",
584
+ kywi_preview_init: "essential",
585
+ kywi_bucket: "essential",
586
+ kywi_entry: "essential",
587
+ kywi_utm_visit: "essential",
588
+ kywi_selfid: "essential",
589
+ kywi_signals: "personalization",
590
+ kywi_visitor: "personalization",
591
+ kywi_utm: "personalization",
592
+ kywi_audience: "personalization",
593
+ kywi_known: "personalization"
594
+ };
595
+ function getConsentState() {
596
+ const cookies = readCookies();
597
+ const raw = cookies.get(CONSENT_COOKIE);
598
+ if (!raw) return null;
599
+ let parsed;
600
+ try {
601
+ parsed = JSON.parse(raw);
602
+ } catch {
603
+ return null;
604
+ }
605
+ if (typeof parsed !== "object" || parsed === null) return null;
606
+ const obj = parsed;
607
+ return {
608
+ essential: true,
609
+ analytics: obj.analytics === true,
610
+ marketing: obj.marketing === true,
611
+ personalization: obj.personalization === true
612
+ };
613
+ }
614
+ function setConsent(prefs) {
615
+ const state = {
616
+ essential: true,
617
+ analytics: prefs.analytics ?? false,
618
+ marketing: prefs.marketing ?? false,
619
+ personalization: prefs.personalization ?? false
620
+ };
621
+ setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
622
+ clearDeniedCookies(state);
623
+ announceConsent(state);
624
+ }
625
+ function announceConsent(state) {
626
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
627
+ try {
628
+ window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: state }));
629
+ } catch {
630
+ }
631
+ }
632
+ function clearDeniedCookies(state) {
633
+ if (typeof document === "undefined") return [];
634
+ const cookies = readCookies();
635
+ const deleted = [];
636
+ for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
637
+ if (category === "essential") continue;
638
+ if (state && state[category] === true) continue;
639
+ if (isDeliberatePreference(name, cookies.get(name))) continue;
640
+ deleteCookie(name);
641
+ deleted.push(name);
642
+ }
643
+ return deleted;
644
+ }
645
+ function isDeliberatePreference(name, value) {
646
+ if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
647
+ return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
648
+ }
649
+ function hasConsent(category, options = {}) {
650
+ if (options.requireConsent === false) return true;
651
+ if (category === "essential") return true;
652
+ const state = getConsentState();
653
+ if (!state) return false;
654
+ return state[category] === true;
655
+ }
656
+
657
+ // src/experiments/pending.ts
658
+ var PENDING_EVENTS_META = "kywi:pending-events";
659
+ var PENDING_STORAGE_KEY = "kywi:pending-events";
660
+ var EMPTY = () => ({ exposures: [], matched: [] });
661
+ var memory = EMPTY();
662
+ var consentListener = null;
663
+ function hasValidSiteId(o) {
664
+ return o.siteId === void 0 || typeof o.siteId === "string" && o.siteId.length > 0;
665
+ }
666
+ function isExposure(v) {
667
+ if (typeof v !== "object" || v === null) return false;
668
+ const o = v;
669
+ return typeof o.experimentId === "string" && o.experimentId.length > 0 && (typeof o.variantKey === "string" || o.variantKey === null) && typeof o.at === "number" && hasValidSiteId(o);
670
+ }
671
+ function isMatch(v) {
672
+ if (typeof v !== "object" || v === null) return false;
673
+ const o = v;
674
+ return typeof o.audienceId === "string" && o.audienceId.length > 0 && typeof o.at === "number" && typeof o.sig === "string" && o.sig.length > 0 && hasValidSiteId(o);
675
+ }
676
+ function parsePending(raw) {
677
+ if (!raw) return EMPTY();
678
+ try {
679
+ const parsed = JSON.parse(raw);
680
+ const exposures = Array.isArray(parsed?.exposures) ? parsed.exposures.filter(isExposure) : [];
681
+ const matched = Array.isArray(parsed?.matched) ? parsed.matched.filter(isMatch) : [];
682
+ return { exposures, matched };
683
+ } catch {
684
+ return EMPTY();
685
+ }
686
+ }
687
+ function readStorage() {
688
+ try {
689
+ return parsePending(sessionStorage.getItem(PENDING_STORAGE_KEY));
690
+ } catch {
691
+ return EMPTY();
692
+ }
693
+ }
694
+ function writeStorage(events) {
695
+ try {
696
+ if (isEmpty(events)) sessionStorage.removeItem(PENDING_STORAGE_KEY);
697
+ else sessionStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(events));
698
+ } catch {
699
+ }
700
+ }
701
+ function isEmpty(events) {
702
+ return events.exposures.length === 0 && events.matched.length === 0;
703
+ }
704
+ function merge(into, from) {
705
+ const expKey = (e) => `${e.siteId ?? ""}|${e.experimentId}`;
706
+ const audKey = (m) => `${m.siteId ?? ""}|${m.audienceId}`;
707
+ const byExperiment = new Map(into.exposures.map((e) => [expKey(e), e]));
708
+ for (const e of from.exposures) if (!byExperiment.has(expKey(e))) byExperiment.set(expKey(e), e);
709
+ const byAudience = new Map(into.matched.map((m) => [audKey(m), m]));
710
+ for (const m of from.matched) if (!byAudience.has(audKey(m))) byAudience.set(audKey(m), m);
711
+ return { exposures: [...byExperiment.values()], matched: [...byAudience.values()] };
712
+ }
713
+ function stampSite(events) {
714
+ const siteId = currentSiteId();
715
+ if (!siteId) return events;
716
+ return {
717
+ exposures: events.exposures.map((e) => e.siteId ? e : { ...e, siteId }),
718
+ matched: events.matched.map((m) => m.siteId ? m : { ...m, siteId })
719
+ };
720
+ }
721
+ function pendingEvents() {
722
+ return merge(memory, readStorage());
723
+ }
724
+ function queuePendingEvents(events) {
725
+ memory = merge(pendingEvents(), stampSite(events));
726
+ writeStorage(memory);
727
+ }
728
+ function clearQueue() {
729
+ memory = EMPTY();
730
+ writeStorage(memory);
731
+ }
732
+ function urlForSite(url, siteId) {
733
+ if (!siteId) return withSiteId(url);
734
+ const origin = typeof window !== "undefined" ? window.location?.origin : void 0;
735
+ const sameOrigin = url.startsWith("/") && !url.startsWith("//") || !!origin && origin !== "null" && (url === origin || url.startsWith(`${origin}/`));
736
+ if (!sameOrigin || /[?&]siteId=/.test(url)) return url;
737
+ return `${url}${url.includes("?") ? "&" : "?"}siteId=${encodeURIComponent(siteId)}`;
738
+ }
739
+ function post(url, siteId, body, csrfToken) {
740
+ try {
741
+ void fetch(urlForSite(url, siteId), {
742
+ method: "POST",
743
+ headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
744
+ body: JSON.stringify(body),
745
+ keepalive: true
746
+ }).catch(() => {
747
+ });
748
+ } catch {
749
+ }
750
+ }
751
+ function flushPendingEvents(config) {
752
+ const gate = config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
753
+ const queue = pendingEvents();
754
+ if (isEmpty(queue)) return 0;
755
+ if (!hasConsent("personalization", gate) || !hasConsent("analytics", gate)) {
756
+ if (getConsentState() !== null) clearQueue();
757
+ return 0;
758
+ }
759
+ for (const e of queue.exposures) {
760
+ post(`${config.apiBase}/kywi/experiments/exposure`, e.siteId, { experimentId: e.experimentId, at: e.at }, config.csrfToken);
761
+ }
762
+ for (const m of queue.matched) {
763
+ post(`${config.apiBase}/kywi/analytics/matched`, m.siteId, { audienceId: m.audienceId, at: m.at, sig: m.sig }, config.csrfToken);
764
+ }
765
+ clearQueue();
766
+ return queue.exposures.length + queue.matched.length;
767
+ }
768
+ function bootPendingEvents(config) {
769
+ if (typeof document !== "undefined") {
770
+ const raw = document.querySelector(`meta[name="${PENDING_EVENTS_META}"]`)?.getAttribute("content");
771
+ queuePendingEvents(parsePending(raw));
772
+ }
773
+ flushPendingEvents(config);
774
+ if (typeof window === "undefined") return;
775
+ if (consentListener) window.removeEventListener(CONSENT_EVENT, consentListener);
776
+ consentListener = () => {
777
+ flushPendingEvents(config);
778
+ };
779
+ window.addEventListener(CONSENT_EVENT, consentListener);
780
+ }
781
+
517
782
  // src/experiments/index.ts
518
783
  function apiBaseFrom(ctx) {
519
784
  return ctx.basePath && ctx.basePath !== "/" ? `${ctx.basePath}/api/v1` : "/api/v1";
@@ -540,7 +805,7 @@ var Kywi = (() => {
540
805
  }
541
806
  function beacon(url, body, csrfToken) {
542
807
  try {
543
- void fetch(url, {
808
+ void fetch(withSiteId(url), {
544
809
  method: "POST",
545
810
  headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
546
811
  body: JSON.stringify(body),
@@ -566,6 +831,7 @@ var Kywi = (() => {
566
831
  if (experimentId) {
567
832
  beacon(`${apiBase}/kywi/experiments/exposure`, { experimentId, visitorId }, csrf);
568
833
  }
834
+ bootPendingEvents({ apiBase, ...csrf ? { csrfToken: csrf } : {} });
569
835
  if (config.autoPageview !== false) {
570
836
  trackConversion({ type: "pageview", url: currentPath });
571
837
  }
@@ -582,7 +848,7 @@ var Kywi = (() => {
582
848
  // src/audience/selfid-collector.ts
583
849
  var SELFID_KEY = "kywi_selfid";
584
850
  function collectSelfId() {
585
- const raw = readStorage(SELFID_KEY);
851
+ const raw = readStorage2(SELFID_KEY);
586
852
  if (!raw) return {};
587
853
  try {
588
854
  const parsed = JSON.parse(raw);
@@ -592,7 +858,9 @@ var Kywi = (() => {
592
858
  return {};
593
859
  }
594
860
  }
595
- function storeSelfId(responses) {
861
+ function storeSelfId(responses, gate = {}) {
862
+ const category = COOKIE_CONSENT_CATEGORY[SELFID_KEY] ?? "personalization";
863
+ if (!hasConsent(category, gate)) return;
596
864
  try {
597
865
  localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
598
866
  } catch {
@@ -604,7 +872,7 @@ var Kywi = (() => {
604
872
  } catch {
605
873
  }
606
874
  }
607
- function readStorage(key) {
875
+ function readStorage2(key) {
608
876
  try {
609
877
  return localStorage.getItem(key);
610
878
  } catch {
@@ -651,7 +919,7 @@ var Kywi = (() => {
651
919
  async function submit(fields, options) {
652
920
  const csrfToken = resolveCsrfToken(options.csrfToken);
653
921
  try {
654
- const res = await fetch(`${resolveApiBase(options.apiBase)}/kywi/self-id`, {
922
+ const res = await fetch(withSiteId(`${resolveApiBase(options.apiBase)}/kywi/self-id`), {
655
923
  method: "POST",
656
924
  headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
657
925
  body: JSON.stringify({
@@ -925,106 +1193,6 @@ var Kywi = (() => {
925
1193
  return search ? `${currentPath}${search}` : currentPath;
926
1194
  }
927
1195
 
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
-
1028
1196
  // src/audience/behavioral-collector.ts
1029
1197
  var SESSION_KEY = "kywi_session";
1030
1198
  var TOTAL_VIEWS_KEY = "kywi_total_views";
@@ -1079,7 +1247,7 @@ var Kywi = (() => {
1079
1247
  }
1080
1248
 
1081
1249
  // src/audience/evaluate.ts
1082
- function merge(base, partial) {
1250
+ function merge2(base, partial) {
1083
1251
  return {
1084
1252
  utm: partial.utm ?? base.utm,
1085
1253
  referrer: partial.referrer ?? base.referrer,
@@ -1093,10 +1261,10 @@ var Kywi = (() => {
1093
1261
  };
1094
1262
  }
1095
1263
  function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory, gate = {}) {
1096
- let signals = merge(createEmptySignals(), serverSignals);
1264
+ let signals = merge2(createEmptySignals(), serverSignals);
1097
1265
  const client = collectBehavioral(currentPath, pageCategory, gate);
1098
1266
  const selfId = collectSelfId();
1099
- signals = merge(signals, { session: client.session, behavioral: client.behavioral, query: collectQuery(), selfId, meta: { ...signals.meta, resolvedAt: "client" } });
1267
+ signals = merge2(signals, { session: client.session, behavioral: client.behavioral, query: collectQuery(), selfId, meta: { ...signals.meta, resolvedAt: "client" } });
1100
1268
  const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
1101
1269
  const isOptedOut2 = signals.meta.isOptedOut;
1102
1270
  return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
@@ -1134,14 +1302,14 @@ var Kywi = (() => {
1134
1302
  var DEFAULT_API_BASE2 = "/api/v1";
1135
1303
  var lastState = null;
1136
1304
  var forcedOpen = false;
1137
- function readStorage2(key) {
1305
+ function readStorage3(key) {
1138
1306
  try {
1139
1307
  return localStorage.getItem(key);
1140
1308
  } catch {
1141
1309
  return null;
1142
1310
  }
1143
1311
  }
1144
- function writeStorage(key, value) {
1312
+ function writeStorage2(key, value) {
1145
1313
  try {
1146
1314
  localStorage.setItem(key, value);
1147
1315
  } catch {
@@ -1154,15 +1322,15 @@ var Kywi = (() => {
1154
1322
  }
1155
1323
  }
1156
1324
  function isCollapsed() {
1157
- if (readStorage2(LEGACY_DISMISSED_KEY) === "1") {
1325
+ if (readStorage3(LEGACY_DISMISSED_KEY) === "1") {
1158
1326
  removeStorage(LEGACY_DISMISSED_KEY);
1159
- writeStorage(COLLAPSED_KEY, "1");
1327
+ writeStorage2(COLLAPSED_KEY, "1");
1160
1328
  return true;
1161
1329
  }
1162
- return readStorage2(COLLAPSED_KEY) !== "0";
1330
+ return readStorage3(COLLAPSED_KEY) !== "0";
1163
1331
  }
1164
1332
  function setCollapsed(collapsed) {
1165
- writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
1333
+ writeStorage2(COLLAPSED_KEY, collapsed ? "1" : "0");
1166
1334
  }
1167
1335
  function optOut() {
1168
1336
  setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
@@ -1180,7 +1348,7 @@ var Kywi = (() => {
1180
1348
  async function pinAudience(audienceId, opts = {}) {
1181
1349
  const apiBase = opts.apiBase ?? DEFAULT_API_BASE2;
1182
1350
  try {
1183
- const res = await fetch(`${apiBase}/kywi/audience`, {
1351
+ const res = await fetch(withSiteId(`${apiBase}/kywi/audience`), {
1184
1352
  method: "POST",
1185
1353
  headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
1186
1354
  body: JSON.stringify({ audienceId, source: "switcher" })
@@ -1393,14 +1561,18 @@ var Kywi = (() => {
1393
1561
  }
1394
1562
  function clearPreview() {
1395
1563
  sessionStorage.removeItem(PREVIEW_KEY);
1564
+ deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
1565
+ }
1566
+ function previewAudienceFromCookie(value) {
1567
+ if (!value) return null;
1568
+ const parts = value.split(".");
1569
+ if (parts.length < 3) return null;
1570
+ const id = parts[0];
1571
+ return id.length > 0 ? id : null;
1396
1572
  }
1397
1573
  function checkPreviewInit() {
1398
- const cookies = readCookies();
1399
- const initId = cookies.get(COOKIE_NAMES.PREVIEW_INIT);
1400
- if (initId) {
1401
- setPreviewAudienceId(initId);
1402
- deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
1403
- }
1574
+ const initId = previewAudienceFromCookie(readCookies().get(COOKIE_NAMES.PREVIEW_INIT));
1575
+ if (initId) setPreviewAudienceId(initId);
1404
1576
  }
1405
1577
  function renderPreviewBanner(audienceName2) {
1406
1578
  document.querySelector(".kywi-preview-banner")?.remove();
@@ -1461,7 +1633,7 @@ var Kywi = (() => {
1461
1633
  if (cached) return cached;
1462
1634
  let result;
1463
1635
  if (adapter.side === "server-proxy") {
1464
- const res = await fetch(`${apiBase}/kywi/adapters/${adapter.id}`, {
1636
+ const res = await fetch(withSiteId(`${apiBase}/kywi/adapters/${adapter.id}`), {
1465
1637
  method: "POST",
1466
1638
  headers: { "Content-Type": "application/json" },
1467
1639
  body: JSON.stringify({ signals: currentSignals })
@@ -1480,16 +1652,18 @@ var Kywi = (() => {
1480
1652
  }
1481
1653
 
1482
1654
  // src/audience/selfid-submit.ts
1483
- function handleSelfIdSubmit(responses, audiences, currentPath) {
1484
- storeSelfId(responses);
1655
+ function handleSelfIdSubmit(responses, audiences, currentPath, gate = {}) {
1656
+ storeSelfId(responses, gate);
1485
1657
  if (audiences && currentPath) {
1486
1658
  const cookies = readCookies();
1487
1659
  const serverMeta = readServerMeta();
1660
+ const mayPersonalize = hasConsent("personalization", gate);
1661
+ const storedVisitorId = mayPersonalize ? cookies.get(COOKIE_NAMES.VISITOR) : void 0;
1488
1662
  const serverSignals = {
1489
- identity: { visitorId: serverMeta.visitorId || cookies.get(COOKIE_NAMES.VISITOR) || "", isKnown: cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1663
+ identity: { visitorId: serverMeta.visitorId || storedVisitorId || "", isKnown: mayPersonalize && cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1490
1664
  meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
1491
1665
  };
1492
- const result = evaluateClientSide(audiences, serverSignals, currentPath);
1666
+ const result = evaluateClientSide(audiences, serverSignals, currentPath, void 0, gate);
1493
1667
  patchVariantContainers(result.winningAudienceId);
1494
1668
  populateDataLayer({
1495
1669
  visitorId: result.signals.identity.visitorId,
@@ -1561,10 +1735,28 @@ var Kywi = (() => {
1561
1735
  function markShown() {
1562
1736
  localStorage.setItem(SHOWN_KEY, String(Date.now()));
1563
1737
  }
1738
+ function consentGate() {
1739
+ return config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
1740
+ }
1741
+ function patchClientSide(responses) {
1742
+ handleSelfIdSubmit(responses, config.audiences ?? [], config.currentPath ?? "/", consentGate());
1743
+ }
1564
1744
  function handleSubmit(responses) {
1565
- storeSelfId(responses);
1745
+ if (config.serverSubmit !== false) {
1746
+ markShown();
1747
+ const options = {
1748
+ ...config.apiBase !== void 0 ? { apiBase: config.apiBase } : {},
1749
+ ...config.csrfToken !== void 0 ? { csrfToken: config.csrfToken } : {}
1750
+ };
1751
+ void setSelfId(responses, options).then((result) => {
1752
+ if (!result || !result.persisted) patchClientSide(responses);
1753
+ }).catch(() => {
1754
+ });
1755
+ return;
1756
+ }
1757
+ storeSelfId(responses, consentGate());
1566
1758
  try {
1567
- handleSelfIdSubmit(responses, config.audiences ?? [], config.currentPath ?? "/");
1759
+ patchClientSide(responses);
1568
1760
  } finally {
1569
1761
  markShown();
1570
1762
  }
@@ -1817,7 +2009,7 @@ var Kywi = (() => {
1817
2009
  populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1818
2010
  fireKywiReady();
1819
2011
  if (!isPinned && mayPersonalize && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1820
- fetch(`${apiBase}/kywi/audience`, {
2012
+ fetch(withSiteId(`${apiBase}/kywi/audience`), {
1821
2013
  method: "POST",
1822
2014
  headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
1823
2015
  // Explicitly the AUTOMATIC write, so the server applies the
@@ -1828,7 +2020,15 @@ var Kywi = (() => {
1828
2020
  });
1829
2021
  }
1830
2022
  if (config.selfIdWidget) {
1831
- const widget = createSelfIdWidget(config.selfIdWidget);
2023
+ const widget = createSelfIdWidget({
2024
+ // The API base and CSRF token this boot resolved, so the widget's server
2025
+ // submit posts where the rest of the runtime does (review fix 4); a
2026
+ // value the widget config sets itself still wins.
2027
+ apiBase,
2028
+ csrfToken: Kywi.context?.csrfToken ?? null,
2029
+ ...config.selfIdWidget,
2030
+ ...config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent }
2031
+ });
1832
2032
  if (widget.shouldShow()) {
1833
2033
  const doRender = () => {
1834
2034
  const mount = resolveSelfIdMount(config.selfIdWidget.displayMode);
@@ -1914,6 +2114,11 @@ var Kywi = (() => {
1914
2114
  setSelfId,
1915
2115
  /** Forget the visitor's self-identification and un-pin their audience. */
1916
2116
  clearSelfId,
2117
+ /**
2118
+ * Re-open the cookie-consent banner so the visitor can change an earlier
2119
+ * decision (kywi-cms#191) — for a site's own "Cookie settings" link.
2120
+ */
2121
+ reopenConsent,
1917
2122
  getEntity: null,
1918
2123
  getFeed: null,
1919
2124
  renderFeed: null,