@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.
package/dist/index.js CHANGED
@@ -9577,7 +9577,7 @@ function error(prefix, message, data) {
9577
9577
  }
9578
9578
 
9579
9579
  // src/version.ts
9580
- var SDK_VERSION = "2.8.0-canary.183";
9580
+ var SDK_VERSION = "2.8.0-canary.185";
9581
9581
 
9582
9582
  // src/types.ts
9583
9583
  var SDK_SCHEMA_VERSION = "2.0";
@@ -13916,6 +13916,306 @@ function getDerivedCdnBase() {
13916
13916
  return derivedCdnBase;
13917
13917
  }
13918
13918
 
13919
+ // src/defaults/clickIds.ts
13920
+ var CLICK_ID_KEYS = [
13921
+ "fbclid",
13922
+ // Meta (Facebook + Instagram)
13923
+ "ttclid",
13924
+ // TikTok
13925
+ "gclid",
13926
+ // Google Ads
13927
+ "gbraid",
13928
+ // Google Ads (iOS app web)
13929
+ "wbraid",
13930
+ // Google Ads (web app)
13931
+ "msclkid",
13932
+ // Microsoft / Bing Ads
13933
+ "twclid",
13934
+ // X / Twitter
13935
+ "li_fat_id",
13936
+ // LinkedIn
13937
+ "epik",
13938
+ // Pinterest
13939
+ "ScCid",
13940
+ // Snapchat
13941
+ "yclid",
13942
+ // Yandex
13943
+ "_kx",
13944
+ // Klaviyo
13945
+ "mc_eid",
13946
+ // Mailchimp recipient
13947
+ "mc_cid"
13948
+ // Mailchimp campaign
13949
+ ];
13950
+ function promoteClickIds(telemetry) {
13951
+ if (typeof window === "undefined") return;
13952
+ if (!telemetry.setPersonPropertiesOnce) return;
13953
+ const params = new URLSearchParams(window.location.search);
13954
+ const captured = {};
13955
+ for (const key of CLICK_ID_KEYS) {
13956
+ const value = params.get(key);
13957
+ if (value) {
13958
+ captured[`$initial_${key}`] = value;
13959
+ }
13960
+ }
13961
+ if (Object.keys(captured).length > 0) {
13962
+ telemetry.setPersonPropertiesOnce(captured);
13963
+ }
13964
+ }
13965
+
13966
+ // src/defaults/identifyOnEmail.ts
13967
+ 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;
13968
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13969
+ var DENYLIST_PATTERNS = [
13970
+ /@yopmail\.com$/i,
13971
+ /@mailinator\.com$/i,
13972
+ /@guerrillamail\.(com|info|net|org|biz)$/i,
13973
+ /@10minutemail\.com$/i,
13974
+ /^test@/i,
13975
+ /^noreply@/i,
13976
+ /^no-reply@/i
13977
+ ];
13978
+ function isValidEmail(value) {
13979
+ if (!value) return false;
13980
+ return EMAIL_RE.test(value);
13981
+ }
13982
+ function isEmailDenylisted(value) {
13983
+ return DENYLIST_PATTERNS.some((re2) => re2.test(value));
13984
+ }
13985
+ function isEmailLikeInput(el) {
13986
+ if (el.type === "email") return true;
13987
+ const name = (el.name || "").toLowerCase();
13988
+ const id = (el.id || "").toLowerCase();
13989
+ if (/email/.test(name) || /email/.test(id)) return true;
13990
+ return false;
13991
+ }
13992
+ function isOptedOut(el) {
13993
+ return el.closest("[data-syntro-no-identify]") !== null;
13994
+ }
13995
+ function isAnonymous(distinctId) {
13996
+ if (!distinctId) return true;
13997
+ return ANONYMOUS_UUID_RE.test(distinctId);
13998
+ }
13999
+ function installEmailIdentifyListener(telemetry, consentGate) {
14000
+ if (typeof document === "undefined") return () => {
14001
+ };
14002
+ if (!telemetry.identify && !telemetry.alias) return () => {
14003
+ };
14004
+ const seenThisSession = /* @__PURE__ */ new Set();
14005
+ const fireIdentifyOrAlias = (value) => {
14006
+ var _a3, _b, _c;
14007
+ const currentId = (_a3 = telemetry.getDistinctId) == null ? void 0 : _a3.call(telemetry);
14008
+ if (currentId === value) {
14009
+ seenThisSession.add(value);
14010
+ return;
14011
+ }
14012
+ if (isAnonymous(currentId)) {
14013
+ (_b = telemetry.identify) == null ? void 0 : _b.call(telemetry, value, { email: value });
14014
+ } else {
14015
+ (_c = telemetry.alias) == null ? void 0 : _c.call(telemetry, value);
14016
+ }
14017
+ seenThisSession.add(value);
14018
+ };
14019
+ const handler = (e) => {
14020
+ var _a3;
14021
+ const target = e.target;
14022
+ if (!(target instanceof HTMLInputElement)) return;
14023
+ if (isOptedOut(target)) return;
14024
+ if (!isEmailLikeInput(target)) return;
14025
+ const value = ((_a3 = target.value) != null ? _a3 : "").trim().toLowerCase();
14026
+ if (!isValidEmail(value)) return;
14027
+ if (isEmailDenylisted(value)) return;
14028
+ if (seenThisSession.has(value)) return;
14029
+ if (!consentGate) {
14030
+ fireIdentifyOrAlias(value);
14031
+ return;
14032
+ }
14033
+ const status = consentGate.getStatus();
14034
+ if (status === "granted") {
14035
+ fireIdentifyOrAlias(value);
14036
+ return;
14037
+ }
14038
+ if (status === "denied") {
14039
+ return;
14040
+ }
14041
+ consentGate.resolvePending().then((resolved) => {
14042
+ if (resolved === "granted") fireIdentifyOrAlias(value);
14043
+ });
14044
+ };
14045
+ document.addEventListener("change", handler, true);
14046
+ return () => {
14047
+ document.removeEventListener("change", handler, true);
14048
+ seenThisSession.clear();
14049
+ };
14050
+ }
14051
+
14052
+ // src/defaults/identifyOnUrlParam.ts
14053
+ var SYNTRO_UID_PARAM = "_syu";
14054
+ 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;
14055
+ function isAnonymous2(distinctId) {
14056
+ if (!distinctId) return true;
14057
+ return ANONYMOUS_UUID_RE2.test(distinctId);
14058
+ }
14059
+ function identifyFromUrlParam(telemetry, consentGate) {
14060
+ if (typeof window === "undefined") return;
14061
+ if (!telemetry.identify && !telemetry.alias) return;
14062
+ const value = new URLSearchParams(window.location.search).get(SYNTRO_UID_PARAM);
14063
+ if (!value) return;
14064
+ const trimmed = value.trim();
14065
+ if (!trimmed) return;
14066
+ const fire = () => {
14067
+ var _a3, _b, _c;
14068
+ const currentId = (_a3 = telemetry.getDistinctId) == null ? void 0 : _a3.call(telemetry);
14069
+ if (currentId === trimmed) return;
14070
+ const properties = isValidEmail(trimmed) ? { email: trimmed } : void 0;
14071
+ if (isAnonymous2(currentId)) {
14072
+ (_b = telemetry.identify) == null ? void 0 : _b.call(telemetry, trimmed, properties);
14073
+ } else {
14074
+ (_c = telemetry.alias) == null ? void 0 : _c.call(telemetry, trimmed);
14075
+ }
14076
+ };
14077
+ if (!consentGate) {
14078
+ fire();
14079
+ return;
14080
+ }
14081
+ if (consentGate.getStatus() === "granted") {
14082
+ fire();
14083
+ return;
14084
+ }
14085
+ let fired = false;
14086
+ const unsub = consentGate.subscribe((status) => {
14087
+ if (fired) return;
14088
+ if (status !== "granted") return;
14089
+ fired = true;
14090
+ unsub();
14091
+ fire();
14092
+ });
14093
+ }
14094
+
14095
+ // src/defaults/initialProperties.ts
14096
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
14097
+ function isStrictRegion(geo) {
14098
+ if (geo.is_eu === "true") return true;
14099
+ if (geo.country_code === "GB" || geo.country_code === "CH") return true;
14100
+ if (!geo.is_eu && !geo.country_code) return true;
14101
+ return false;
14102
+ }
14103
+ function safeReferringDomain(referrer) {
14104
+ if (!referrer) return "";
14105
+ try {
14106
+ return new URL(referrer).hostname;
14107
+ } catch {
14108
+ return "";
14109
+ }
14110
+ }
14111
+ function safeOriginPathname() {
14112
+ if (typeof window === "undefined") return "";
14113
+ return `${window.location.origin}${window.location.pathname}`;
14114
+ }
14115
+ function captureInitialProperties(telemetry, geoPromise) {
14116
+ if (typeof window === "undefined" || typeof document === "undefined") return;
14117
+ if (!telemetry.setPersonPropertiesOnce) return;
14118
+ const referrer = document.referrer || "";
14119
+ const params = new URLSearchParams(window.location.search);
14120
+ const props = {
14121
+ $initial_pathname: window.location.pathname,
14122
+ $initial_referrer: referrer,
14123
+ $initial_referring_domain: safeReferringDomain(referrer)
14124
+ };
14125
+ for (const key of UTM_KEYS) {
14126
+ const value = params.get(key);
14127
+ if (value) {
14128
+ props[`$initial_${key}`] = value;
14129
+ }
14130
+ }
14131
+ telemetry.setPersonPropertiesOnce(props);
14132
+ if (!geoPromise) {
14133
+ telemetry.setPersonPropertiesOnce({ $initial_url: safeOriginPathname() });
14134
+ return;
14135
+ }
14136
+ geoPromise.then((geo) => {
14137
+ var _a3;
14138
+ const url = isStrictRegion(geo) ? safeOriginPathname() : window.location.href;
14139
+ (_a3 = telemetry.setPersonPropertiesOnce) == null ? void 0 : _a3.call(telemetry, { $initial_url: url });
14140
+ }).catch(() => {
14141
+ var _a3;
14142
+ (_a3 = telemetry.setPersonPropertiesOnce) == null ? void 0 : _a3.call(telemetry, { $initial_url: safeOriginPathname() });
14143
+ });
14144
+ }
14145
+
14146
+ // src/defaults/jsonLd.ts
14147
+ var PII_KEYS = /* @__PURE__ */ new Set([
14148
+ "email",
14149
+ "telephone",
14150
+ "streetAddress",
14151
+ "addressLocality",
14152
+ "addressRegion",
14153
+ "postalCode",
14154
+ "givenName",
14155
+ "familyName"
14156
+ ]);
14157
+ var MAX_DESCRIPTION_LENGTH = 500;
14158
+ var MAX_IMAGE_ARRAY_LENGTH = 3;
14159
+ function stripPii(obj) {
14160
+ if (Array.isArray(obj)) return obj.map(stripPii);
14161
+ if (obj && typeof obj === "object") {
14162
+ const out = {};
14163
+ for (const [k2, v2] of Object.entries(obj)) {
14164
+ if (PII_KEYS.has(k2)) continue;
14165
+ out[k2] = stripPii(v2);
14166
+ }
14167
+ return out;
14168
+ }
14169
+ return obj;
14170
+ }
14171
+ function stripBloat(obj) {
14172
+ if (Array.isArray(obj)) return obj.map(stripBloat);
14173
+ if (obj && typeof obj === "object") {
14174
+ const out = {};
14175
+ for (const [k2, v2] of Object.entries(obj)) {
14176
+ if (k2 === "description" && typeof v2 === "string" && v2.length > MAX_DESCRIPTION_LENGTH) {
14177
+ continue;
14178
+ }
14179
+ if (k2 === "image" && Array.isArray(v2) && v2.length > MAX_IMAGE_ARRAY_LENGTH) {
14180
+ out[k2] = v2.slice(0, MAX_IMAGE_ARRAY_LENGTH);
14181
+ continue;
14182
+ }
14183
+ out[k2] = stripBloat(v2);
14184
+ }
14185
+ return out;
14186
+ }
14187
+ return obj;
14188
+ }
14189
+ function extractJsonLdBlocks() {
14190
+ if (typeof document === "undefined") return [];
14191
+ return Array.from(document.querySelectorAll('script[type="application/ld+json"]')).filter((el) => !el.hasAttribute("data-syntro-no-jsonld")).map((el) => {
14192
+ var _a3;
14193
+ try {
14194
+ return JSON.parse((_a3 = el.textContent) != null ? _a3 : "");
14195
+ } catch {
14196
+ return null;
14197
+ }
14198
+ }).filter((parsed) => parsed !== null);
14199
+ }
14200
+ function extractAndRegisterJsonLd(telemetry) {
14201
+ if (!telemetry.register) return;
14202
+ const blocks = extractJsonLdBlocks().map(stripBloat).map(stripPii);
14203
+ if (blocks.length === 0) return;
14204
+ telemetry.register({ json_ld: blocks });
14205
+ }
14206
+
14207
+ // src/defaults/index.ts
14208
+ function installDefaults(telemetry, consentGate, options = {}) {
14209
+ captureInitialProperties(telemetry, options.geoPromise);
14210
+ promoteClickIds(telemetry);
14211
+ extractAndRegisterJsonLd(telemetry);
14212
+ identifyFromUrlParam(telemetry, consentGate);
14213
+ const teardownEmail = installEmailIdentifyListener(telemetry, consentGate);
14214
+ return () => {
14215
+ teardownEmail();
14216
+ };
14217
+ }
14218
+
13919
14219
  // src/events/validation.ts
