@syntrologie/runtime-sdk 2.8.0-canary.183 → 2.8.0-canary.185

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.
@@ -16144,7 +16144,7 @@ Please report this to https://github.com/markedjs/marked.`, e9) {
16144
16144
  }
16145
16145
 
16146
16146
  // src/version.ts
16147
- var SDK_VERSION = "2.8.0-canary.183";
16147
+ var SDK_VERSION = "2.8.0-canary.185";
16148
16148
 
16149
16149
  // src/types.ts
16150
16150
  var SDK_SCHEMA_VERSION = "2.0";
@@ -21640,6 +21640,300 @@ ${cssRules}
21640
21640
  return derivedCdnBase;
21641
21641
  }
21642
21642
 
21643
+ // src/defaults/clickIds.ts
21644
+ var CLICK_ID_KEYS = [
21645
+ "fbclid",
21646
+ // Meta (Facebook + Instagram)
21647
+ "ttclid",
21648
+ // TikTok
21649
+ "gclid",
21650
+ // Google Ads
21651
+ "gbraid",
21652
+ // Google Ads (iOS app web)
21653
+ "wbraid",
21654
+ // Google Ads (web app)
21655
+ "msclkid",
21656
+ // Microsoft / Bing Ads
21657
+ "twclid",
21658
+ // X / Twitter
21659
+ "li_fat_id",
21660
+ // LinkedIn
21661
+ "epik",
21662
+ // Pinterest
21663
+ "ScCid",
21664
+ // Snapchat
21665
+ "yclid",
21666
+ // Yandex
21667
+ "_kx",
21668
+ // Klaviyo
21669
+ "mc_eid",
21670
+ // Mailchimp recipient
21671
+ "mc_cid"
21672
+ // Mailchimp campaign
21673
+ ];
21674
+ function promoteClickIds(telemetry) {
21675
+ if (typeof window === "undefined") return;
21676
+ if (!telemetry.setPersonPropertiesOnce) return;
21677
+ const params = new URLSearchParams(window.location.search);
21678
+ const captured = {};
21679
+ for (const key of CLICK_ID_KEYS) {
21680
+ const value = params.get(key);
21681
+ if (value) {
21682
+ captured[`$initial_${key}`] = value;
21683
+ }
21684
+ }
21685
+ if (Object.keys(captured).length > 0) {
21686
+ telemetry.setPersonPropertiesOnce(captured);
21687
+ }
21688
+ }
21689
+
21690
+ // src/defaults/identifyOnEmail.ts
21691
+ var ANONYMOUS_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21692
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
21693
+ var DENYLIST_PATTERNS = [
21694
+ /@yopmail\.com$/i,
21695
+ /@mailinator\.com$/i,
21696
+ /@guerrillamail\.(com|info|net|org|biz)$/i,
21697
+ /@10minutemail\.com$/i,
21698
+ /^test@/i,
21699
+ /^noreply@/i,
21700
+ /^no-reply@/i
21701
+ ];
21702
+ function isValidEmail(value) {
21703
+ if (!value) return false;
21704
+ return EMAIL_RE.test(value);
21705
+ }
21706
+ function isEmailDenylisted(value) {
21707
+ return DENYLIST_PATTERNS.some((re3) => re3.test(value));
21708
+ }
21709
+ function isEmailLikeInput(el) {
21710
+ if (el.type === "email") return true;
21711
+ const name = (el.name || "").toLowerCase();
21712
+ const id = (el.id || "").toLowerCase();
21713
+ if (/email/.test(name) || /email/.test(id)) return true;
21714
+ return false;
21715
+ }
21716
+ function isOptedOut(el) {
21717
+ return el.closest("[data-syntro-no-identify]") !== null;
21718
+ }
21719
+ function isAnonymous(distinctId) {
21720
+ if (!distinctId) return true;
21721
+ return ANONYMOUS_UUID_RE.test(distinctId);
21722
+ }
21723
+ function installEmailIdentifyListener(telemetry, consentGate) {
21724
+ if (typeof document === "undefined") return () => {
21725
+ };
21726
+ if (!telemetry.identify && !telemetry.alias) return () => {
21727
+ };
21728
+ const seenThisSession = /* @__PURE__ */ new Set();
21729
+ const fireIdentifyOrAlias = (value) => {
21730
+ const currentId = telemetry.getDistinctId?.();
21731
+ if (currentId === value) {
21732
+ seenThisSession.add(value);
21733
+ return;
21734
+ }
21735
+ if (isAnonymous(currentId)) {
21736
+ telemetry.identify?.(value, { email: value });
21737
+ } else {
21738
+ telemetry.alias?.(value);
21739
+ }
21740
+ seenThisSession.add(value);
21741
+ };
21742
+ const handler = (e9) => {
21743
+ const target = e9.target;
21744
+ if (!(target instanceof HTMLInputElement)) return;
21745
+ if (isOptedOut(target)) return;
21746
+ if (!isEmailLikeInput(target)) return;
21747
+ const value = (target.value ?? "").trim().toLowerCase();
21748
+ if (!isValidEmail(value)) return;
21749
+ if (isEmailDenylisted(value)) return;
21750
+ if (seenThisSession.has(value)) return;
21751
+ if (!consentGate) {
21752
+ fireIdentifyOrAlias(value);
21753
+ return;
21754
+ }
21755
+ const status = consentGate.getStatus();
21756
+ if (status === "granted") {
21757
+ fireIdentifyOrAlias(value);
21758
+ return;
21759
+ }
21760
+ if (status === "denied") {
21761
+ return;
21762
+ }
21763
+ consentGate.resolvePending().then((resolved) => {
21764
+ if (resolved === "granted") fireIdentifyOrAlias(value);
21765
+ });
21766
+ };
21767
+ document.addEventListener("change", handler, true);
21768
+ return () => {
21769
+ document.removeEventListener("change", handler, true);
21770
+ seenThisSession.clear();
21771
+ };
21772
+ }
21773
+
21774
+ // src/defaults/identifyOnUrlParam.ts
21775
+ var SYNTRO_UID_PARAM = "_syu";
21776
+ var ANONYMOUS_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21777
+ function isAnonymous2(distinctId) {
21778
+ if (!distinctId) return true;
21779
+ return ANONYMOUS_UUID_RE2.test(distinctId);
21780
+ }
21781
+ function identifyFromUrlParam(telemetry, consentGate) {
21782
+ if (typeof window === "undefined") return;
21783
+ if (!telemetry.identify && !telemetry.alias) return;
21784
+ const value = new URLSearchParams(window.location.search).get(SYNTRO_UID_PARAM);
21785
+ if (!value) return;
21786
+ const trimmed = value.trim();
21787
+ if (!trimmed) return;
21788
+ const fire = () => {
21789
+ const currentId = telemetry.getDistinctId?.();
21790
+ if (currentId === trimmed) return;
21791
+ const properties = isValidEmail(trimmed) ? { email: trimmed } : void 0;
21792
+ if (isAnonymous2(currentId)) {
21793
+ telemetry.identify?.(trimmed, properties);
21794
+ } else {
21795
+ telemetry.alias?.(trimmed);
21796
+ }
21797
+ };
21798
+ if (!consentGate) {
21799
+ fire();
21800
+ return;
21801
+ }
21802
+ if (consentGate.getStatus() === "granted") {
21803
+ fire();
21804
+ return;
21805
+ }
21806
+ let fired = false;
21807
+ const unsub = consentGate.subscribe((status) => {
21808
+ if (fired) return;
21809
+ if (status !== "granted") return;
21810
+ fired = true;
21811
+ unsub();
21812
+ fire();
21813
+ });
21814
+ }
21815
+
21816
+ // src/defaults/initialProperties.ts
21817
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
21818
+ function isStrictRegion(geo) {
21819
+ if (geo.is_eu === "true") return true;
21820
+ if (geo.country_code === "GB" || geo.country_code === "CH") return true;
21821
+ if (!geo.is_eu && !geo.country_code) return true;
21822
+ return false;
21823
+ }
21824
+ function safeReferringDomain(referrer) {
21825
+ if (!referrer) return "";
21826
+ try {
21827
+ return new URL(referrer).hostname;
21828
+ } catch {
21829
+ return "";
21830
+ }
21831
+ }
21832
+ function safeOriginPathname() {
21833
+ if (typeof window === "undefined") return "";
21834
+ return `${window.location.origin}${window.location.pathname}`;
21835
+ }
21836
+ function captureInitialProperties(telemetry, geoPromise) {
21837
+ if (typeof window === "undefined" || typeof document === "undefined") return;
21838
+ if (!telemetry.setPersonPropertiesOnce) return;
21839
+ const referrer = document.referrer || "";
21840
+ const params = new URLSearchParams(window.location.search);
21841
+ const props = {
21842
+ $initial_pathname: window.location.pathname,
21843
+ $initial_referrer: referrer,
21844
+ $initial_referring_domain: safeReferringDomain(referrer)
21845
+ };
21846
+ for (const key of UTM_KEYS) {
21847
+ const value = params.get(key);
21848
+ if (value) {
21849
+ props[`$initial_${key}`] = value;
21850
+ }
21851
+ }
21852
+ telemetry.setPersonPropertiesOnce(props);
21853
+ if (!geoPromise) {
21854
+ telemetry.setPersonPropertiesOnce({ $initial_url: safeOriginPathname() });
21855
+ return;
21856
+ }
21857
+ geoPromise.then((geo) => {
21858
+ const url = isStrictRegion(geo) ? safeOriginPathname() : window.location.href;
21859
+ telemetry.setPersonPropertiesOnce?.({ $initial_url: url });
21860
+ }).catch(() => {
21861
+ telemetry.setPersonPropertiesOnce?.({ $initial_url: safeOriginPathname() });
21862
+ });
21863
+ }
21864
+
21865
+ // src/defaults/jsonLd.ts
21866
+ var PII_KEYS = /* @__PURE__ */ new Set([
21867
+ "email",
21868
+ "telephone",
21869
+ "streetAddress",
21870
+ "addressLocality",
21871
+ "addressRegion",
21872
+ "postalCode",
21873
+ "givenName",
21874
+ "familyName"
21875
+ ]);
21876
+ var MAX_DESCRIPTION_LENGTH = 500;
21877
+ var MAX_IMAGE_ARRAY_LENGTH = 3;
21878
+ function stripPii(obj) {
21879
+ if (Array.isArray(obj)) return obj.map(stripPii);
21880
+ if (obj && typeof obj === "object") {
21881
+ const out = {};
21882
+ for (const [k4, v4] of Object.entries(obj)) {
21883
+ if (PII_KEYS.has(k4)) continue;
21884
+ out[k4] = stripPii(v4);
21885
+ }
21886
+ return out;
21887
+ }
21888
+ return obj;
21889
+ }
21890
+ function stripBloat(obj) {
21891
+ if (Array.isArray(obj)) return obj.map(stripBloat);
21892
+ if (obj && typeof obj === "object") {
21893
+ const out = {};
21894
+ for (const [k4, v4] of Object.entries(obj)) {
21895
+ if (k4 === "description" && typeof v4 === "string" && v4.length > MAX_DESCRIPTION_LENGTH) {
21896
+ continue;
21897
+ }
21898
+ if (k4 === "image" && Array.isArray(v4) && v4.length > MAX_IMAGE_ARRAY_LENGTH) {
21899
+ out[k4] = v4.slice(0, MAX_IMAGE_ARRAY_LENGTH);
21900
+ continue;
21901
+ }
21902
+ out[k4] = stripBloat(v4);
21903
+ }
21904
+ return out;
21905
+ }
21906
+ return obj;
21907
+ }
21908
+ function extractJsonLdBlocks() {
21909
+ if (typeof document === "undefined") return [];
21910
+ return Array.from(document.querySelectorAll('script[type="application/ld+json"]')).filter((el) => !el.hasAttribute("data-syntro-no-jsonld")).map((el) => {
21911
+ try {
21912
+ return JSON.parse(el.textContent ?? "");
21913
+ } catch {
21914
+ return null;
21915
+ }
21916
+ }).filter((parsed) => parsed !== null);
21917
+ }
21918
+ function extractAndRegisterJsonLd(telemetry) {
21919
+ if (!telemetry.register) return;
21920
+ const blocks = extractJsonLdBlocks().map(stripBloat).map(stripPii);
21921
+ if (blocks.length === 0) return;
21922
+ telemetry.register({ json_ld: blocks });
21923
+ }
21924
+
21925
+ // src/defaults/index.ts
21926
+ function installDefaults(telemetry, consentGate, options = {}) {
21927
+ captureInitialProperties(telemetry, options.geoPromise);
21928
+ promoteClickIds(telemetry);
21929
+ extractAndRegisterJsonLd(telemetry);
21930
+ identifyFromUrlParam(telemetry, consentGate);
21931
+ const teardownEmail = installEmailIdentifyListener(telemetry, consentGate);
21932
+ return () => {
21933
+ teardownEmail();
21934
+ };
21935
+ }
21936
+
21643
21937
  // src/events/validation.ts