13920
14220
  var APP_PREFIX = "app:";
13921
14221
  var RESERVED_PREFIX = "syntro:";
@@ -16879,6 +17179,353 @@ function createSmartCanvasRuntime(options = {}) {
16879
17179
  return runtime5;
16880
17180
  }
16881
17181
 
17182
+ // src/telemetry/consent/adapters/gtmConsentMode.ts
17183
+ function mapStorage(value) {
17184
+ if (value === "granted") return "granted";
17185
+ if (value === "denied") return "denied";
17186
+ return "pending";
17187
+ }
17188
+ function extractAnalyticsStorage(entry) {
17189
+ var _a3, _b;
17190
+ if (!entry) return null;
17191
+ if (Array.isArray(entry) && entry[0] === "consent") {
17192
+ const payload = entry[2];
17193
+ return (_a3 = payload == null ? void 0 : payload.analytics_storage) != null ? _a3 : null;
17194
+ }
17195
+ if (typeof entry === "object" && entry.event === "consent_update") {
17196
+ const payload = entry;
17197
+ return (_b = payload.analytics_storage) != null ? _b : null;
17198
+ }
17199
+ return null;
17200
+ }
17201
+ var GtmConsentModeAdapter = class {
17202
+ constructor() {
17203
+ __publicField(this, "name", "gtm-consent-mode-v2");
17204
+ }
17205
+ detect() {
17206
+ if (typeof window === "undefined") return false;
17207
+ const dl = window.dataLayer;
17208
+ if (!Array.isArray(dl)) return false;
17209
+ for (const entry of dl) {
17210
+ if (extractAnalyticsStorage(entry) !== null) return true;
17211
+ }
17212
+ return false;
17213
+ }
17214
+ initialize(callback) {
17215
+ if (typeof window === "undefined") {
17216
+ callback("pending");
17217
+ return () => {
17218
+ };
17219
+ }
17220
+ if (!Array.isArray(window.dataLayer)) {
17221
+ window.dataLayer = [];
17222
+ }
17223
+ callback(this.readCurrentState());
17224
+ const dl = window.dataLayer;
17225
+ const original = dl.push.bind(dl);
17226
+ const wrappedPush = (...args) => {
17227
+ const result = original(...args);
17228
+ for (const arg of args) {
17229
+ const storage = extractAnalyticsStorage(arg);
17230
+ if (storage !== null) {
17231
+ callback(mapStorage(storage));
17232
+ }
17233
+ }
17234
+ return result;
17235
+ };
17236
+ dl.push = wrappedPush;
17237
+ return () => {
17238
+ if (dl.push === wrappedPush) {
17239
+ dl.push = original;
17240
+ }
17241
+ };
17242
+ }
17243
+ readCurrentState() {
17244
+ if (typeof window === "undefined") return "pending";
17245
+ const dl = window.dataLayer;
17246
+ if (!Array.isArray(dl)) return "pending";
17247
+ for (let i = dl.length - 1; i >= 0; i--) {
17248
+ const storage = extractAnalyticsStorage(dl[i]);
17249
+ if (storage !== null) return mapStorage(storage);
17250
+ }
17251
+ return "pending";
17252
+ }
17253
+ };
17254
+
17255
+ // src/telemetry/consent/adapters/iabTcf.ts
17256
+ var PURPOSE_STORAGE = "1";
17257
+ var PURPOSE_ANALYTICS = "8";
17258
+ var IabTcfAdapter = class {
17259
+ constructor() {
17260
+ __publicField(this, "name", "iab-tcf-v2");
17261
+ }
17262
+ detect() {
17263
+ if (typeof window === "undefined") return false;
17264
+ return typeof window.__tcfapi === "function";
17265
+ }
17266
+ initialize(callback) {
17267
+ callback("pending");
17268
+ if (typeof window === "undefined") return () => {
17269
+ };
17270
+ const tcfapi = window.__tcfapi;
17271
+ if (typeof tcfapi !== "function") return () => {
17272
+ };
17273
+ let listenerId;
17274
+ tcfapi("addEventListener", 2, (tcData, success) => {
17275
+ var _a3, _b;
17276
+ if (!success || !tcData) return;
17277
+ if (listenerId === void 0 && typeof tcData.listenerId === "number") {
17278
+ listenerId = tcData.listenerId;
17279
+ }
17280
+ const ready = tcData.eventStatus === "tcloaded" || tcData.eventStatus === "useractioncomplete";
17281
+ if (!ready) return;
17282
+ if (tcData.gdprApplies === false) {
17283
+ callback("granted");
17284
+ return;
17285
+ }
17286
+ const consents = (_b = (_a3 = tcData.purpose) == null ? void 0 : _a3.consents) != null ? _b : {};
17287
+ const p1 = consents[PURPOSE_STORAGE] === true;
17288
+ const p8 = consents[PURPOSE_ANALYTICS] === true;
17289
+ callback(p1 && p8 ? "granted" : "denied");
17290
+ });
17291
+ return () => {
17292
+ if (listenerId !== void 0) {
17293
+ try {
17294
+ tcfapi("removeEventListener", 2, () => {
17295
+ }, listenerId);
17296
+ } catch {
17297
+ }
17298
+ }
17299
+ };
17300
+ }
17301
+ };
17302
+
17303
+ // src/telemetry/consent/adapters/shopify.ts
17304
+ var VISITOR_CONSENT_EVENT = "visitorConsentCollected";
17305
+ var ShopifyCustomerPrivacyAdapter = class {
17306
+ constructor() {
17307
+ __publicField(this, "name", "shopify-customer-privacy");
17308
+ }
17309
+ detect() {
17310
+ if (typeof window === "undefined") return false;
17311
+ const shopify = window.Shopify;
17312
+ return typeof (shopify == null ? void 0 : shopify.loadFeatures) === "function";
17313
+ }
17314
+ initialize(callback) {
17315
+ callback("pending");
17316
+ if (typeof window === "undefined") return () => {
17317
+ };
17318
+ const shopify = window.Shopify;
17319
+ if (!(shopify == null ? void 0 : shopify.loadFeatures)) return () => {
17320
+ };
17321
+ let visitorEventHandler;
17322
+ shopify.loadFeatures([{ name: "consent-tracking-api", version: "0.1" }], (error2) => {
17323
+ var _a3, _b;
17324
+ if (error2) {
17325
+ return;
17326
+ }
17327
+ const allowed = (_b = (_a3 = shopify.customerPrivacy) == null ? void 0 : _a3.userCanBeTracked()) != null ? _b : true;
17328
+ callback(allowed ? "granted" : "denied");
17329
+ visitorEventHandler = (event) => {
17330
+ const detail = event.detail;
17331
+ callback((detail == null ? void 0 : detail.analyticsAllowed) ? "granted" : "denied");
17332
+ };
17333
+ document.addEventListener(VISITOR_CONSENT_EVENT, visitorEventHandler);
17334
+ });
17335
+ return () => {
17336
+ if (visitorEventHandler) {
17337
+ document.removeEventListener(VISITOR_CONSENT_EVENT, visitorEventHandler);
17338
+ }
17339
+ };
17340
+ }
17341
+ };
17342
+
17343
+ // src/telemetry/consent/ConsentDetector.ts
17344
+ var ConsentDetector = class {
17345
+ constructor(options) {
17346
+ this.options = options;
17347
+ }
17348
+ /**
17349
+ * Start the detector. Probes adapters in array order; first match wins.
17350
+ * Returns a teardown function that unsubscribes the active adapter.
17351
+ *
17352
+ * Async because adapters may need async initialization. Bootstrap
17353
+ * should NOT await this — feature flags and anonymous telemetry
17354
+ * should keep flowing while the detector probes.
17355
+ */
17356
+ async start(gate) {
17357
+ var _a3, _b, _c, _d;
17358
+ for (const adapter of this.options.adapters) {
17359
+ let detected = false;
17360
+ try {
17361
+ detected = adapter.detect();
17362
+ } catch {
17363
+ continue;
17364
+ }
17365
+ if (!detected) continue;
17366
+ try {
17367
+ const teardown = adapter.initialize((status) => gate.setStatus(status));
17368
+ (_b = (_a3 = this.options).onAdapterSelected) == null ? void 0 : _b.call(_a3, adapter.name);
17369
+ return teardown;
17370
+ } catch (err) {
17371
+ console.warn(`[Syntro consent] Adapter "${adapter.name}" failed:`, err);
17372
+ }
17373
+ }
17374
+ (_d = (_c = this.options).onAdapterSelected) == null ? void 0 : _d.call(_c, null);
17375
+ const fallback = await this.resolveFallback();
17376
+ gate.setStatus(fallback);
17377
+ return () => {
17378
+ };
17379
+ }
17380
+ async resolveFallback() {
17381
+ if (!this.options.fallbackStatus) return "pending";
17382
+ try {
17383
+ return await this.options.fallbackStatus();
17384
+ } catch {
17385
+ return "pending";
17386
+ }
17387
+ }
17388
+ };
17389
+
17390
+ // src/telemetry/consent/ConsentGate.ts
17391
+ var ConsentGate = class {
17392
+ constructor(config = {}) {
17393
+ __publicField(this, "status");
17394
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
17395
+ __publicField(this, "configCallback");
17396
+ __publicField(this, "gtmEnabled");
17397
+ __publicField(this, "destroyed", false);
17398
+ __publicField(this, "pendingResolverFn");
17399
+ var _a3, _b;
17400
+ this.status = (_a3 = config.initialStatus) != null ? _a3 : "pending";
17401
+ this.configCallback = config.onConsentChange;
17402
+ this.gtmEnabled = (_b = config.readFromGtmConsentMode) != null ? _b : false;
17403
+ this.pendingResolverFn = config.pendingResolver;
17404
+ }
17405
+ /**
17406
+ * Grant consent — enables telemetry collection.
17407
+ */
17408
+ grant() {
17409
+ this.setStatus("granted");
17410
+ }
17411
+ /**
17412
+ * Deny consent — disables telemetry collection.
17413
+ */
17414
+ deny() {
17415
+ this.setStatus("denied");
17416
+ }
17417
+ /**
17418
+ * Get the current consent status.
17419
+ */
17420
+ getStatus() {
17421
+ return this.status;
17422
+ }
17423
+ /**
17424
+ * Subscribe to consent status changes.
17425
+ * Returns an unsubscribe function.
17426
+ */
17427
+ subscribe(listener) {
17428
+ this.listeners.add(listener);
17429
+ return () => {
17430
+ this.listeners.delete(listener);
17431
+ };
17432
+ }
17433
+ /**
17434
+ * Subscribe to the next definitive (granted | denied) status change,
17435
+ * then auto-unsubscribe. If status is already definitive, fires
17436
+ * synchronously (next microtask) with current status.
17437
+ *
17438
+ * Used by D-2 (URL-param identify) to defer the identify() call
17439
+ * until consent is decided.
17440
+ */
17441
+ subscribeOnce(listener) {
17442
+ if (this.status !== "pending") {
17443
+ queueMicrotask(() => listener(this.status));
17444
+ return () => {
17445
+ };
17446
+ }
17447
+ const unsub = this.subscribe((status) => {
17448
+ if (status === "pending") return;
17449
+ unsub();
17450
+ listener(status);
17451
+ });
17452
+ return unsub;
17453
+ }
17454
+ /**
17455
+ * Resolve what the current 'pending' state should be treated as,
17456
+ * via the configured pendingResolver. Returns the current status
17457
+ * directly if it's already 'granted' or 'denied'.
17458
+ *
17459
+ * Defaults to 'denied' if no resolver is configured (conservative).
17460
+ */
17461
+ async resolvePending() {
17462
+ if (this.status !== "pending") return this.status;
17463
+ if (!this.pendingResolverFn) return "denied";
17464
+ try {
17465
+ return await this.pendingResolverFn();
17466
+ } catch {
17467
+ return "denied";
17468
+ }
17469
+ }
17470
+ /**
17471
+ * Public setter — used by ConsentDetector / adapters to push state changes.
17472
+ */
17473
+ setStatus(newStatus) {
17474
+ this.applyStatus(newStatus);
17475
+ }
17476
+ /**
17477
+ * Poll GTM's dataLayer for consent state.
17478
+ * Scans for the most recent consent update event with analytics_storage.
17479
+ */
17480
+ pollGtmConsent() {
17481
+ if (!this.gtmEnabled || typeof window === "undefined") return;
17482
+ const dataLayer = window.dataLayer;
17483
+ if (!Array.isArray(dataLayer)) return;
17484
+ for (let i = dataLayer.length - 1; i >= 0; i--) {
17485
+ const entry = dataLayer[i];
17486
+ if (!entry) continue;
17487
+ const isConsentUpdate = entry[0] === "consent" && entry[1] === "update" && entry[2];
17488
+ if (isConsentUpdate) {
17489
+ const storage = entry[2].analytics_storage;
17490
+ if (storage === "granted") {
17491
+ this.setStatus("granted");
17492
+ } else if (storage === "denied") {
17493
+ this.setStatus("denied");
17494
+ }
17495
+ return;
17496
+ }
17497
+ }
17498
+ }
17499
+ /**
17500
+ * Clean up listeners and stop responding to changes.
17501
+ */
17502
+ destroy() {
17503
+ this.destroyed = true;
17504
+ this.listeners.clear();
17505
+ }
17506
+ // ─── Private ─────────────────────────────────────────────────────
17507
+ applyStatus(newStatus) {
17508
+ if (this.destroyed) return;
17509
+ if (this.status === newStatus) return;
17510
+ this.status = newStatus;
17511
+ this.notify(newStatus);
17512
+ }
17513
+ notify(status) {
17514
+ if (this.configCallback) {
17515
+ try {
17516
+ this.configCallback(status);
17517
+ } catch {
17518
+ }
17519
+ }
17520
+ for (const listener of this.listeners) {
17521
+ try {
17522
+ listener(status);
17523
+ } catch {
17524
+ }
17525
+ }
17526
+ }
17527
+ };
17528
+
16882
17529
  // src/telemetry/InterventionTracker.ts