21644
21938
  var APP_PREFIX = "app:";
21645
21939
  var RESERVED_PREFIX = "syntro:";
@@ -27253,6 +27547,344 @@ ${cssRules}
27253
27547
  return runtime5;
27254
27548
  }
27255
27549
 
27550
+ // src/telemetry/consent/adapters/gtmConsentMode.ts
27551
+ function mapStorage(value) {
27552
+ if (value === "granted") return "granted";
27553
+ if (value === "denied") return "denied";
27554
+ return "pending";
27555
+ }
27556
+ function extractAnalyticsStorage(entry) {
27557
+ if (!entry) return null;
27558
+ if (Array.isArray(entry) && entry[0] === "consent") {
27559
+ const payload = entry[2];
27560
+ return payload?.analytics_storage ?? null;
27561
+ }
27562
+ if (typeof entry === "object" && entry.event === "consent_update") {
27563
+ const payload = entry;
27564
+ return payload.analytics_storage ?? null;
27565
+ }
27566
+ return null;
27567
+ }
27568
+ var GtmConsentModeAdapter = class {
27569
+ constructor() {
27570
+ this.name = "gtm-consent-mode-v2";
27571
+ }
27572
+ detect() {
27573
+ if (typeof window === "undefined") return false;
27574
+ const dl = window.dataLayer;
27575
+ if (!Array.isArray(dl)) return false;
27576
+ for (const entry of dl) {
27577
+ if (extractAnalyticsStorage(entry) !== null) return true;
27578
+ }
27579
+ return false;
27580
+ }
27581
+ initialize(callback) {
27582
+ if (typeof window === "undefined") {
27583
+ callback("pending");
27584
+ return () => {
27585
+ };
27586
+ }
27587
+ if (!Array.isArray(window.dataLayer)) {
27588
+ window.dataLayer = [];
27589
+ }
27590
+ callback(this.readCurrentState());
27591
+ const dl = window.dataLayer;
27592
+ const original = dl.push.bind(dl);
27593
+ const wrappedPush = (...args) => {
27594
+ const result = original(...args);
27595
+ for (const arg of args) {
27596
+ const storage = extractAnalyticsStorage(arg);
27597
+ if (storage !== null) {
27598
+ callback(mapStorage(storage));
27599
+ }
27600
+ }
27601
+ return result;
27602
+ };
27603
+ dl.push = wrappedPush;
27604
+ return () => {
27605
+ if (dl.push === wrappedPush) {
27606
+ dl.push = original;
27607
+ }
27608
+ };
27609
+ }
27610
+ readCurrentState() {
27611
+ if (typeof window === "undefined") return "pending";
27612
+ const dl = window.dataLayer;
27613
+ if (!Array.isArray(dl)) return "pending";
27614
+ for (let i9 = dl.length - 1; i9 >= 0; i9--) {
27615
+ const storage = extractAnalyticsStorage(dl[i9]);
27616
+ if (storage !== null) return mapStorage(storage);
27617
+ }
27618
+ return "pending";
27619
+ }
27620
+ };
27621
+
27622
+ // src/telemetry/consent/adapters/iabTcf.ts
27623
+ var PURPOSE_STORAGE = "1";
27624
+ var PURPOSE_ANALYTICS = "8";
27625
+ var IabTcfAdapter = class {
27626
+ constructor() {
27627
+ this.name = "iab-tcf-v2";
27628
+ }
27629
+ detect() {
27630
+ if (typeof window === "undefined") return false;
27631
+ return typeof window.__tcfapi === "function";
27632
+ }
27633
+ initialize(callback) {
27634
+ callback("pending");
27635
+ if (typeof window === "undefined") return () => {
27636
+ };
27637
+ const tcfapi = window.__tcfapi;
27638
+ if (typeof tcfapi !== "function") return () => {
27639
+ };
27640
+ let listenerId;
27641
+ tcfapi("addEventListener", 2, (tcData, success) => {
27642
+ if (!success || !tcData) return;
27643
+ if (listenerId === void 0 && typeof tcData.listenerId === "number") {
27644
+ listenerId = tcData.listenerId;
27645
+ }
27646
+ const ready = tcData.eventStatus === "tcloaded" || tcData.eventStatus === "useractioncomplete";
27647
+ if (!ready) return;
27648
+ if (tcData.gdprApplies === false) {
27649
+ callback("granted");
27650
+ return;
27651
+ }
27652
+ const consents = tcData.purpose?.consents ?? {};
27653
+ const p1 = consents[PURPOSE_STORAGE] === true;
27654
+ const p8 = consents[PURPOSE_ANALYTICS] === true;
27655
+ callback(p1 && p8 ? "granted" : "denied");
27656
+ });
27657
+ return () => {
27658
+ if (listenerId !== void 0) {
27659
+ try {
27660
+ tcfapi("removeEventListener", 2, () => {
27661
+ }, listenerId);
27662
+ } catch {
27663
+ }
27664
+ }
27665
+ };
27666
+ }
27667
+ };
27668
+
27669
+ // src/telemetry/consent/adapters/shopify.ts
27670
+ var VISITOR_CONSENT_EVENT = "visitorConsentCollected";
27671
+ var ShopifyCustomerPrivacyAdapter = class {
27672
+ constructor() {
27673
+ this.name = "shopify-customer-privacy";
27674
+ }
27675
+ detect() {
27676
+ if (typeof window === "undefined") return false;
27677
+ const shopify = window.Shopify;
27678
+ return typeof shopify?.loadFeatures === "function";
27679
+ }
27680
+ initialize(callback) {
27681
+ callback("pending");
27682
+ if (typeof window === "undefined") return () => {
27683
+ };
27684
+ const shopify = window.Shopify;
27685
+ if (!shopify?.loadFeatures) return () => {
27686
+ };
27687
+ let visitorEventHandler;
27688
+ shopify.loadFeatures([{ name: "consent-tracking-api", version: "0.1" }], (error2) => {
27689
+ if (error2) {
27690
+ return;
27691
+ }
27692
+ const allowed = shopify.customerPrivacy?.userCanBeTracked() ?? true;
27693
+ callback(allowed ? "granted" : "denied");
27694
+ visitorEventHandler = (event) => {
27695
+ const detail = event.detail;
27696
+ callback(detail?.analyticsAllowed ? "granted" : "denied");
27697
+ };
27698
+ document.addEventListener(VISITOR_CONSENT_EVENT, visitorEventHandler);
27699
+ });
27700
+ return () => {
27701
+ if (visitorEventHandler) {
27702
+ document.removeEventListener(VISITOR_CONSENT_EVENT, visitorEventHandler);
27703
+ }
27704
+ };
27705
+ }
27706
+ };
27707
+
27708
+ // src/telemetry/consent/ConsentDetector.ts
27709
+ var ConsentDetector = class {
27710
+ constructor(options) {
27711
+ this.options = options;
27712
+ }
27713
+ /**
27714
+ * Start the detector. Probes adapters in array order; first match wins.
27715
+ * Returns a teardown function that unsubscribes the active adapter.
27716
+ *
27717
+ * Async because adapters may need async initialization. Bootstrap
27718
+ * should NOT await this — feature flags and anonymous telemetry
27719
+ * should keep flowing while the detector probes.
27720
+ */
27721
+ async start(gate) {
27722
+ for (const adapter of this.options.adapters) {
27723
+ let detected = false;
27724
+ try {
27725
+ detected = adapter.detect();
27726
+ } catch {
27727
+ continue;
27728
+ }
27729
+ if (!detected) continue;
27730
+ try {
27731
+ const teardown = adapter.initialize((status) => gate.setStatus(status));
27732
+ this.options.onAdapterSelected?.(adapter.name);
27733
+ return teardown;
27734
+ } catch (err) {
27735
+ console.warn(`[Syntro consent] Adapter "${adapter.name}" failed:`, err);
27736
+ }
27737
+ }
27738
+ this.options.onAdapterSelected?.(null);
27739
+ const fallback = await this.resolveFallback();
27740
+ gate.setStatus(fallback);
27741
+ return () => {
27742
+ };
27743
+ }
27744
+ async resolveFallback() {
27745
+ if (!this.options.fallbackStatus) return "pending";
27746
+ try {
27747
+ return await this.options.fallbackStatus();
27748
+ } catch {
27749
+ return "pending";
27750
+ }
27751
+ }
27752
+ };
27753
+
27754
+ // src/telemetry/consent/ConsentGate.ts
27755
+ var ConsentGate = class {
27756
+ constructor(config = {}) {
27757
+ this.listeners = /* @__PURE__ */ new Set();
27758
+ this.destroyed = false;
27759
+ this.status = config.initialStatus ?? "pending";
27760
+ this.configCallback = config.onConsentChange;
27761
+ this.gtmEnabled = config.readFromGtmConsentMode ?? false;
27762
+ this.pendingResolverFn = config.pendingResolver;
27763
+ }
27764
+ /**
27765
+ * Grant consent — enables telemetry collection.
27766
+ */
27767
+ grant() {
27768
+ this.setStatus("granted");
27769
+ }
27770
+ /**
27771
+ * Deny consent — disables telemetry collection.
27772
+ */
27773
+ deny() {
27774
+ this.setStatus("denied");
27775
+ }
27776
+ /**
27777
+ * Get the current consent status.
27778
+ */
27779
+ getStatus() {
27780
+ return this.status;
27781
+ }
27782
+ /**
27783
+ * Subscribe to consent status changes.
27784
+ * Returns an unsubscribe function.
27785
+ */
27786
+ subscribe(listener) {
27787
+ this.listeners.add(listener);
27788
+ return () => {
27789
+ this.listeners.delete(listener);
27790
+ };
27791
+ }
27792
+ /**
27793
+ * Subscribe to the next definitive (granted | denied) status change,
27794
+ * then auto-unsubscribe. If status is already definitive, fires
27795
+ * synchronously (next microtask) with current status.
27796
+ *
27797
+ * Used by D-2 (URL-param identify) to defer the identify() call
27798
+ * until consent is decided.
27799
+ */
27800
+ subscribeOnce(listener) {
27801
+ if (this.status !== "pending") {
27802
+ queueMicrotask(() => listener(this.status));
27803
+ return () => {
27804
+ };
27805
+ }
27806
+ const unsub = this.subscribe((status) => {
27807
+ if (status === "pending") return;
27808
+ unsub();
27809
+ listener(status);
27810
+ });
27811
+ return unsub;
27812
+ }
27813
+ /**
27814
+ * Resolve what the current 'pending' state should be treated as,
27815
+ * via the configured pendingResolver. Returns the current status
27816
+ * directly if it's already 'granted' or 'denied'.
27817
+ *
27818
+ * Defaults to 'denied' if no resolver is configured (conservative).
27819
+ */
27820
+ async resolvePending() {
27821
+ if (this.status !== "pending") return this.status;
27822
+ if (!this.pendingResolverFn) return "denied";
27823
+ try {
27824
+ return await this.pendingResolverFn();
27825
+ } catch {
27826
+ return "denied";
27827
+ }
27828
+ }
27829
+ /**
27830
+ * Public setter — used by ConsentDetector / adapters to push state changes.
27831
+ */
27832
+ setStatus(newStatus) {
27833
+ this.applyStatus(newStatus);
27834
+ }
27835
+ /**
27836
+ * Poll GTM's dataLayer for consent state.
27837
+ * Scans for the most recent consent update event with analytics_storage.
27838
+ */
27839
+ pollGtmConsent() {
27840
+ if (!this.gtmEnabled || typeof window === "undefined") return;
27841
+ const dataLayer = window.dataLayer;
27842
+ if (!Array.isArray(dataLayer)) return;
27843
+ for (let i9 = dataLayer.length - 1; i9 >= 0; i9--) {
27844
+ const entry = dataLayer[i9];
27845
+ if (!entry) continue;
27846
+ const isConsentUpdate = entry[0] === "consent" && entry[1] === "update" && entry[2];
27847
+ if (isConsentUpdate) {
27848
+ const storage = entry[2].analytics_storage;
27849
+ if (storage === "granted") {
27850
+ this.setStatus("granted");
27851
+ } else if (storage === "denied") {
27852
+ this.setStatus("denied");
27853
+ }
27854
+ return;
27855
+ }
27856
+ }
27857
+ }
27858
+ /**
27859
+ * Clean up listeners and stop responding to changes.
27860
+ */
27861
+ destroy() {
27862
+ this.destroyed = true;
27863
+ this.listeners.clear();
27864
+ }
27865
+ // ─── Private ─────────────────────────────────────────────────────
27866
+ applyStatus(newStatus) {
27867
+ if (this.destroyed) return;
27868
+ if (this.status === newStatus) return;
27869
+ this.status = newStatus;
27870
+ this.notify(newStatus);
27871
+ }
27872
+ notify(status) {
27873
+ if (this.configCallback) {
27874
+ try {
27875
+ this.configCallback(status);
27876
+ } catch {
27877
+ }
27878
+ }
27879
+ for (const listener of this.listeners) {
27880
+ try {
27881
+ listener(status);
27882
+ } catch {
27883
+ }
27884
+ }
27885
+ }
27886
+ };
27887
+
27256
27888
  // src/telemetry/InterventionTracker.ts
27257
27889
  var InterventionTracker = class {
27258
27890
  constructor(telemetry, variantId) {
@@ -27348,6 +27980,14 @@ ${cssRules}
27348
27980
  }
27349
27981
  track(_eventName, _properties) {
27350
27982
  }
27983
+ identify(_distinctId, _properties) {
27984
+ }
27985
+ alias(_newDistinctId, _oldDistinctId) {
27986
+ }
27987
+ optInCapturing() {
27988
+ }
27989
+ optOutCapturing() {
27990
+ }
27351
27991
  };
27352
27992
  function createNoopClient() {
27353
27993
  return new NoopAdapter();
@@ -32286,6 +32926,9 @@ ${cssRules}
32286
32926
  // Enable web vitals
32287
32927
  enable_recording_console_log: true
32288
32928
  };
32929
+ if (options.cookieless_mode) {
32930
+ initOptions.cookieless_mode = options.cookieless_mode;
32931
+ }
32289
32932
  const result = Jo.init(
32290
32933
  options.apiKey,
32291
32934
  initOptions,
@@ -32390,6 +33033,22 @@ ${cssRules}
32390
33033
  alias(id, aliasId) {
32391
33034
  this.client?.alias(id, aliasId);
32392
33035
  }
33036
+ /**
33037
+ * Opt the current visitor INTO capturing. Drives the granted-state
33038
+ * transition from the consent gate. When previously cookieless or
33039
+ * opted-out, switches to full identified capture.
33040
+ */
33041
+ optInCapturing() {
33042
+ this.client?.opt_in_capturing();
33043
+ }
33044
+ /**
33045
+ * Opt the current visitor OUT of capturing. Drives the denied-state
33046
+ * transition from the consent gate. Stops sending events; existing
33047
+ * cookieless/anonymous events remain in the data warehouse.
33048
+ */
33049
+ optOutCapturing() {
33050
+ this.client?.opt_out_capturing();
33051
+ }
32393
33052
  track(eventName, payload) {
32394
33053
  this.client?.capture(eventName, payload);
32395
33054
  }
@@ -32648,6 +33307,11 @@ ${cssRules}
32648
33307
  // Enable PostHog feature flags for segment membership
32649
33308
  enableFeatureFlags: true,
32650
33309
  sessionRecording: true,
33310
+ // Cookieless mode: anonymous baseline until consent decision.
33311
+ // PostHog captures behavioral data with privacy-preserving session
33312
+ // hashes during pending; switches to cookied tracking on grant;
33313
+ // continues cookielessly on reject.
33314
+ cookieless_mode: "on_reject",
32651
33315
  // Wire up callback for when flags are loaded (Phase 2)
32652
33316
  onFeatureFlagsLoaded,
32653
33317
  // Wire up event capture to feed into event processor
@@ -32668,6 +33332,51 @@ ${cssRules}
32668
33332
  events.setPosthogCapture((name, props) => {
32669
33333
  telemetryForCapture.track?.(name, props);
32670
33334
  });
33335
+ const telemetryForGate = telemetry;
33336
+ const resolveGeoStatus = async () => {
33337
+ const geo = await geoPromise;
33338
+ const isStrictRegion2 = geo.is_eu === "true" || geo.country_code === "GB" || geo.country_code === "CH";
33339
+ return isStrictRegion2 ? "denied" : "granted";
33340
+ };
33341
+ const consentGate = new ConsentGate({
33342
+ initialStatus: "pending",
33343
+ pendingResolver: resolveGeoStatus,
33344
+ // Drive PostHog opt-in/opt-out from gate transitions.
33345
+ onConsentChange: (status) => {
33346
+ if (status === "granted") {
33347
+ telemetryForGate.optInCapturing?.();
33348
+ } else if (status === "denied") {
33349
+ telemetryForGate.optOutCapturing?.();
33350
+ }
33351
+ }
33352
+ });
33353
+ try {
33354
+ installDefaults(telemetry, consentGate, { geoPromise });
33355
+ debug("Syntro Bootstrap", "SDK defaults installed (D-1..D-5) with consent gate");
33356
+ } catch (err) {
33357
+ warn("Syntro Bootstrap", "installDefaults failed", err);
33358
+ }
33359
+ try {
33360
+ const detector = new ConsentDetector({
33361
+ adapters: [
33362
+ new ShopifyCustomerPrivacyAdapter(),
33363
+ new IabTcfAdapter(),
33364
+ new GtmConsentModeAdapter()
33365
+ ],
33366
+ fallbackStatus: resolveGeoStatus,
33367
+ onAdapterSelected: (name) => {
33368
+ telemetryForGate.track?.("syntro_consent_adapter_selected", {
33369
+ adapter: name ?? "none"
33370
+ });
33371
+ debug("Syntro Bootstrap", `Consent adapter selected: ${name ?? "none"}`);
33372
+ }
33373
+ });
33374
+ detector.start(consentGate).catch((err) => {
33375
+ warn("Syntro Bootstrap", "Consent detector failed to start", err);
33376
+ });
33377
+ } catch (err) {
33378
+ warn("Syntro Bootstrap", "Consent detector setup failed", err);
33379
+ }
32671
33380
  if (platformAdapter?.name === "shopify" && telemetryHost) {
32672
33381
  try {
32673
33382
  shopifyPixelBridge = new ShopifyPixelBridge({
@@ -33381,7 +34090,7 @@ ${cssRules}
33381
34090
  }
33382
34091
 
33383
34092
  // src/index.ts
33384
- var RUNTIME_SDK_BUILD = true ? `${"2026-05-04T20:14:50.300Z"} (${"c39b614e594"})` : "dev";
34093
+ var RUNTIME_SDK_BUILD = true ? `${"2026-05-05T03:23:35.905Z"} (${"b84ed35ba47"})` : "dev";
33385
34094
  if (typeof window !== "undefined") {
33386
34095
  console.log(`[Syntro Runtime] Build: ${RUNTIME_SDK_BUILD} (Lit)`);
33387
34096
  const existing = window.SynOS;