16883
17530
  var InterventionTracker = class {
16884
17531
  constructor(telemetry, variantId) {
@@ -16981,6 +17628,14 @@ var NoopAdapter = class {
16981
17628
  }
16982
17629
  track(_eventName, _properties) {
16983
17630
  }
17631
+ identify(_distinctId, _properties) {
17632
+ }
17633
+ alias(_newDistinctId, _oldDistinctId) {
17634
+ }
17635
+ optInCapturing() {
17636
+ }
17637
+ optOutCapturing() {
17638
+ }
16984
17639
  };
16985
17640
  function createNoopClient() {
16986
17641
  return new NoopAdapter();
@@ -17058,6 +17713,9 @@ var PostHogAdapter = class {
17058
17713
  // Enable web vitals
17059
17714
  enable_recording_console_log: true
17060
17715
  };
17716
+ if (options.cookieless_mode) {
17717
+ initOptions.cookieless_mode = options.cookieless_mode;
17718
+ }
17061
17719
  const result = posthog.init(
17062
17720
  options.apiKey,
17063
17721
  initOptions,
@@ -17168,6 +17826,24 @@ var PostHogAdapter = class {
17168
17826
  var _a3;
17169
17827
  (_a3 = this.client) == null ? void 0 : _a3.alias(id, aliasId);
17170
17828
  }
17829
+ /**
17830
+ * Opt the current visitor INTO capturing. Drives the granted-state
17831
+ * transition from the consent gate. When previously cookieless or
17832
+ * opted-out, switches to full identified capture.
17833
+ */
17834
+ optInCapturing() {
17835
+ var _a3;
17836
+ (_a3 = this.client) == null ? void 0 : _a3.opt_in_capturing();
17837
+ }
17838
+ /**
17839
+ * Opt the current visitor OUT of capturing. Drives the denied-state
17840
+ * transition from the consent gate. Stops sending events; existing
17841
+ * cookieless/anonymous events remain in the data warehouse.
17842
+ */
17843
+ optOutCapturing() {
17844
+ var _a3;
17845
+ (_a3 = this.client) == null ? void 0 : _a3.opt_out_capturing();
17846
+ }
17171
17847
  track(eventName, payload) {
17172
17848
  var _a3;
17173
17849
  (_a3 = this.client) == null ? void 0 : _a3.capture(eventName, payload);
@@ -17436,6 +18112,11 @@ async function _initCore(options) {
17436
18112
  // Enable PostHog feature flags for segment membership
17437
18113
  enableFeatureFlags: true,
17438
18114
  sessionRecording: true,
18115
+ // Cookieless mode: anonymous baseline until consent decision.
18116
+ // PostHog captures behavioral data with privacy-preserving session
18117
+ // hashes during pending; switches to cookied tracking on grant;
18118
+ // continues cookielessly on reject.
18119
+ cookieless_mode: "on_reject",
17439
18120
  // Wire up callback for when flags are loaded (Phase 2)
17440
18121
  onFeatureFlagsLoaded,
17441
18122
  // Wire up event capture to feed into event processor
@@ -17457,6 +18138,53 @@ async function _initCore(options) {
17457
18138
  var _a4;
17458
18139
  (_a4 = telemetryForCapture.track) == null ? void 0 : _a4.call(telemetryForCapture, name, props);
17459
18140
  });
18141
+ const telemetryForGate = telemetry;
18142
+ const resolveGeoStatus = async () => {
18143
+ const geo = await geoPromise;
18144
+ const isStrictRegion2 = geo.is_eu === "true" || geo.country_code === "GB" || geo.country_code === "CH";
18145
+ return isStrictRegion2 ? "denied" : "granted";
18146
+ };
18147
+ const consentGate = new ConsentGate({
18148
+ initialStatus: "pending",
18149
+ pendingResolver: resolveGeoStatus,
18150
+ // Drive PostHog opt-in/opt-out from gate transitions.
18151
+ onConsentChange: (status) => {
18152
+ var _a4, _b2;
18153
+ if (status === "granted") {
18154
+ (_a4 = telemetryForGate.optInCapturing) == null ? void 0 : _a4.call(telemetryForGate);
18155
+ } else if (status === "denied") {
18156
+ (_b2 = telemetryForGate.optOutCapturing) == null ? void 0 : _b2.call(telemetryForGate);
18157
+ }
18158
+ }
18159
+ });
18160
+ try {
18161
+ installDefaults(telemetry, consentGate, { geoPromise });
18162
+ debug("Syntro Bootstrap", "SDK defaults installed (D-1..D-5) with consent gate");
18163
+ } catch (err) {
18164
+ warn("Syntro Bootstrap", "installDefaults failed", err);
18165
+ }
18166
+ try {
18167
+ const detector = new ConsentDetector({
18168
+ adapters: [
18169
+ new ShopifyCustomerPrivacyAdapter(),
18170
+ new IabTcfAdapter(),
18171
+ new GtmConsentModeAdapter()
18172
+ ],
18173
+ fallbackStatus: resolveGeoStatus,
18174
+ onAdapterSelected: (name) => {
18175
+ var _a4;
18176
+ (_a4 = telemetryForGate.track) == null ? void 0 : _a4.call(telemetryForGate, "syntro_consent_adapter_selected", {
18177
+ adapter: name != null ? name : "none"
18178
+ });
18179
+ debug("Syntro Bootstrap", `Consent adapter selected: ${name != null ? name : "none"}`);
18180
+ }
18181
+ });
18182
+ detector.start(consentGate).catch((err) => {
18183
+ warn("Syntro Bootstrap", "Consent detector failed to start", err);
18184
+ });
18185
+ } catch (err) {
18186
+ warn("Syntro Bootstrap", "Consent detector setup failed", err);
18187
+ }
17460
18188
  if ((platformAdapter == null ? void 0 : platformAdapter.name) === "shopify" && telemetryHost) {
17461
18189
  try {
17462
18190
  shopifyPixelBridge = new ShopifyPixelBridge({