@leapio/web 0.0.0-stage → 1.0.1

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 ADDED
@@ -0,0 +1,3628 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region src/cookie.ts
6
+ const LEGACY_UID_COOKIE = "lgspt_uid";
7
+ const ONE_DAY_IN_SECONDS = 86400;
8
+ const ONE_YEAR_IN_SECONDS = 360 * ONE_DAY_IN_SECONDS;
9
+ const setCookie = ({ name, value, expiresInSeconds, domain }) => {
10
+ let date = /* @__PURE__ */ new Date();
11
+ date.setTime(date.getTime() + expiresInSeconds * 1e3);
12
+ const expires = "; expires=" + date.toUTCString();
13
+ const domainProperty = domain ? `; domain=${domain}` : "";
14
+ document.cookie = name + "=" + (value || "") + domainProperty + expires + "; path=/";
15
+ };
16
+ const getCookie = (name) => {
17
+ const nameEQ = name + "=";
18
+ const ca = document.cookie.split(";");
19
+ for (var i = 0; i < ca.length; i++) {
20
+ let c = ca[i];
21
+ while (c.charAt(0) == " ") c = c.substring(1, c.length);
22
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
23
+ }
24
+ return null;
25
+ };
26
+ const eraseCookie = (name, domain) => {
27
+ const domainProperty = domain ? `; Domain=${domain}` : "";
28
+ document.cookie = name + `=; Path=/${domainProperty}; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
29
+ };
30
+ //#endregion
31
+ //#region src/safe-storage.ts
32
+ const safeLocalStorage = () => {
33
+ try {
34
+ if (typeof window === "undefined" || !window.localStorage) return null;
35
+ const probeKey = "__leapio_probe__";
36
+ window.localStorage.setItem(probeKey, "1");
37
+ window.localStorage.removeItem(probeKey);
38
+ return window.localStorage;
39
+ } catch (_unused) {
40
+ return null;
41
+ }
42
+ };
43
+ //#endregion
44
+ //#region src/anonymous-id.ts
45
+ const ANONYMOUS_ID_KEY = "leapio_anonymous_id";
46
+ const LEGACY_ANONYMOUS_ID_KEY = "lgspt_anonymous_id";
47
+ const MAX_ANONYMOUS_ID_LENGTH = 64;
48
+ /**
49
+ * Generate a v4 UUID. Prefer crypto.randomUUID() (modern browsers); fall back
50
+ * to a Math.random-based implementation for older environments.
51
+ */
52
+ const generateAnonymousId = () => {
53
+ try {
54
+ const c = typeof crypto !== "undefined" ? crypto : void 0;
55
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
56
+ } catch (_unused) {}
57
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
58
+ const r = Math.random() * 16 | 0;
59
+ return (c === "x" ? r : r & 3 | 8).toString(16);
60
+ });
61
+ };
62
+ const tryReadCookie$1 = (name) => {
63
+ try {
64
+ return getCookie(name);
65
+ } catch (_unused2) {
66
+ return null;
67
+ }
68
+ };
69
+ const tryWriteCookie$1 = (value, opts) => {
70
+ try {
71
+ var _opts$cookieExpiratio, _opts$cookieDomain;
72
+ setCookie({
73
+ name: ANONYMOUS_ID_KEY,
74
+ value,
75
+ expiresInSeconds: (_opts$cookieExpiratio = opts.cookieExpirationInSeconds) !== null && _opts$cookieExpiratio !== void 0 ? _opts$cookieExpiratio : ONE_YEAR_IN_SECONDS,
76
+ domain: (_opts$cookieDomain = opts.cookieDomain) !== null && _opts$cookieDomain !== void 0 ? _opts$cookieDomain : null
77
+ });
78
+ return getCookie(ANONYMOUS_ID_KEY) === value;
79
+ } catch (_unused3) {
80
+ return false;
81
+ }
82
+ };
83
+ const tryReadLocalStorage$1 = (key) => {
84
+ const ls = safeLocalStorage();
85
+ if (!ls) return null;
86
+ try {
87
+ return ls.getItem(key);
88
+ } catch (_unused4) {
89
+ return null;
90
+ }
91
+ };
92
+ const tryWriteLocalStorage$1 = (value) => {
93
+ const ls = safeLocalStorage();
94
+ if (!ls) return false;
95
+ try {
96
+ ls.setItem(ANONYMOUS_ID_KEY, value);
97
+ return true;
98
+ } catch (_unused5) {
99
+ return false;
100
+ }
101
+ };
102
+ let inMemoryAnonymousId = null;
103
+ /**
104
+ * Read existing anonymous id from cookie -> legacy SDK cookie -> localStorage
105
+ * -> in-memory. The legacy `lgspt_uid` cookie was the old anonymous browser id;
106
+ * migrating it preserves pre-identify history without continuing to send it as
107
+ * `user_id`.
108
+ */
109
+ const readAnonymousId = (opts) => {
110
+ var _tryReadLocalStorage;
111
+ if (!opts.cookiesDisabled) {
112
+ var _tryReadCookie;
113
+ const fromCookie = (_tryReadCookie = tryReadCookie$1("leapio_anonymous_id")) !== null && _tryReadCookie !== void 0 ? _tryReadCookie : tryReadCookie$1(LEGACY_ANONYMOUS_ID_KEY);
114
+ if (fromCookie) return fromCookie;
115
+ const fromLegacyCookie = tryReadCookie$1(LEGACY_UID_COOKIE);
116
+ if (fromLegacyCookie && fromLegacyCookie.length <= MAX_ANONYMOUS_ID_LENGTH) return fromLegacyCookie;
117
+ }
118
+ const fromLs = (_tryReadLocalStorage = tryReadLocalStorage$1("leapio_anonymous_id")) !== null && _tryReadLocalStorage !== void 0 ? _tryReadLocalStorage : tryReadLocalStorage$1(LEGACY_ANONYMOUS_ID_KEY);
119
+ if (fromLs) return fromLs;
120
+ return inMemoryAnonymousId;
121
+ };
122
+ /**
123
+ * Persist the anonymous id through the storage chain. Cookie attempted first
124
+ * (unless disabled), then localStorage, then in-memory as last resort.
125
+ */
126
+ const writeAnonymousId = (value, opts) => {
127
+ if (!opts.cookiesDisabled) tryWriteCookie$1(value, opts);
128
+ tryWriteLocalStorage$1(value);
129
+ inMemoryAnonymousId = value;
130
+ };
131
+ /** Clear the anonymous id from every storage layer. Used by reset(). */
132
+ const clearAnonymousId = (opts) => {
133
+ for (const name of [
134
+ ANONYMOUS_ID_KEY,
135
+ LEGACY_ANONYMOUS_ID_KEY,
136
+ LEGACY_UID_COOKIE
137
+ ]) try {
138
+ var _opts$cookieDomain2;
139
+ eraseCookie(name, (_opts$cookieDomain2 = opts.cookieDomain) !== null && _opts$cookieDomain2 !== void 0 ? _opts$cookieDomain2 : null);
140
+ } catch (_unused6) {}
141
+ const ls = safeLocalStorage();
142
+ if (ls) try {
143
+ ls.removeItem(ANONYMOUS_ID_KEY);
144
+ ls.removeItem(LEGACY_ANONYMOUS_ID_KEY);
145
+ } catch (_unused7) {}
146
+ inMemoryAnonymousId = null;
147
+ };
148
+ /**
149
+ * Get-or-create: return existing anonymous id if present, otherwise generate
150
+ * a new one and persist it.
151
+ */
152
+ const ensureAnonymousId = (opts) => {
153
+ const existing = readAnonymousId(opts);
154
+ if (existing) {
155
+ inMemoryAnonymousId = existing;
156
+ writeAnonymousId(existing, opts);
157
+ return existing;
158
+ }
159
+ const fresh = generateAnonymousId();
160
+ writeAnonymousId(fresh, opts);
161
+ return fresh;
162
+ };
163
+ /** Force-rotate: generate a new anonymous id, clear all old layers, persist new. */
164
+ const rotateAnonymousId = (opts) => {
165
+ clearAnonymousId(opts);
166
+ const fresh = generateAnonymousId();
167
+ writeAnonymousId(fresh, opts);
168
+ return fresh;
169
+ };
170
+ //#endregion
171
+ //#region src/ac-overrides.ts
172
+ /** localStorage key prefix; the public key is appended for per-project dedup. */
173
+ const AC_OVERRIDES_KEY_PREFIX = "leapio_acov_";
174
+ const keyFor$1 = (publicKey) => `${AC_OVERRIDES_KEY_PREFIX}${publicKey}`;
175
+ /** The per-type keys the SDK reports (the server-controllable types). */
176
+ const REPORTED_TYPE_KEYS = [
177
+ "outboundLinks",
178
+ "fileDownloads",
179
+ "formSubmissions",
180
+ "mediaPlayback"
181
+ ];
182
+ /**
183
+ * Build the EXPLICIT snippet-override report from the normalized snippet shape
184
+ * (`{ master, types }` produced by `normalizeSnippet`) plus the legacy
185
+ * `enableAutoClicks` flag. Only keys the snippet explicitly set are included:
186
+ *
187
+ * - `master` is included (as `true`/`false`) only when the snippet/host set it.
188
+ * `enableAutoClicks: true` counts as a snippet-side master-enable (matching the
189
+ * resolver), but only when the snippet did not already set the master.
190
+ * - `types` is included only when the snippet explicitly set one of the
191
+ * server-controllable types (`outboundLinks` / `fileDownloads` /
192
+ * `formSubmissions` / `mediaPlayback`). `clicks` / `inputChanges` are
193
+ * snippet-only knobs the server doesn't model, so they are not reported.
194
+ *
195
+ * Returns an empty object `{}` when the snippet set nothing reportable (the
196
+ * common case). `master` carries the explicit boolean; absent means unset.
197
+ */
198
+ const buildSnippetOverrideReport = (normalized, enableAutoClicks) => {
199
+ const report = {};
200
+ const master = typeof normalized.master === "boolean" ? normalized.master : enableAutoClicks ? true : void 0;
201
+ if (typeof master === "boolean") report.master = master;
202
+ const types = {};
203
+ for (const key of REPORTED_TYPE_KEYS) {
204
+ const value = normalized.types[key];
205
+ if (typeof value === "boolean") types[key] = value;
206
+ }
207
+ if (Object.keys(types).length > 0) report.types = types;
208
+ return report;
209
+ };
210
+ /**
211
+ * Whether a built report carries any explicit override (a master or any type).
212
+ * The empty report `{}` means "the snippet overrode nothing."
213
+ */
214
+ const hasSnippetOverrides = (report) => "master" in report || report.types !== void 0;
215
+ /**
216
+ * Stable fingerprint of a snippet-override report: the JSON of the report with
217
+ * type keys emitted in a fixed order, so logically-equal reports always produce
218
+ * an identical string regardless of insertion order. Returns "" for the empty
219
+ * (no-overrides) report.
220
+ */
221
+ const computeOverrideFingerprint = (report) => {
222
+ if (!hasSnippetOverrides(report)) return "";
223
+ const canonical = {};
224
+ if (typeof report.master === "boolean") canonical.master = report.master;
225
+ if (report.types) {
226
+ const orderedTypes = {};
227
+ for (const key of REPORTED_TYPE_KEYS) {
228
+ const value = report.types[key];
229
+ if (typeof value === "boolean") orderedTypes[key] = value;
230
+ }
231
+ canonical.types = orderedTypes;
232
+ }
233
+ try {
234
+ return JSON.stringify(canonical);
235
+ } catch (_unused) {
236
+ return "";
237
+ }
238
+ };
239
+ /** Read the last-reported fingerprint for a public key. Never throws; "" when absent. */
240
+ const readReportedFingerprint = (publicKey) => {
241
+ if (!publicKey) return "";
242
+ const ls = safeLocalStorage();
243
+ if (!ls) return "";
244
+ try {
245
+ var _ls$getItem;
246
+ return (_ls$getItem = ls.getItem(keyFor$1(publicKey))) !== null && _ls$getItem !== void 0 ? _ls$getItem : "";
247
+ } catch (_unused2) {
248
+ return "";
249
+ }
250
+ };
251
+ /** Persist the last-reported fingerprint for a public key. Best-effort; never throws. */
252
+ const writeReportedFingerprint = (publicKey, fingerprint) => {
253
+ if (!publicKey) return;
254
+ const ls = safeLocalStorage();
255
+ if (!ls) return;
256
+ try {
257
+ ls.setItem(keyFor$1(publicKey), fingerprint);
258
+ } catch (_unused3) {}
259
+ };
260
+ /** Remove the stored fingerprint for a public key. Never throws. */
261
+ const clearReportedFingerprint = (publicKey) => {
262
+ if (!publicKey) return;
263
+ const ls = safeLocalStorage();
264
+ if (!ls) return;
265
+ try {
266
+ ls.removeItem(keyFor$1(publicKey));
267
+ } catch (_unused4) {}
268
+ };
269
+ /**
270
+ * Decide whether to attach `_acOverrides` to the next track request, and update
271
+ * the stored fingerprint so the same override set is reported at most once per
272
+ * browser. All localStorage access is best-effort and never throws.
273
+ *
274
+ * Cases:
275
+ * - Snippet has overrides AND the fingerprint differs from the last reported one
276
+ * → return the report (with `url`) and persist the new fingerprint.
277
+ * - Snippet has NO overrides BUT a non-empty fingerprint was stored → send the
278
+ * CLEAR report once (`{ snippet: { types: {} } }`, no master) so the server
279
+ * clears the flag, then clear the stored fingerprint.
280
+ * - Snippet has no overrides and nothing was previously reported → return null
281
+ * (send nothing — the common case).
282
+ * - Fingerprint unchanged → return null (already reported).
283
+ *
284
+ * The store is updated synchronously here (before the request resolves) so a
285
+ * second event in the same page load does not re-attach the report.
286
+ */
287
+ const resolveAcOverridesToReport = (params) => {
288
+ const { publicKey, report } = params;
289
+ if (!publicKey) return null;
290
+ try {
291
+ const fingerprint = computeOverrideFingerprint(report);
292
+ if (fingerprint === readReportedFingerprint(publicKey)) return null;
293
+ if (fingerprint === "") {
294
+ clearReportedFingerprint(publicKey);
295
+ return { snippet: { types: {} } };
296
+ }
297
+ writeReportedFingerprint(publicKey, fingerprint);
298
+ const out = { snippet: report };
299
+ if (params.url) out.url = params.url;
300
+ return out;
301
+ } catch (_unused5) {
302
+ return null;
303
+ }
304
+ };
305
+ //#endregion
306
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/typeof.js
307
+ function _typeof(o) {
308
+ "@babel/helpers - typeof";
309
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
310
+ return typeof o;
311
+ } : function(o) {
312
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
313
+ }, _typeof(o);
314
+ }
315
+ //#endregion
316
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/toPrimitive.js
317
+ function toPrimitive(t, r) {
318
+ if ("object" != _typeof(t) || !t) return t;
319
+ var e = t[Symbol.toPrimitive];
320
+ if (void 0 !== e) {
321
+ var i = e.call(t, r || "default");
322
+ if ("object" != _typeof(i)) return i;
323
+ throw new TypeError("@@toPrimitive must return a primitive value.");
324
+ }
325
+ return ("string" === r ? String : Number)(t);
326
+ }
327
+ //#endregion
328
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/toPropertyKey.js
329
+ function toPropertyKey(t) {
330
+ var i = toPrimitive(t, "string");
331
+ return "symbol" == _typeof(i) ? i : i + "";
332
+ }
333
+ //#endregion
334
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/defineProperty.js
335
+ function _defineProperty(e, r, t) {
336
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
337
+ value: t,
338
+ enumerable: !0,
339
+ configurable: !0,
340
+ writable: !0
341
+ }) : e[r] = t, e;
342
+ }
343
+ //#endregion
344
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/objectSpread2.js
345
+ function ownKeys(e, r) {
346
+ var t = Object.keys(e);
347
+ if (Object.getOwnPropertySymbols) {
348
+ var o = Object.getOwnPropertySymbols(e);
349
+ r && (o = o.filter(function(r) {
350
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
351
+ })), t.push.apply(t, o);
352
+ }
353
+ return t;
354
+ }
355
+ function _objectSpread2(e) {
356
+ for (var r = 1; r < arguments.length; r++) {
357
+ var t = null != arguments[r] ? arguments[r] : {};
358
+ r % 2 ? ownKeys(Object(t), !0).forEach(function(r) {
359
+ _defineProperty(e, r, t[r]);
360
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r) {
361
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
362
+ });
363
+ }
364
+ return e;
365
+ }
366
+ //#endregion
367
+ //#region src/consent.ts
368
+ /**
369
+ * Consent store for the Leapio SDK.
370
+ *
371
+ * Holds the current consent snapshot ({categories, signals, source, updatedAt})
372
+ * and persists it through the same layered fallback chain used by
373
+ * `anonymous-id.ts`: cookie (`leapio_consent`) -> localStorage -> in-memory.
374
+ *
375
+ * The store is *source-priority aware*: an external consent source (CMP plugin,
376
+ * GCM reader, explicit Leapio API) writes a snapshot via `applySnapshot`, which
377
+ * keeps the highest-priority snapshot authoritative. This lets a later plugins
378
+ * agent register additional sources without re-architecting the store.
379
+ */
380
+ const CONSENT_KEY = "leapio_consent";
381
+ const LEGACY_CONSENT_KEY = "lgspt_consent";
382
+ /** Category -> single-letter code. */
383
+ const CATEGORY_TO_LETTER = {
384
+ analytics: "a",
385
+ functional: "f",
386
+ marketing: "m"
387
+ };
388
+ /** Canonical category order: a < f < m. */
389
+ const CATEGORY_ORDER = [
390
+ "analytics",
391
+ "functional",
392
+ "marketing"
393
+ ];
394
+ /** Signal -> single-letter code. */
395
+ const SIGNAL_TO_LETTER = [
396
+ {
397
+ key: "doNotSellShare",
398
+ letter: "d"
399
+ },
400
+ {
401
+ key: "gpc",
402
+ letter: "g"
403
+ },
404
+ {
405
+ key: "gppDetected",
406
+ letter: "p"
407
+ }
408
+ ];
409
+ /**
410
+ * Encode a set of granted categories into a canonical string (`a` < `f` < `m`).
411
+ * Categories whose value is not strictly `true` are omitted.
412
+ */
413
+ const encodeCategories = (categories) => {
414
+ let out = "";
415
+ for (const category of CATEGORY_ORDER) if (categories[category] === true) out += CATEGORY_TO_LETTER[category];
416
+ return out;
417
+ };
418
+ /**
419
+ * Encode privacy signals into a canonical string (`d` < `g` < `p`).
420
+ * Only signals whose value is strictly `true` are included; `gppString` is a
421
+ * raw value carried separately and never encoded into `privacy_signals`.
422
+ */
423
+ const encodeSignals = (signals) => {
424
+ let out = "";
425
+ for (const { key, letter } of SIGNAL_TO_LETTER) if (signals[key] === true) out += letter;
426
+ return out;
427
+ };
428
+ const SOURCE_PRIORITY = {
429
+ leapio_api: 100,
430
+ concord: 80,
431
+ onetrust: 60,
432
+ usercentrics: 60,
433
+ cookiebot: 60,
434
+ gcm: 40
435
+ };
436
+ /** Resolve a source's priority; unknown sources rank below all known ones. */
437
+ const consentSourcePriority = (source) => {
438
+ var _SOURCE_PRIORITY;
439
+ return (_SOURCE_PRIORITY = SOURCE_PRIORITY[source]) !== null && _SOURCE_PRIORITY !== void 0 ? _SOURCE_PRIORITY : 0;
440
+ };
441
+ const serialize = (snapshot) => {
442
+ try {
443
+ return JSON.stringify(snapshot);
444
+ } catch (_unused) {
445
+ return "";
446
+ }
447
+ };
448
+ const deserialize = (raw) => {
449
+ if (!raw) return null;
450
+ try {
451
+ const parsed = JSON.parse(raw);
452
+ if (!parsed || typeof parsed !== "object" || typeof parsed.source !== "string") return null;
453
+ return {
454
+ categories: parsed.categories && typeof parsed.categories === "object" ? parsed.categories : {},
455
+ signals: parsed.signals && typeof parsed.signals === "object" ? parsed.signals : {},
456
+ source: parsed.source.endsWith("_api") ? "leapio_api" : parsed.source,
457
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
458
+ sourceMetadata: parsed.sourceMetadata && typeof parsed.sourceMetadata === "object" ? parsed.sourceMetadata : void 0
459
+ };
460
+ } catch (_unused2) {
461
+ return null;
462
+ }
463
+ };
464
+ const tryReadCookie = (key) => {
465
+ try {
466
+ const raw = getCookie(key);
467
+ return deserialize(raw ? decodeURIComponent(raw) : null);
468
+ } catch (_unused3) {
469
+ return null;
470
+ }
471
+ };
472
+ const tryWriteCookie = (snapshot, opts) => {
473
+ try {
474
+ var _opts$cookieExpiratio, _opts$cookieDomain;
475
+ setCookie({
476
+ name: CONSENT_KEY,
477
+ value: encodeURIComponent(serialize(snapshot)),
478
+ expiresInSeconds: (_opts$cookieExpiratio = opts.cookieExpirationInSeconds) !== null && _opts$cookieExpiratio !== void 0 ? _opts$cookieExpiratio : ONE_YEAR_IN_SECONDS,
479
+ domain: (_opts$cookieDomain = opts.cookieDomain) !== null && _opts$cookieDomain !== void 0 ? _opts$cookieDomain : null
480
+ });
481
+ } catch (_unused4) {}
482
+ };
483
+ const tryReadLocalStorage = (key) => {
484
+ const ls = safeLocalStorage();
485
+ if (!ls) return null;
486
+ try {
487
+ return deserialize(ls.getItem(key));
488
+ } catch (_unused5) {
489
+ return null;
490
+ }
491
+ };
492
+ const tryWriteLocalStorage = (snapshot) => {
493
+ const ls = safeLocalStorage();
494
+ if (!ls) return;
495
+ try {
496
+ ls.setItem(CONSENT_KEY, serialize(snapshot));
497
+ } catch (_unused6) {}
498
+ };
499
+ let inMemorySnapshot = null;
500
+ /**
501
+ * Read the persisted snapshot: cookie -> localStorage -> in-memory.
502
+ * Honors `cookiesDisabled` exactly like `readAnonymousId`.
503
+ */
504
+ const readConsentSnapshot = (opts) => {
505
+ var _tryReadLocalStorage;
506
+ if (!opts.cookiesDisabled) {
507
+ var _tryReadCookie;
508
+ const fromCookie = (_tryReadCookie = tryReadCookie("leapio_consent")) !== null && _tryReadCookie !== void 0 ? _tryReadCookie : tryReadCookie(LEGACY_CONSENT_KEY);
509
+ if (fromCookie) return fromCookie;
510
+ }
511
+ const fromLs = (_tryReadLocalStorage = tryReadLocalStorage("leapio_consent")) !== null && _tryReadLocalStorage !== void 0 ? _tryReadLocalStorage : tryReadLocalStorage(LEGACY_CONSENT_KEY);
512
+ if (fromLs) return fromLs;
513
+ return inMemorySnapshot;
514
+ };
515
+ /**
516
+ * Persist a snapshot through the storage chain. Cookie first (unless disabled),
517
+ * then localStorage, then in-memory as the last-resort layer.
518
+ */
519
+ const writeConsentSnapshot = (snapshot, opts) => {
520
+ if (!opts.cookiesDisabled) tryWriteCookie(snapshot, opts);
521
+ tryWriteLocalStorage(snapshot);
522
+ inMemorySnapshot = snapshot;
523
+ };
524
+ /** Clear the consent snapshot from every storage layer. Used by reset(). */
525
+ const clearConsentSnapshot = (opts) => {
526
+ for (const name of [CONSENT_KEY, LEGACY_CONSENT_KEY]) try {
527
+ var _opts$cookieDomain2;
528
+ eraseCookie(name, (_opts$cookieDomain2 = opts.cookieDomain) !== null && _opts$cookieDomain2 !== void 0 ? _opts$cookieDomain2 : null);
529
+ } catch (_unused7) {}
530
+ const ls = safeLocalStorage();
531
+ if (ls) try {
532
+ ls.removeItem(CONSENT_KEY);
533
+ ls.removeItem(LEGACY_CONSENT_KEY);
534
+ } catch (_unused8) {}
535
+ inMemorySnapshot = null;
536
+ };
537
+ /**
538
+ * Stateful wrapper around the persisted snapshot. The SDK keeps one instance.
539
+ *
540
+ * `applySnapshot(snapshot)` is the clean internal API external sources write
541
+ * through: it enforces source priority (a lower-priority source never replaces
542
+ * a higher-priority snapshot) and persists + returns whether it became
543
+ * authoritative. The consent-source registry writes through this API.
544
+ */
545
+ var ConsentStore = class {
546
+ constructor(opts) {
547
+ this.opts = opts;
548
+ this.current = readConsentSnapshot(opts);
549
+ }
550
+ /** The current authoritative snapshot, or null if no source has spoken. */
551
+ get() {
552
+ return this.current;
553
+ }
554
+ /** True once any source has produced a snapshot. */
555
+ hasSnapshot() {
556
+ return this.current !== null;
557
+ }
558
+ /**
559
+ * Apply a snapshot from a consent source. Returns `true` if it became (or
560
+ * remained) the authoritative snapshot and was persisted, `false` if it was
561
+ * rejected because a strictly higher-priority source is already authoritative.
562
+ *
563
+ * Ties (same source priority, e.g. a fresh read from the same source) always
564
+ * win so updates from the authoritative source are not dropped.
565
+ */
566
+ applySnapshot(snapshot) {
567
+ if (this.current) {
568
+ if (consentSourcePriority(snapshot.source) < consentSourcePriority(this.current.source)) return false;
569
+ }
570
+ this.current = snapshot;
571
+ writeConsentSnapshot(snapshot, this.opts);
572
+ return true;
573
+ }
574
+ /**
575
+ * Merge a partial update into the snapshot from a given source. Used by
576
+ * `setConsent` (source `leapio_api`): unspecified categories/signals are
577
+ * preserved from the existing snapshot. Subject to the same priority rule.
578
+ */
579
+ mergeSnapshot(partial, source) {
580
+ var _base$categories, _partial$categories, _base$signals, _partial$signals, _partial$sourceMetada;
581
+ const base = this.current;
582
+ const merged = {
583
+ categories: _objectSpread2(_objectSpread2({}, (_base$categories = base === null || base === void 0 ? void 0 : base.categories) !== null && _base$categories !== void 0 ? _base$categories : {}), (_partial$categories = partial.categories) !== null && _partial$categories !== void 0 ? _partial$categories : {}),
584
+ signals: _objectSpread2(_objectSpread2({}, (_base$signals = base === null || base === void 0 ? void 0 : base.signals) !== null && _base$signals !== void 0 ? _base$signals : {}), (_partial$signals = partial.signals) !== null && _partial$signals !== void 0 ? _partial$signals : {}),
585
+ source,
586
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
587
+ sourceMetadata: (_partial$sourceMetada = partial.sourceMetadata) !== null && _partial$sourceMetada !== void 0 ? _partial$sourceMetada : base === null || base === void 0 ? void 0 : base.sourceMetadata
588
+ };
589
+ return this.applySnapshot(merged);
590
+ }
591
+ /** Clear all consent state (every storage layer). Used by reset(). */
592
+ clear() {
593
+ clearConsentSnapshot(this.opts);
594
+ this.current = null;
595
+ }
596
+ };
597
+ //#endregion
598
+ //#region src/remote-config.ts
599
+ /** localStorage key prefix; the public key is appended for per-project caching. */
600
+ const REMOTE_CONFIG_KEY_PREFIX = "leapio_cfg_";
601
+ const keyFor = (publicKey) => `${REMOTE_CONFIG_KEY_PREFIX}${publicKey}`;
602
+ /** Narrow an unknown parsed value to a cache envelope, or null. */
603
+ const asCachedConfig = (value) => {
604
+ if (typeof value !== "object" || value === null) return null;
605
+ const candidate = value;
606
+ if (typeof candidate.cv !== "number" || typeof candidate.ts !== "number") return null;
607
+ if (typeof candidate.config !== "object" || candidate.config === null) return null;
608
+ return candidate;
609
+ };
610
+ /**
611
+ * Read the cached config for a public key. Returns null when there is nothing
612
+ * cached, when the cache format / TTL no longer matches, or when anything at
613
+ * all goes wrong (corrupt JSON, unavailable storage). Never throws.
614
+ */
615
+ const readRemoteConfig = (publicKey) => {
616
+ if (!publicKey) return null;
617
+ const ls = safeLocalStorage();
618
+ if (!ls) return null;
619
+ let raw;
620
+ try {
621
+ raw = ls.getItem(keyFor(publicKey));
622
+ } catch (_unused) {
623
+ return null;
624
+ }
625
+ if (!raw) return null;
626
+ let parsed;
627
+ try {
628
+ parsed = JSON.parse(raw);
629
+ } catch (_unused2) {
630
+ return null;
631
+ }
632
+ const cached = asCachedConfig(parsed);
633
+ if (!cached) return null;
634
+ if (cached.cv !== 1) return null;
635
+ if (Date.now() - cached.ts > 864e5) return null;
636
+ return cached.config;
637
+ };
638
+ /**
639
+ * Persist the config for a public key. Best-effort: silently no-ops when
640
+ * storage is unavailable or the write fails. Never throws.
641
+ */
642
+ const writeRemoteConfig = (publicKey, config) => {
643
+ if (!publicKey || typeof config !== "object" || config === null) return;
644
+ const ls = safeLocalStorage();
645
+ if (!ls) return;
646
+ const envelope = {
647
+ cv: 1,
648
+ ts: Date.now(),
649
+ config
650
+ };
651
+ try {
652
+ ls.setItem(keyFor(publicKey), JSON.stringify(envelope));
653
+ } catch (_unused3) {}
654
+ };
655
+ //#endregion
656
+ //#region src/url-privacy.ts
657
+ const DEFAULT_QUERY_PARAM_ALLOWLIST = [
658
+ "utm_source",
659
+ "utm_id",
660
+ "utm_medium",
661
+ "utm_campaign",
662
+ "utm_term",
663
+ "utm_content",
664
+ "gclid",
665
+ "gbraid",
666
+ "wbraid",
667
+ "fbclid",
668
+ "msclkid",
669
+ "ttclid",
670
+ "ref"
671
+ ];
672
+ const hasScheme = (value) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
673
+ /**
674
+ * Remove non-allow-listed query params (and always the hash) from `rawUrl`.
675
+ * - Matching is case-insensitive on the param NAME; kept params preserve their
676
+ * original order, repeats, and values.
677
+ * - Relative URLs are supported (the sentinel base is stripped back off).
678
+ * - Never throws and never returns a broken url: on parse failure it returns the
679
+ * original string (the server still strips authoritatively).
680
+ */
681
+ const stripUrl = (rawUrl, allowlist = DEFAULT_QUERY_PARAM_ALLOWLIST) => {
682
+ if (!rawUrl) return rawUrl;
683
+ try {
684
+ const allow = new Set(allowlist.map((name) => name.toLowerCase()));
685
+ const absolute = hasScheme(rawUrl);
686
+ const parsed = new URL(rawUrl, absolute ? void 0 : "http://x.invalid");
687
+ const kept = new URLSearchParams();
688
+ parsed.searchParams.forEach((value, key) => {
689
+ if (allow.has(key.toLowerCase())) kept.append(key, value);
690
+ });
691
+ const search = kept.toString();
692
+ const path = `${parsed.pathname}${search ? `?${search}` : ""}`;
693
+ return absolute ? `${parsed.protocol}//${parsed.host}${path}` : path;
694
+ } catch (_unused) {
695
+ return rawUrl;
696
+ }
697
+ };
698
+ //#endregion
699
+ //#region src/api.ts
700
+ const API_URL = "https://api.leapio.com";
701
+ const deriveEndpoint = (config, suffix) => {
702
+ if (!config.externalApiUrl) return `${API_URL}/${suffix}`;
703
+ try {
704
+ const url = new URL(config.externalApiUrl);
705
+ if (/\/(i|track)\/?$/.test(url.pathname)) {
706
+ url.pathname = url.pathname.replace(/\/(i|track)\/?$/, `/${suffix}`);
707
+ return url.toString();
708
+ }
709
+ } catch (_unused) {}
710
+ console.error(`Leapio - cannot derive the /${suffix} endpoint from externalApiUrl. Set ${suffix === "identify" ? "identifyApiUrl" : "consentApiUrl"} explicitly.`);
711
+ return null;
712
+ };
713
+ const identifyUrlFromConfig = (config) => {
714
+ if (config.identifyApiUrl) return config.identifyApiUrl;
715
+ return deriveEndpoint(config, "identify");
716
+ };
717
+ const consentUrlFromConfig = (config) => {
718
+ if (config.consentApiUrl) return config.consentApiUrl;
719
+ return deriveEndpoint(config, "consent");
720
+ };
721
+ const groupUrlFromConfig = (config) => {
722
+ if (config.groupApiUrl) return config.groupApiUrl;
723
+ return deriveEndpoint(config, "group");
724
+ };
725
+ /**
726
+ * Apply consent stamps to an event payload. Called *after* the user's
727
+ * `eventMapper` so the stamps are authoritative and never clobbered. When no
728
+ * snapshot exists the stamps default to empty strings ("pre-consent / unknown"),
729
+ * matching the server's legacy-empty handling.
730
+ */
731
+ const consentStampFields = (snapshot) => snapshot ? {
732
+ privacy_categories: encodeCategories(snapshot.categories),
733
+ privacy_signals: encodeSignals(snapshot.signals),
734
+ consent_source: snapshot.source
735
+ } : {
736
+ privacy_categories: "",
737
+ privacy_signals: "",
738
+ consent_source: null
739
+ };
740
+ const stampConsent = (payload, snapshot) => _objectSpread2(_objectSpread2({}, payload), consentStampFields(snapshot));
741
+ const trackEvent = async (config, payload, consentSnapshot, acOverrides) => {
742
+ if (!payload.event) {
743
+ console.error("Leapio - event parameter is required");
744
+ return;
745
+ }
746
+ try {
747
+ var _payload$userId, _payload$anonymousId, _payload$metadata, _payload$referrer;
748
+ let payloadToSend = _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({
749
+ name: payload.event,
750
+ channel: payload.channel,
751
+ message: payload.message,
752
+ notify: payload.notify,
753
+ user_id: (_payload$userId = payload.userId) !== null && _payload$userId !== void 0 ? _payload$userId : null,
754
+ anonymous_id: (_payload$anonymousId = payload.anonymousId) !== null && _payload$anonymousId !== void 0 ? _payload$anonymousId : null,
755
+ metadata: (_payload$metadata = payload.metadata) !== null && _payload$metadata !== void 0 ? _payload$metadata : {},
756
+ hostname: payload.hostname,
757
+ url: payload.url,
758
+ referrer: ((_payload$referrer = payload.referrer) === null || _payload$referrer === void 0 ? void 0 : _payload$referrer.length) ? payload.referrer : null,
759
+ language: payload.language,
760
+ screen: payload.screen
761
+ }, payload.value != null ? { value: payload.value } : {}), payload.currency != null ? { currency: payload.currency } : {}), payload.groupId ? { group_id: payload.groupId } : {}), payload.groupId ? { group_type: payload.groupType || "company" } : {});
762
+ if (config.eventMapper) payloadToSend = await config.eventMapper(payloadToSend);
763
+ if (config.stripQueryParams) {
764
+ var _payloadToSend$referr;
765
+ payloadToSend = _objectSpread2(_objectSpread2({}, payloadToSend), {}, {
766
+ url: stripUrl(payloadToSend.url, config.queryParamAllowlist),
767
+ referrer: ((_payloadToSend$referr = payloadToSend.referrer) === null || _payloadToSend$referr === void 0 ? void 0 : _payloadToSend$referr.length) ? stripUrl(payloadToSend.referrer, config.queryParamAllowlist) : null
768
+ });
769
+ }
770
+ if (payloadToSend.referrer && payloadToSend.referrer.length > 255) payloadToSend = _objectSpread2(_objectSpread2({}, payloadToSend), {}, { referrer: payloadToSend.referrer.slice(0, 255) });
771
+ payloadToSend = stampConsent(payloadToSend, consentSnapshot !== null && consentSnapshot !== void 0 ? consentSnapshot : null);
772
+ const wireBody = acOverrides ? _objectSpread2(_objectSpread2({}, payloadToSend), {}, { _acOverrides: acOverrides }) : payloadToSend;
773
+ const response = await fetch(config.externalApiUrl ? config.externalApiUrl : `${API_URL}/i`, {
774
+ method: "POST",
775
+ headers: {
776
+ "Content-Type": "application/json",
777
+ "x-leapio-pk": config.publicKey
778
+ },
779
+ mode: "cors",
780
+ keepalive: true,
781
+ body: JSON.stringify(wireBody)
782
+ });
783
+ if (!response.ok) {
784
+ console.error("Leapio - track rejected", response.status);
785
+ return;
786
+ }
787
+ await persistConfigFromResponse(config.publicKey, response);
788
+ } catch (err) {
789
+ console.error("Leapio - could not track event");
790
+ }
791
+ };
792
+ /**
793
+ * Read the `config` field from a track response and cache it for the public
794
+ * key. Tolerates non-JSON / missing bodies and never throws — config delivery
795
+ * is additive and must never break tracking.
796
+ */
797
+ const persistConfigFromResponse = async (publicKey, response) => {
798
+ if (!publicKey || !response || !response.ok) return;
799
+ try {
800
+ const body = await response.json();
801
+ if (typeof body !== "object" || body === null) return;
802
+ const config = body.config;
803
+ if (typeof config === "object" && config !== null) writeRemoteConfig(publicKey, config);
804
+ } catch (_unused2) {}
805
+ };
806
+ const identifyUser = async (config, payload, consentSnapshot) => {
807
+ var _payload$traits, _payload$options;
808
+ if (!payload.userId) {
809
+ console.error("Leapio - identify requires a userId");
810
+ return {
811
+ ok: false,
812
+ reason: "missing_user_id"
813
+ };
814
+ }
815
+ const body = {
816
+ anonymous_id: payload.anonymousId,
817
+ user_id: payload.userId,
818
+ traits: (_payload$traits = payload.traits) !== null && _payload$traits !== void 0 ? _payload$traits : {},
819
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
820
+ };
821
+ Object.assign(body, consentStampFields(consentSnapshot !== null && consentSnapshot !== void 0 ? consentSnapshot : null));
822
+ if ((_payload$options = payload.options) === null || _payload$options === void 0 || (_payload$options = _payload$options.identityVerification) === null || _payload$options === void 0 ? void 0 : _payload$options.token) body.identity_verification = { token: payload.options.identityVerification.token };
823
+ const endpoint = identifyUrlFromConfig(config);
824
+ if (!endpoint) return {
825
+ ok: false,
826
+ reason: "no_endpoint"
827
+ };
828
+ try {
829
+ const response = await fetch(endpoint, {
830
+ method: "POST",
831
+ headers: {
832
+ "Content-Type": "application/json",
833
+ "x-leapio-pk": config.publicKey
834
+ },
835
+ mode: "cors",
836
+ keepalive: true,
837
+ body: JSON.stringify(body)
838
+ });
839
+ if (response.ok) return {
840
+ ok: true,
841
+ status: response.status
842
+ };
843
+ let reason;
844
+ try {
845
+ const errorBody = await response.json();
846
+ reason = typeof (errorBody === null || errorBody === void 0 ? void 0 : errorBody.message) === "string" ? errorBody.message : void 0;
847
+ } catch (_unused3) {}
848
+ console.error("Leapio - identify failed", response.status, reason);
849
+ return {
850
+ ok: false,
851
+ status: response.status,
852
+ reason
853
+ };
854
+ } catch (err) {
855
+ console.error("Leapio - could not identify user");
856
+ return {
857
+ ok: false,
858
+ reason: "network_error"
859
+ };
860
+ }
861
+ };
862
+ /**
863
+ * Associate the current user/anonymous visitor with a group (Segment `group()`).
864
+ * Sent with the `x-leapio-pk` header like the other public-API calls. The
865
+ * platform requires a `user_id` or `anonymous_id`; the SDK always supplies its
866
+ * managed `anonymous_id`, and `user_id` when the visitor is identified.
867
+ */
868
+ const groupUser = async (config, payload) => {
869
+ var _payload$options2;
870
+ if (!payload.groupId) {
871
+ console.error("Leapio - group requires a groupId");
872
+ return {
873
+ ok: false,
874
+ reason: "missing_group_id"
875
+ };
876
+ }
877
+ if (!payload.userId && !payload.anonymousId) {
878
+ console.error("Leapio - group requires a userId or anonymousId");
879
+ return {
880
+ ok: false,
881
+ reason: "missing_subject"
882
+ };
883
+ }
884
+ const body = {
885
+ group_id: payload.groupId,
886
+ type: payload.type || "company",
887
+ anonymous_id: payload.anonymousId,
888
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
889
+ };
890
+ if (payload.userId) body.user_id = payload.userId;
891
+ if (payload.traits) body.traits = payload.traits;
892
+ if ((_payload$options2 = payload.options) === null || _payload$options2 === void 0 || (_payload$options2 = _payload$options2.identityVerification) === null || _payload$options2 === void 0 ? void 0 : _payload$options2.token) body.identity_verification = { token: payload.options.identityVerification.token };
893
+ const endpoint = groupUrlFromConfig(config);
894
+ if (!endpoint) return {
895
+ ok: false,
896
+ reason: "no_endpoint"
897
+ };
898
+ try {
899
+ const response = await fetch(endpoint, {
900
+ method: "POST",
901
+ headers: {
902
+ "Content-Type": "application/json",
903
+ "x-leapio-pk": config.publicKey
904
+ },
905
+ mode: "cors",
906
+ keepalive: true,
907
+ body: JSON.stringify(body)
908
+ });
909
+ if (response.ok) return {
910
+ ok: true,
911
+ status: response.status
912
+ };
913
+ let reason;
914
+ try {
915
+ const errorBody = await response.json();
916
+ reason = typeof (errorBody === null || errorBody === void 0 ? void 0 : errorBody.message) === "string" ? errorBody.message : void 0;
917
+ } catch (_unused4) {}
918
+ console.error("Leapio - group failed", response.status, reason);
919
+ return {
920
+ ok: false,
921
+ status: response.status,
922
+ reason
923
+ };
924
+ } catch (err) {
925
+ console.error("Leapio - could not associate group");
926
+ return {
927
+ ok: false,
928
+ reason: "network_error"
929
+ };
930
+ }
931
+ };
932
+ /**
933
+ * POST the current consent state to the backend `/consent` endpoint.
934
+ *
935
+ * Body shape:
936
+ * { anonymous_id?, user_id?, analytics?, functional?, marketing?,
937
+ * doNotSellShare?, gpc?, gppDetected?, gppString?, source, sourceMetadata? }
938
+ *
939
+ * Sent with the `x-leapio-pk` header like the other public-API calls. Only
940
+ * fields that are present in the snapshot are included so the server can tell
941
+ * "not specified" apart from an explicit `false`.
942
+ */
943
+ const postConsent = async (config, payload) => {
944
+ var _payload$identityVeri;
945
+ const { categories, signals, source, sourceMetadata } = payload.snapshot;
946
+ const body = { source };
947
+ if (payload.anonymousId) body.anonymous_id = payload.anonymousId;
948
+ if (payload.userId) body.user_id = payload.userId;
949
+ if (typeof categories.analytics === "boolean") body.analytics = categories.analytics;
950
+ if (typeof categories.functional === "boolean") body.functional = categories.functional;
951
+ if (typeof categories.marketing === "boolean") body.marketing = categories.marketing;
952
+ if (typeof signals.doNotSellShare === "boolean") body.doNotSellShare = signals.doNotSellShare;
953
+ if (typeof signals.gpc === "boolean") body.gpc = signals.gpc;
954
+ if (typeof signals.gppDetected === "boolean") body.gppDetected = signals.gppDetected;
955
+ if (typeof signals.gppString === "string") body.gppString = signals.gppString;
956
+ if (sourceMetadata) body.sourceMetadata = sourceMetadata;
957
+ if ((_payload$identityVeri = payload.identityVerification) === null || _payload$identityVeri === void 0 ? void 0 : _payload$identityVeri.token) body.identity_verification = { token: payload.identityVerification.token };
958
+ const endpoint = consentUrlFromConfig(config);
959
+ if (!endpoint) return {
960
+ ok: false,
961
+ reason: "no_endpoint"
962
+ };
963
+ try {
964
+ const response = await fetch(endpoint, {
965
+ method: "POST",
966
+ headers: {
967
+ "Content-Type": "application/json",
968
+ "x-leapio-pk": config.publicKey
969
+ },
970
+ mode: "cors",
971
+ keepalive: true,
972
+ body: JSON.stringify(body)
973
+ });
974
+ if (response.ok) return {
975
+ ok: true,
976
+ status: response.status
977
+ };
978
+ let reason;
979
+ try {
980
+ const errorBody = await response.json();
981
+ reason = typeof (errorBody === null || errorBody === void 0 ? void 0 : errorBody.message) === "string" ? errorBody.message : void 0;
982
+ } catch (_unused5) {}
983
+ console.error("Leapio - consent push failed", response.status, reason);
984
+ return {
985
+ ok: false,
986
+ status: response.status,
987
+ reason
988
+ };
989
+ } catch (err) {
990
+ console.error("Leapio - could not push consent");
991
+ return {
992
+ ok: false,
993
+ reason: "network_error"
994
+ };
995
+ }
996
+ };
997
+ //#endregion
998
+ //#region src/utils.ts
999
+ const removeQueryParamsFromUrl = (url, params) => {
1000
+ try {
1001
+ const hashIndex = url.indexOf("#");
1002
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
1003
+ const [base, queryString] = (hashIndex >= 0 ? url.slice(0, hashIndex) : url).split("?");
1004
+ if (!queryString) return `${base}${hash}`;
1005
+ const cleaned = Array.from(new URLSearchParams(queryString).entries()).filter(([param]) => !params.includes(param));
1006
+ if (!cleaned.length) return `${base}${hash}`;
1007
+ const queryParams = new URLSearchParams();
1008
+ for (const [param, value] of cleaned) queryParams.append(param, value);
1009
+ return `${base}?${queryParams}${hash}`;
1010
+ } catch (err) {
1011
+ return url;
1012
+ }
1013
+ };
1014
+ const b64EncodeUnicode = (str) => {
1015
+ return btoa(encodeURIComponent(str));
1016
+ };
1017
+ const UnicodeDecodeB64 = (str) => {
1018
+ return decodeURIComponent(atob(str));
1019
+ };
1020
+ //#endregion
1021
+ //#region src/campaign.ts
1022
+ const CAMPAIGN_KEYWORDS = [
1023
+ "gclid",
1024
+ "fbclid",
1025
+ "msclkid",
1026
+ "utm_source",
1027
+ "utm_id",
1028
+ "utm_medium",
1029
+ "utm_campaign",
1030
+ "utm_content",
1031
+ "utm_term"
1032
+ ];
1033
+ const LINK_TAGS = [...CAMPAIGN_KEYWORDS, "ref"];
1034
+ const CAMPAIGN_COOKIE = "leapio_c";
1035
+ const LEGACY_CAMPAIGN_COOKIE = "lgspt_c";
1036
+ const LEGACY_FIRST_TOUCH_COOKIE = "lgspt_ft";
1037
+ const FIRST_TOUCH_COOKIE = "leapio_ft";
1038
+ const DEFAULT_EXPIRATION = 604800;
1039
+ const FIRST_TOUCH_EXPIRATION = 31536e3;
1040
+ var Campaign = class {
1041
+ constructor(sdkConfig) {
1042
+ this.sdkConfig = sdkConfig;
1043
+ const params = this._hasLinkTag() ? this._getParams() : this._getCampaignCookie();
1044
+ if (sdkConfig.stickyCampaigns) this._setCampaignCookie(params);
1045
+ this.campaignParams = params;
1046
+ this._captureFirstTouch();
1047
+ }
1048
+ getCampaignParams() {
1049
+ const params = this._getParams();
1050
+ if (this.sdkConfig.stickyCampaigns && !this._hasLinkTag()) return this.campaignParams;
1051
+ return params;
1052
+ }
1053
+ /**
1054
+ * The durable first-touch params captured on the visitor's first campaign
1055
+ * visit (UTM/click-ids + referrer + landing url). Empty object when no
1056
+ * first-touch has been recorded (pure direct traffic so far). Read-only — the
1057
+ * cookie is never overwritten once set.
1058
+ */
1059
+ getFirstTouchParams() {
1060
+ return this._getFirstTouchCookie();
1061
+ }
1062
+ _getCampaignCookie() {
1063
+ try {
1064
+ var _getCookie;
1065
+ const cookie = (_getCookie = getCookie(CAMPAIGN_COOKIE)) !== null && _getCookie !== void 0 ? _getCookie : getCookie(LEGACY_CAMPAIGN_COOKIE);
1066
+ return cookie ? JSON.parse(UnicodeDecodeB64(cookie)) : {};
1067
+ } catch (_unused) {
1068
+ return {};
1069
+ }
1070
+ }
1071
+ _setCampaignCookie(campaignParams) {
1072
+ var _this$sdkConfig$stick, _this$sdkConfig$cooki;
1073
+ setCookie({
1074
+ name: CAMPAIGN_COOKIE,
1075
+ value: b64EncodeUnicode(JSON.stringify(campaignParams)),
1076
+ expiresInSeconds: (_this$sdkConfig$stick = this.sdkConfig.stickyCampaignsExpirationInSeconds) !== null && _this$sdkConfig$stick !== void 0 ? _this$sdkConfig$stick : DEFAULT_EXPIRATION,
1077
+ domain: (_this$sdkConfig$cooki = this.sdkConfig.cookieDomain) !== null && _this$sdkConfig$cooki !== void 0 ? _this$sdkConfig$cooki : null
1078
+ });
1079
+ }
1080
+ _hasLinkTag() {
1081
+ const query = new URLSearchParams(window.location.search);
1082
+ return LINK_TAGS.some((key) => Boolean(query.get(key)));
1083
+ }
1084
+ _getParams() {
1085
+ const urlSearchParams = new URLSearchParams(window.location.search);
1086
+ const queryParams = Object.fromEntries(urlSearchParams.entries());
1087
+ return CAMPAIGN_KEYWORDS.reduce((acc, next) => {
1088
+ var _queryParams$next;
1089
+ if ((_queryParams$next = queryParams[next]) === null || _queryParams$next === void 0 ? void 0 : _queryParams$next.length) return _objectSpread2(_objectSpread2({}, acc), {}, { [next]: queryParams[next] });
1090
+ return acc;
1091
+ }, {});
1092
+ }
1093
+ _getFirstTouchCookie() {
1094
+ try {
1095
+ var _getCookie2;
1096
+ const cookie = (_getCookie2 = getCookie(FIRST_TOUCH_COOKIE)) !== null && _getCookie2 !== void 0 ? _getCookie2 : getCookie(LEGACY_FIRST_TOUCH_COOKIE);
1097
+ return cookie ? JSON.parse(UnicodeDecodeB64(cookie)) : {};
1098
+ } catch (_unused2) {
1099
+ return {};
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Persist the first-touch snapshot exactly once. A first-touch is recorded
1104
+ * only when the visit carries a campaign signal (any UTM/click-id) or a
1105
+ * referrer — pure direct first visits do not lock in an empty first-touch, so
1106
+ * a later campaign visit can still become the first-touch.
1107
+ */
1108
+ _captureFirstTouch() {
1109
+ var _this$sdkConfig$cooki2;
1110
+ const existing = this._getFirstTouchCookie();
1111
+ if (Object.keys(existing).length > 0) return;
1112
+ const params = this._getParams();
1113
+ const rawReferrer = typeof document !== "undefined" ? document.referrer || "" : "";
1114
+ const referrer = this._isExternalReferrer(rawReferrer) ? rawReferrer : "";
1115
+ const landingUrl = typeof window !== "undefined" && window.location ? window.location.href : "";
1116
+ const hasCampaignSignal = Object.keys(params).length > 0;
1117
+ const hasReferrer = referrer.length > 0;
1118
+ if (!hasCampaignSignal && !hasReferrer) return;
1119
+ const firstTouch = _objectSpread2({}, params);
1120
+ if (hasReferrer) firstTouch.referrer = referrer;
1121
+ if (landingUrl) firstTouch.landing_url = landingUrl;
1122
+ setCookie({
1123
+ name: FIRST_TOUCH_COOKIE,
1124
+ value: b64EncodeUnicode(JSON.stringify(firstTouch)),
1125
+ expiresInSeconds: FIRST_TOUCH_EXPIRATION,
1126
+ domain: (_this$sdkConfig$cooki2 = this.sdkConfig.cookieDomain) !== null && _this$sdkConfig$cooki2 !== void 0 ? _this$sdkConfig$cooki2 : null
1127
+ });
1128
+ }
1129
+ _isExternalReferrer(referrer) {
1130
+ if (!referrer) return false;
1131
+ try {
1132
+ return new URL(referrer).host !== window.location.host;
1133
+ } catch (_unused3) {
1134
+ return true;
1135
+ }
1136
+ }
1137
+ };
1138
+ //#endregion
1139
+ //#region src/class-listener.ts
1140
+ const LEGACY_CLASS_PREFIX = "lgspt-";
1141
+ /**
1142
+ * Attach the always-on `lgspt-` classname click listener. Returns a teardown
1143
+ * that removes the listener (invoked on `reset()`); the listener semantics are
1144
+ * otherwise unchanged from the historical always-on behavior.
1145
+ */
1146
+ const listenToClassClicks = (trackClick) => {
1147
+ const onClick = (event) => {
1148
+ const target = event.target;
1149
+ if (new RegExp(LEGACY_CLASS_PREFIX).test(target.className) === false) return;
1150
+ const classList = target.className.split(" ").filter((className) => className.startsWith(LEGACY_CLASS_PREFIX));
1151
+ if (classList.length === 0) return;
1152
+ trackClick({ name: classList[0].replace(LEGACY_CLASS_PREFIX, "").split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ") });
1153
+ };
1154
+ document.addEventListener("click", onClick);
1155
+ return () => document.removeEventListener("click", onClick);
1156
+ };
1157
+ //#endregion
1158
+ //#region src/consent-sources/listen.ts
1159
+ const listenToWindowEvents = (events, onEvent) => {
1160
+ const attached = [];
1161
+ for (const eventName of events) try {
1162
+ window.addEventListener(eventName, onEvent);
1163
+ attached.push(() => window.removeEventListener(eventName, onEvent));
1164
+ } catch (_unused) {}
1165
+ return () => {
1166
+ for (const off of attached) try {
1167
+ off();
1168
+ } catch (_unused2) {}
1169
+ };
1170
+ };
1171
+ //#endregion
1172
+ //#region src/consent-sources/concord.ts
1173
+ /**
1174
+ * Concord CMP adapter (concord.tech). Read-only. Sits high on the source
1175
+ * ladder (`concord`, just below the explicit API).
1176
+ *
1177
+ * Contract per the public docs:
1178
+ * https://www.concord.tech/docs/manually-blocking-trackers-before-consent
1179
+ *
1180
+ * window.concord.consent.hasConsent(category: string): boolean
1181
+ * — reads `store.consentState.allowState[category] === true`. Definitive
1182
+ * under Concord's enforcement model: `false` means "not granted (yet)",
1183
+ * which is exactly what Concord itself uses to gate trackers, so we
1184
+ * mirror it verbatim.
1185
+ * window.concord.consent.hasDoNotSell(): boolean
1186
+ * — `allowState.do_not_sell === true`. The adapter reports this signal
1187
+ * FAITHFULLY whenever it is active — signals and categories are
1188
+ * independent facts and the adapter is a reader, not an interpreter.
1189
+ * SEMANTICS NOTE: `marketing: true` +
1190
+ * `do_not_sell: true` almost always means DNS/GPC was detected and
1191
+ * auto-declined marketing, and the user then manually flipped
1192
+ * marketing back on. Either way the CMP result is trusted AS-IS — how
1193
+ * marketing got to true is Concord's business. The resolution happens
1194
+ * DOWNSTREAM in Leapio's export evaluation (a source-reported
1195
+ * `marketing: true` in current consent overrides `doNotSellShare`;
1196
+ * DNS wins only when no source ever spoke for marketing), so both
1197
+ * facts must arrive intact.
1198
+ * window.concord.consent.hasGPC(): boolean
1199
+ * — true when the `global_privacy_control` consent record is accepted
1200
+ * (i.e. Concord detected and applied GPC). Feeds Leapio's `gpc`
1201
+ * signal.
1202
+ * window.concord.consent.isDefaults: boolean (proxy getter)
1203
+ * — true while the state is region-template defaults (no real user
1204
+ * decision yet). Recorded as sourceMetadata so Leapio can tell
1205
+ * default-derived consent from an actual user decision.
1206
+ * window.concord.consent.onConsent(category, cb)
1207
+ * — one-shot grant hook; we rely on the window events below instead,
1208
+ * which also cover revocations.
1209
+ *
1210
+ * Window events:
1211
+ * concord-ready — fully initialized
1212
+ * concord-consent-state-loading — fetching consent state (reads unsafe)
1213
+ * concord-consent-state-loaded — consent state retrieved
1214
+ * concord-consent-state-changed — user updated preferences
1215
+ * concord-unavailable — Concord aborted (it also sets
1216
+ * window.concord = undefined)
1217
+ *
1218
+ * Category mapping is IDENTITY: Concord's canonical categories are already
1219
+ * `analytics` / `functional` / `marketing` — `hasConsent` is called with our
1220
+ * category names directly.
1221
+ *
1222
+ * The signal helpers (`hasDoNotSell`/`hasGPC`) and `isDefaults` are guarded as
1223
+ * optional — older Concord builds that only ship `hasConsent` still work,
1224
+ * they just emit categories only.
1225
+ */
1226
+ const SOURCE_NAME$4 = "concord";
1227
+ const PRIORITY$4 = 80;
1228
+ const CATEGORIES = [
1229
+ "analytics",
1230
+ "functional",
1231
+ "marketing"
1232
+ ];
1233
+ const getConcordConsentApi = () => {
1234
+ try {
1235
+ var _win$concord;
1236
+ const consent = (_win$concord = window.concord) === null || _win$concord === void 0 ? void 0 : _win$concord.consent;
1237
+ if (consent && typeof consent === "object" && typeof consent.hasConsent === "function") return consent;
1238
+ return null;
1239
+ } catch (_unused) {
1240
+ return null;
1241
+ }
1242
+ };
1243
+ const STATE_READY_EVENTS = [
1244
+ "concord-ready",
1245
+ "concord-consent-state-loaded",
1246
+ "concord-consent-state-changed"
1247
+ ];
1248
+ /**
1249
+ * Construct the Concord CMP source. `read()` yields `null` until
1250
+ * `window.concord.consent` is present (and not mid-load / aborted — see the
1251
+ * loading/unavailable guards, which only engage once `subscribe()` has
1252
+ * observed the corresponding events).
1253
+ */
1254
+ const createConcordSource = () => {
1255
+ let stateLoading = false;
1256
+ let unavailable = false;
1257
+ return {
1258
+ name: SOURCE_NAME$4,
1259
+ priority: PRIORITY$4,
1260
+ read() {
1261
+ try {
1262
+ if (unavailable || stateLoading) return null;
1263
+ const api = getConcordConsentApi();
1264
+ if (!api) return null;
1265
+ const categories = {};
1266
+ for (const category of CATEGORIES) categories[category] = api.hasConsent(category) === true;
1267
+ const signals = {};
1268
+ const sourceMetadata = {};
1269
+ if (typeof api.hasDoNotSell === "function" && api.hasDoNotSell()) signals.doNotSellShare = true;
1270
+ if (typeof api.hasGPC === "function" && api.hasGPC() === true) signals.gpc = true;
1271
+ if (typeof api.isDefaults === "boolean") sourceMetadata.isDefaults = api.isDefaults;
1272
+ const snapshot = {
1273
+ categories,
1274
+ signals,
1275
+ source: SOURCE_NAME$4,
1276
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1277
+ };
1278
+ if (Object.keys(sourceMetadata).length > 0) snapshot.sourceMetadata = sourceMetadata;
1279
+ return snapshot;
1280
+ } catch (_unused2) {
1281
+ return null;
1282
+ }
1283
+ },
1284
+ subscribe(cb) {
1285
+ const notify = () => {
1286
+ try {
1287
+ cb();
1288
+ } catch (_unused3) {}
1289
+ };
1290
+ const offReady = listenToWindowEvents(STATE_READY_EVENTS, () => {
1291
+ stateLoading = false;
1292
+ unavailable = false;
1293
+ notify();
1294
+ });
1295
+ const offLoading = listenToWindowEvents(["concord-consent-state-loading"], () => {
1296
+ stateLoading = true;
1297
+ });
1298
+ const offUnavailable = listenToWindowEvents(["concord-unavailable"], () => {
1299
+ unavailable = true;
1300
+ });
1301
+ return () => {
1302
+ offReady();
1303
+ offLoading();
1304
+ offUnavailable();
1305
+ };
1306
+ }
1307
+ };
1308
+ };
1309
+ //#endregion
1310
+ //#region src/consent-sources/gcm-reader.ts
1311
+ /**
1312
+ * Google Consent Mode v2 READER.
1313
+ *
1314
+ * Strictly read-only: this source reads the current Google Consent Mode state
1315
+ * that some *other* tool on the page (a CMP, gtag bootstrap, GTM) has set, and
1316
+ * maps it onto Leapio's categories. It NEVER calls `gtag('consent', ...)` or
1317
+ * mutates Google state in any way — Leapio is a consumer of consent, never a
1318
+ * writer into Google.
1319
+ *
1320
+ * ## Where the state is read from
1321
+ *
1322
+ * gtag's bootstrap is `function gtag(){dataLayer.push(arguments)}`, so every
1323
+ * `gtag('consent','default'|'update', {...})` pushes the function's *arguments*
1324
+ * object (array-like: `entry[0]='consent'`, `entry[1]='default'|'update'`,
1325
+ * `entry[2]={signal:'granted'|'denied', ...}`) onto `window.dataLayer`. We scan
1326
+ * `dataLayer` in order and fold every consent entry into a per-signal map; later
1327
+ * entries win, so an `update` issued after the user chooses supersedes the
1328
+ * initial `default` (Google docs: "effective state = update when present, else
1329
+ * default").
1330
+ * Source: https://developers.google.com/tag-platform/security/guides/consent
1331
+ *
1332
+ * As a best-effort *fallback* (when no scannable dataLayer entries exist, e.g. a
1333
+ * CMP registered the default internally), we read the internal consent state at
1334
+ * `window.google_tag_data.ics`. That object is INTERNAL/UNDOCUMENTED (it does
1335
+ * not appear in any developers.google.com page) and may break across gtag.js
1336
+ * releases, so it is only consulted when the documented dataLayer scan yields
1337
+ * nothing.
1338
+ * Source (non-authoritative, flagged): https://gunnargriese.com/posts/consent-mode-v2/
1339
+ *
1340
+ * ## Mapping
1341
+ *
1342
+ * analytics_storage -> analytics
1343
+ * functionality_storage + personalization_storage -> functional (AND)
1344
+ * ad_storage + ad_user_data + ad_personalization -> marketing (AND)
1345
+ *
1346
+ * ### Why AND across the functional/marketing groups
1347
+ *
1348
+ * Multiple GCM signals map onto a single Leapio category. GCM signals
1349
+ * are independently grantable, so a group can be split (e.g. `ad_storage`
1350
+ * granted but `ad_personalization` denied). Leapio categories gate downstream
1351
+ * *use* — granting `marketing` authorizes advertising use of the data. We choose
1352
+ * the CONSERVATIVE / least-privilege reading: a Leapio category is granted only
1353
+ * when EVERY GCM signal that maps to it is `granted`. If any contributing signal
1354
+ * is denied, the category is denied. This guarantees we never over-permit
1355
+ * marketing/functional use relative to what the user told Google. Verified
1356
+ * against Google's GCM v2 semantics (each signal carries an independent
1357
+ * granted/denied value; there is no documented roll-up), so the AND choice is a
1358
+ * deliberate policy decision, not an inference about Google's own roll-up.
1359
+ *
1360
+ * A signal that is *absent entirely* (never set) is treated as "unknown" and
1361
+ * does not by itself deny the category — only an explicit `denied` denies. A
1362
+ * category is reported only when at least one of its contributing signals has an
1363
+ * explicit value; categories with no information stay absent from the snapshot
1364
+ * (so they fall through to the org's `consentBehavior` server-side, exactly like
1365
+ * any other source that "never spoke" about a category).
1366
+ */
1367
+ const SOURCE_NAME$3 = "gcm";
1368
+ const PRIORITY$3 = 40;
1369
+ /** The GCM signals each Leapio category is composed from (conservative AND). */
1370
+ const CATEGORY_SIGNALS = {
1371
+ analytics: ["analytics_storage"],
1372
+ functional: ["functionality_storage", "personalization_storage"],
1373
+ marketing: [
1374
+ "ad_storage",
1375
+ "ad_user_data",
1376
+ "ad_personalization"
1377
+ ]
1378
+ };
1379
+ const ALL_SIGNALS = [
1380
+ "analytics_storage",
1381
+ "functionality_storage",
1382
+ "personalization_storage",
1383
+ "ad_storage",
1384
+ "ad_user_data",
1385
+ "ad_personalization"
1386
+ ];
1387
+ const isGcmValue = (value) => value === "granted" || value === "denied";
1388
+ /**
1389
+ * Fold the `window.dataLayer` consent entries into a per-signal value map.
1390
+ * Scans in push order so later (`update`) entries override earlier (`default`)
1391
+ * ones on a per-signal basis. Region scoping is ignored in V1: every matching
1392
+ * `default`/`update` is applied in order, so the last writer for each signal
1393
+ * wins (a documented simplification — region-specific defaults exist but are not
1394
+ * resolved here). Returns `null` when no consent entries are present.
1395
+ */
1396
+ const readFromDataLayer = () => {
1397
+ let dataLayer;
1398
+ try {
1399
+ dataLayer = window.dataLayer;
1400
+ } catch (_unused) {
1401
+ return null;
1402
+ }
1403
+ if (!Array.isArray(dataLayer)) return null;
1404
+ const signals = {};
1405
+ let sawConsentEntry = false;
1406
+ for (const entry of dataLayer) {
1407
+ if (!entry || typeof entry !== "object") continue;
1408
+ const arrayLike = entry;
1409
+ if (typeof arrayLike.length !== "number" || arrayLike.length < 3) continue;
1410
+ if (arrayLike[0] !== "consent") continue;
1411
+ const mode = arrayLike[1];
1412
+ if (mode !== "default" && mode !== "update") continue;
1413
+ const params = arrayLike[2];
1414
+ if (!params || typeof params !== "object") continue;
1415
+ sawConsentEntry = true;
1416
+ for (const signal of ALL_SIGNALS) {
1417
+ const value = params[signal];
1418
+ if (isGcmValue(value)) signals[signal] = value;
1419
+ }
1420
+ }
1421
+ return sawConsentEntry ? signals : null;
1422
+ };
1423
+ /**
1424
+ * Best-effort fallback: read the internal consent state at
1425
+ * `window.google_tag_data.ics`. INTERNAL/UNDOCUMENTED — only consulted when the
1426
+ * documented dataLayer scan yields nothing. The `ics.entries` map is keyed by
1427
+ * signal; each entry exposes `update`/`default` (update supersedes) and/or a
1428
+ * `getConsentState(signal)` returning `1` (granted) / `0` (denied). We read
1429
+ * conservatively and tolerate any shape mismatch.
1430
+ */
1431
+ const readFromInternalState = () => {
1432
+ let ics;
1433
+ try {
1434
+ var _google_tag_data;
1435
+ ics = (_google_tag_data = window.google_tag_data) === null || _google_tag_data === void 0 ? void 0 : _google_tag_data.ics;
1436
+ } catch (_unused2) {
1437
+ return null;
1438
+ }
1439
+ if (!ics || typeof ics !== "object") return null;
1440
+ const signals = {};
1441
+ let sawAny = false;
1442
+ const entries = ics.entries;
1443
+ if (entries && typeof entries === "object") for (const signal of ALL_SIGNALS) {
1444
+ const entry = entries[signal];
1445
+ if (!entry || typeof entry !== "object") continue;
1446
+ const record = entry;
1447
+ const raw = record.update !== void 0 ? record.update : record.default;
1448
+ if (raw === true) {
1449
+ signals[signal] = "granted";
1450
+ sawAny = true;
1451
+ } else if (raw === false) {
1452
+ signals[signal] = "denied";
1453
+ sawAny = true;
1454
+ } else if (isGcmValue(raw)) {
1455
+ signals[signal] = raw;
1456
+ sawAny = true;
1457
+ }
1458
+ }
1459
+ return sawAny ? signals : null;
1460
+ };
1461
+ /**
1462
+ * Compute one Leapio category from its GCM signals (conservative AND):
1463
+ * - `false` if ANY contributing signal is explicitly `denied`,
1464
+ * - `true` if ALL contributing signals are explicitly `granted`,
1465
+ * - `undefined` (category omitted) if no contributing signal has a value.
1466
+ */
1467
+ const resolveCategory = (category, signals) => {
1468
+ const contributing = CATEGORY_SIGNALS[category];
1469
+ let sawAny = false;
1470
+ let allGranted = true;
1471
+ for (const signal of contributing) {
1472
+ const value = signals[signal];
1473
+ if (value === void 0) {
1474
+ allGranted = false;
1475
+ continue;
1476
+ }
1477
+ sawAny = true;
1478
+ if (value === "denied") return false;
1479
+ }
1480
+ if (!sawAny || !allGranted) return void 0;
1481
+ return true;
1482
+ };
1483
+ const buildSnapshot$2 = (signals) => {
1484
+ const categories = {};
1485
+ let sawAny = false;
1486
+ Object.keys(CATEGORY_SIGNALS).forEach((category) => {
1487
+ const resolved = resolveCategory(category, signals);
1488
+ if (resolved !== void 0) {
1489
+ categories[category] = resolved;
1490
+ sawAny = true;
1491
+ }
1492
+ });
1493
+ if (!sawAny) return null;
1494
+ return {
1495
+ categories,
1496
+ signals: {},
1497
+ source: SOURCE_NAME$3,
1498
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1499
+ sourceMetadata: { gcm: signals }
1500
+ };
1501
+ };
1502
+ /** Read the current GCM state: documented dataLayer scan, then internal fallback. */
1503
+ const readGcmState = () => {
1504
+ var _readFromDataLayer;
1505
+ return (_readFromDataLayer = readFromDataLayer()) !== null && _readFromDataLayer !== void 0 ? _readFromDataLayer : readFromInternalState();
1506
+ };
1507
+ /**
1508
+ * Construct the Google Consent Mode reader source. Always returns a source; its
1509
+ * `read()` yields `null` until consent entries are observable (so the registry
1510
+ * keeps it registered and re-reads on subscribe callbacks safely even when GCM
1511
+ * is not yet present).
1512
+ */
1513
+ const createGcmReaderSource = () => {
1514
+ return {
1515
+ name: SOURCE_NAME$3,
1516
+ priority: PRIORITY$3,
1517
+ read() {
1518
+ try {
1519
+ const signals = readGcmState();
1520
+ if (!signals) return null;
1521
+ return buildSnapshot$2(signals);
1522
+ } catch (_unused3) {
1523
+ return null;
1524
+ }
1525
+ },
1526
+ subscribe(cb) {
1527
+ let dataLayer;
1528
+ try {
1529
+ const win = window;
1530
+ if (!Array.isArray(win.dataLayer)) {
1531
+ var _win$dataLayer;
1532
+ win.dataLayer = (_win$dataLayer = win.dataLayer) !== null && _win$dataLayer !== void 0 ? _win$dataLayer : [];
1533
+ }
1534
+ dataLayer = win.dataLayer;
1535
+ } catch (_unused4) {
1536
+ return () => {};
1537
+ }
1538
+ if (!Array.isArray(dataLayer)) return () => {};
1539
+ const arr = dataLayer;
1540
+ const originalPush = arr.push;
1541
+ const wrapped = (...items) => {
1542
+ const result = originalPush.apply(arr, items);
1543
+ for (const item of items) {
1544
+ const arrayLike = item;
1545
+ if (arrayLike && typeof arrayLike === "object" && arrayLike[0] === "consent") {
1546
+ try {
1547
+ cb();
1548
+ } catch (_unused5) {}
1549
+ break;
1550
+ }
1551
+ }
1552
+ return result;
1553
+ };
1554
+ try {
1555
+ arr.push = wrapped;
1556
+ } catch (_unused6) {
1557
+ return () => {};
1558
+ }
1559
+ return () => {
1560
+ try {
1561
+ if (arr.push === wrapped) arr.push = originalPush;
1562
+ } catch (_unused7) {}
1563
+ };
1564
+ }
1565
+ };
1566
+ };
1567
+ //#endregion
1568
+ //#region src/consent-sources/onetrust.ts
1569
+ /**
1570
+ * OneTrust Cookie Consent adapter. Read-only.
1571
+ *
1572
+ * ## API surface (verified against OneTrust developer docs)
1573
+ *
1574
+ * - `window.OnetrustActiveGroups` — a comma-DELIMITED string of the currently
1575
+ * active category group IDs, with LEADING and TRAILING commas, e.g.
1576
+ * `,C0001,C0002,C0004,`. The bounding commas exist precisely so callers can
1577
+ * test membership with `.indexOf(',C0002,')` without `C0002` false-matching
1578
+ * `C0012`.
1579
+ * Source: https://developer.onetrust.com/onetrust/docs/google-tag-manager
1580
+ * Source: https://my.onetrust.com/s/article/UUID-66bcaaf1-c7ca-5f32-6760-c75a1337c226
1581
+ *
1582
+ * - `OneTrustGroupsUpdated` window event — dispatched both on initial script
1583
+ * load AND whenever the user updates consent; `event.detail` is an array of
1584
+ * active group IDs (e.g. `["C0001","C0002"]`). This is the cleanest change
1585
+ * hook (no string parsing, no load-order dependency), so we subscribe to it.
1586
+ * Source: https://developer.onetrust.com/onetrust/docs/javascript-events-guide
1587
+ *
1588
+ * - `window.OneTrust.OnConsentChanged(cb)` — a zero-argument change callback;
1589
+ * used as a fallback subscription when the window event is unavailable. Inside
1590
+ * it we re-read `OnetrustActiveGroups`.
1591
+ * Source: https://developer.onetrust.com/onetrust/docs/javascript-api
1592
+ *
1593
+ * - `OneTrust.IsAlertBoxClosed()`: true when the OptanonAlertBoxClosed cookie is set and consent is valid; that cookie is written on Accept All, Reject All, Close (banner), and Save Settings. Sources: https://developer.onetrust.com/onetrust/docs/javascript-api and https://my.onetrust.com/s/article/UUID-2dc719a8-4be5-8d16-1dc8-c7b4147b88e0
1594
+ *
1595
+ * ## Category mapping (OneTrust default group IDs -> Leapio categories)
1596
+ *
1597
+ * C0001 Strictly Necessary -> (no Leapio category; always-on, not mapped)
1598
+ * C0002 Performance -> analytics // OneTrust's own GCM example maps C0002 -> analytics_storage
1599
+ * C0003 Functional -> functional
1600
+ * C0004 Targeting -> marketing // OneTrust maps C0004 -> ad_storage/ad_* in its GCM example
1601
+ * C0005 Social Media -> marketing // social media = cross-site advertising/profiling
1602
+ * Source (defaults + GCM mapping precedent): https://developer.onetrust.com/onetrust/docs/javascript-api
1603
+ *
1604
+ * CAVEAT: C0001–C0005 are OneTrust's OUT-OF-THE-BOX defaults and are
1605
+ * tenant-CUSTOMIZABLE (group IDs and even category labels can be changed per
1606
+ * tenant). We map the standard defaults; tenants using custom group IDs would
1607
+ * need a configurable mapping (a documented V1 limitation). Unknown/custom group
1608
+ * IDs are simply ignored (they grant no Leapio category).
1609
+ *
1610
+ * We report a category as `true` when its mapped group is active. We do NOT emit
1611
+ * `false` for inactive groups: OneTrust's active-groups string lists only what
1612
+ * is *granted*, and after a consent decision a group's absence means denied —
1613
+ * but on first load (opt-in model) only C0001 is present and the rest are
1614
+ * "pending". To stay conservative and avoid asserting a denial the user has not
1615
+ * made, an absent mapped group leaves that category UNSET (falls through to the
1616
+ * org's consentBehavior server-side). Once at least one non-necessary group is
1617
+ * active we treat absent mapped groups as explicit denials, since that indicates
1618
+ * the banner has been resolved. When only necessary groups are active, `IsAlertBoxClosed()` (with the OptanonAlertBoxClosed cookie as fallback when the API object is absent) disambiguates:
1619
+ * true means the banner was resolved (Reject All / dismissed), so we emit an all-false denial snapshot; false or absent keeps the null pre-decision fallthrough.
1620
+ */
1621
+ const SOURCE_NAME$2 = "onetrust";
1622
+ const PRIORITY$2 = 60;
1623
+ /** Default OneTrust group ID -> Leapio category. C0001 (necessary) is unmapped. */
1624
+ const GROUP_TO_CATEGORY = {
1625
+ C0002: "analytics",
1626
+ C0003: "functional",
1627
+ C0004: "marketing",
1628
+ C0005: "marketing"
1629
+ };
1630
+ const getActiveGroupsString = () => {
1631
+ try {
1632
+ const value = window.OnetrustActiveGroups;
1633
+ return typeof value === "string" ? value : null;
1634
+ } catch (_unused) {
1635
+ return null;
1636
+ }
1637
+ };
1638
+ const getOneTrust = () => {
1639
+ try {
1640
+ const ot = window.OneTrust;
1641
+ return ot && typeof ot === "object" ? ot : null;
1642
+ } catch (_unused2) {
1643
+ return null;
1644
+ }
1645
+ };
1646
+ const isAlertBoxClosed = () => {
1647
+ try {
1648
+ const ot = getOneTrust();
1649
+ if (ot && typeof ot.IsAlertBoxClosed === "function") return ot.IsAlertBoxClosed() === true;
1650
+ } catch (_unused3) {
1651
+ return false;
1652
+ }
1653
+ try {
1654
+ return document.cookie.indexOf("OptanonAlertBoxClosed=") !== -1;
1655
+ } catch (_unused4) {
1656
+ return false;
1657
+ }
1658
+ };
1659
+ /** Parse the comma-bounded active-groups string into a Set of group IDs. */
1660
+ const parseActiveGroups = (raw) => {
1661
+ return new Set(raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0));
1662
+ };
1663
+ const buildSnapshot$1 = (raw) => {
1664
+ const active = parseActiveGroups(raw);
1665
+ let anyNonNecessaryActive = false;
1666
+ let anyNonNecessaryActiveRaw = false;
1667
+ active.forEach((id) => {
1668
+ if (id !== "C0001") {
1669
+ anyNonNecessaryActiveRaw = true;
1670
+ if (GROUP_TO_CATEGORY[id]) anyNonNecessaryActive = true;
1671
+ }
1672
+ });
1673
+ const categories = {};
1674
+ for (const [groupId, category] of Object.entries(GROUP_TO_CATEGORY)) if (active.has(groupId)) categories[category] = true;
1675
+ else if (anyNonNecessaryActive && categories[category] !== true) categories[category] = false;
1676
+ if (Object.keys(categories).length === 0) {
1677
+ if (!isAlertBoxClosed()) return null;
1678
+ if (anyNonNecessaryActiveRaw) return null;
1679
+ return {
1680
+ categories: {
1681
+ analytics: false,
1682
+ functional: false,
1683
+ marketing: false
1684
+ },
1685
+ signals: {},
1686
+ source: SOURCE_NAME$2,
1687
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1688
+ sourceMetadata: {
1689
+ activeGroups: Array.from(active),
1690
+ alertBoxClosed: true
1691
+ }
1692
+ };
1693
+ }
1694
+ return {
1695
+ categories,
1696
+ signals: {},
1697
+ source: SOURCE_NAME$2,
1698
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1699
+ sourceMetadata: { activeGroups: Array.from(active) }
1700
+ };
1701
+ };
1702
+ /**
1703
+ * Construct the OneTrust source. Always returns a source; `read()` yields `null`
1704
+ * until `OnetrustActiveGroups` is present, so the registry registers it safely
1705
+ * even before the OneTrust script loads.
1706
+ */
1707
+ const createOneTrustSource = () => {
1708
+ return {
1709
+ name: SOURCE_NAME$2,
1710
+ priority: PRIORITY$2,
1711
+ read() {
1712
+ try {
1713
+ const raw = getActiveGroupsString();
1714
+ if (raw === null) return null;
1715
+ return buildSnapshot$1(raw);
1716
+ } catch (_unused5) {
1717
+ return null;
1718
+ }
1719
+ },
1720
+ subscribe(cb) {
1721
+ const notify = () => {
1722
+ try {
1723
+ cb();
1724
+ } catch (_unused6) {}
1725
+ };
1726
+ const off = listenToWindowEvents(["OneTrustGroupsUpdated"], notify);
1727
+ try {
1728
+ const ot = getOneTrust();
1729
+ if (ot && typeof ot.OnConsentChanged === "function") ot.OnConsentChanged(notify);
1730
+ } catch (_unused7) {}
1731
+ return off;
1732
+ }
1733
+ };
1734
+ };
1735
+ //#endregion
1736
+ //#region src/consent-sources/usercentrics.ts
1737
+ /**
1738
+ * Usercentrics adapter. Read-only.
1739
+ *
1740
+ * ## API surface (verified against Usercentrics docs)
1741
+ *
1742
+ * Usercentrics has TWO browser API generations and we support both:
1743
+ *
1744
+ * - V2 (hosted "UC_UI" CMP): dispatches the `ucEvent` window event. Its
1745
+ * `e.detail.event === 'consent_status'` payload carries a per-category rollup
1746
+ * object `e.detail.ucCategory`, where each category slug maps to
1747
+ * `true` (all services consented) / `false` (all denied) / `null` (mixed).
1748
+ * Init is signalled by `UC_UI_INITIALIZED`.
1749
+ * Source: https://usercentrics.com/docs/web/features/v2/events_v2/events_v2/
1750
+ * Source: https://usercentrics.com/docs/web/features/events/consent-events/
1751
+ *
1752
+ * - V3 (latest "Web CMP", `window.__ucCmp`): dispatches the `UC_CONSENT` window
1753
+ * event whose `e.detail` is a `ConsentDetails` object. `detail.categories` is a
1754
+ * record keyed by category id; each value has a `state` of
1755
+ * `'ALL_DENIED' | 'SOME_ACCEPTED' | 'ALL_ACCEPTED'`. `window.__ucCmp` also
1756
+ * exposes `getConsentDetails(): Promise<ConsentDetails>` (async — not used in
1757
+ * the synchronous `read()`; we rely on the cached event detail instead).
1758
+ * Source: https://usercentrics.com/docs/web/features/api/control-functionality/
1759
+ * Source: https://usercentrics.com/docs/web/features/api/interfaces/
1760
+ *
1761
+ * Because `read()` must be synchronous and the hosted CMP has no synchronous
1762
+ * category getter, this adapter CACHES the most recent consent state seen on the
1763
+ * `ucEvent` / `UC_CONSENT` events (and on any pre-existing `window.__ucCmp`
1764
+ * detail captured at construction) and serves `read()` from that cache.
1765
+ *
1766
+ * ## Category mapping (Usercentrics slugs -> Leapio)
1767
+ *
1768
+ * Usercentrics categories are CONFIGURABLE per account, so there is no fixed
1769
+ * enum. The slugs guaranteed by Usercentrics' own examples are `essential`,
1770
+ * `functional`, `marketing` (+ `payment`, essential). We map by slug, matching
1771
+ * common conventions case-insensitively:
1772
+ *
1773
+ * essential / necessary -> (unmapped; always-on)
1774
+ * functional / preferences -> functional
1775
+ * marketing / advertising -> marketing
1776
+ * analytics / statistics / performance -> analytics
1777
+ *
1778
+ * CAVEAT: because categories are account-configurable, a tenant may use a slug
1779
+ * we don't recognize (it is then ignored — granted nothing). This is a
1780
+ * documented V1 limitation; a configurable mapping would be a follow-up.
1781
+ *
1782
+ * A category rollup of `null` (V2 "mixed") or `'SOME_ACCEPTED'` (V3) is treated
1783
+ * conservatively as NOT granted (we only grant on a definitive all-accepted).
1784
+ */
1785
+ const SOURCE_NAME$1 = "usercentrics";
1786
+ const PRIORITY$1 = 60;
1787
+ /** Normalized slug -> Leapio category. Keys are compared lower-cased. */
1788
+ const SLUG_TO_CATEGORY = {
1789
+ functional: "functional",
1790
+ preferences: "functional",
1791
+ marketing: "marketing",
1792
+ advertising: "marketing",
1793
+ analytics: "analytics",
1794
+ statistics: "analytics",
1795
+ performance: "analytics"
1796
+ };
1797
+ const mapSlug = (slug) => {
1798
+ return SLUG_TO_CATEGORY[slug.trim().toLowerCase()];
1799
+ };
1800
+ /**
1801
+ * Fold a per-slug grant map into Leapio categories. When multiple slugs map to
1802
+ * the same Leapio category (e.g. analytics + statistics), a grant wins over a
1803
+ * denial (most-permissive within a single mapped category), but an explicit
1804
+ * denial still records `false` if no mapped slug granted it.
1805
+ */
1806
+ const foldSlugGrants = (slugGrants) => {
1807
+ const categories = {};
1808
+ for (const { slug, granted } of slugGrants) {
1809
+ const category = mapSlug(slug);
1810
+ if (!category) continue;
1811
+ if (granted === true) categories[category] = true;
1812
+ else if (categories[category] !== true) categories[category] = false;
1813
+ }
1814
+ return categories;
1815
+ };
1816
+ /** Parse a V2 `ucEvent` detail (`ucCategory` rollup object). */
1817
+ const parseV2Detail = (detail) => {
1818
+ if (!detail || typeof detail !== "object") return null;
1819
+ const d = detail;
1820
+ if (d.event !== "consent_status") return null;
1821
+ const ucCategory = d.ucCategory;
1822
+ if (!ucCategory || typeof ucCategory !== "object") return null;
1823
+ const slugGrants = Object.entries(ucCategory).map(([slug, value]) => ({
1824
+ slug,
1825
+ granted: value === true ? true : value === false ? false : null
1826
+ }));
1827
+ const categories = foldSlugGrants(slugGrants);
1828
+ return Object.keys(categories).length > 0 ? categories : null;
1829
+ };
1830
+ /** Parse a V3 `UC_CONSENT` detail (`ConsentDetails.categories`). */
1831
+ const parseV3Detail = (detail) => {
1832
+ if (!detail || typeof detail !== "object") return null;
1833
+ const categoriesRecord = detail.categories;
1834
+ if (!categoriesRecord || typeof categoriesRecord !== "object") return null;
1835
+ const slugGrants = Object.entries(categoriesRecord).map(([slug, value]) => {
1836
+ const state = value && typeof value === "object" ? value.state : void 0;
1837
+ return {
1838
+ slug,
1839
+ granted: state === "ALL_ACCEPTED" ? true : state === "ALL_DENIED" ? false : null
1840
+ };
1841
+ });
1842
+ const categories = foldSlugGrants(slugGrants);
1843
+ return Object.keys(categories).length > 0 ? categories : null;
1844
+ };
1845
+ const parseAnyDetail = (detail) => {
1846
+ var _parseV3Detail;
1847
+ return (_parseV3Detail = parseV3Detail(detail)) !== null && _parseV3Detail !== void 0 ? _parseV3Detail : parseV2Detail(detail);
1848
+ };
1849
+ /**
1850
+ * Construct the Usercentrics source. Caches the latest consent state from the
1851
+ * vendor's events; `read()` returns `null` until a consent event has been seen.
1852
+ */
1853
+ const createUsercentricsSource = () => {
1854
+ let cached = null;
1855
+ return {
1856
+ name: SOURCE_NAME$1,
1857
+ priority: PRIORITY$1,
1858
+ read() {
1859
+ try {
1860
+ if (!cached || Object.keys(cached).length === 0) return null;
1861
+ return {
1862
+ categories: _objectSpread2({}, cached),
1863
+ signals: {},
1864
+ source: SOURCE_NAME$1,
1865
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1866
+ };
1867
+ } catch (_unused) {
1868
+ return null;
1869
+ }
1870
+ },
1871
+ subscribe(cb) {
1872
+ const onDetail = (detail) => {
1873
+ const parsed = parseAnyDetail(detail);
1874
+ if (parsed) {
1875
+ cached = parsed;
1876
+ try {
1877
+ cb();
1878
+ } catch (_unused2) {}
1879
+ }
1880
+ };
1881
+ return listenToWindowEvents(["ucEvent", "UC_CONSENT"], (event) => {
1882
+ onDetail(event.detail);
1883
+ });
1884
+ }
1885
+ };
1886
+ };
1887
+ //#endregion
1888
+ //#region src/consent-sources/cookiebot.ts
1889
+ /**
1890
+ * Cookiebot adapter. Read-only.
1891
+ *
1892
+ * ## API surface (verified against the Cookiebot developer reference)
1893
+ * Source: https://www.cookiebot.com/en/developer/
1894
+ *
1895
+ * - `window.Cookiebot.consent` — read-only booleans for Cookiebot's four fixed
1896
+ * categories: `necessary`, `preferences`, `statistics`, `marketing`. These are
1897
+ * a fixed enum (unlike Usercentrics' configurable categories).
1898
+ * - `window.Cookiebot.hasResponse` — `true` once the user has responded (accept
1899
+ * or decline). Used to distinguish "no decision yet" (all booleans default
1900
+ * false / necessary true) from "explicitly all-denied".
1901
+ * - `window.Cookiebot.consent.method` — `'implied' | 'explicit'` (or null).
1902
+ * - Events (all dispatched on `window`):
1903
+ * `CookiebotOnConsentReady` — fires when consent is ready, whether freshly
1904
+ * submitted OR loaded from an existing cookie (read-on-load + every
1905
+ * change). This is the primary subscription.
1906
+ * `CookiebotOnAccept` / `CookiebotOnDecline` — explicit user actions (also
1907
+ * re-fire for returning consented users); subscribed as a backstop.
1908
+ *
1909
+ * ## Category mapping (Cookiebot -> Leapio)
1910
+ *
1911
+ * necessary -> (always-on; not mapped to a Leapio category)
1912
+ * preferences -> functional // "remember user choices (language, region...)"
1913
+ * statistics -> analytics // "anonymous analytics / how visitors use the site"
1914
+ * marketing -> marketing // "tracking for ads / cross-site profiling"
1915
+ *
1916
+ * We map each Cookiebot boolean directly. Because Cookiebot exposes explicit
1917
+ * booleans (not just a granted-list), we emit `true`/`false` faithfully — but
1918
+ * only once `hasResponse` is true. Before a response, `read()` returns `null`
1919
+ * so the snapshot falls through to the org's consentBehavior server-side rather
1920
+ * than asserting denials the user has not made.
1921
+ */
1922
+ const SOURCE_NAME = "cookiebot";
1923
+ const PRIORITY = 60;
1924
+ /** Cookiebot category -> Leapio category (necessary is intentionally unmapped). */
1925
+ const CONSENT_FIELD_TO_CATEGORY = [
1926
+ {
1927
+ field: "preferences",
1928
+ category: "functional"
1929
+ },
1930
+ {
1931
+ field: "statistics",
1932
+ category: "analytics"
1933
+ },
1934
+ {
1935
+ field: "marketing",
1936
+ category: "marketing"
1937
+ }
1938
+ ];
1939
+ const getCookiebot = () => {
1940
+ try {
1941
+ const cb = window.Cookiebot;
1942
+ return cb && typeof cb === "object" ? cb : null;
1943
+ } catch (_unused) {
1944
+ return null;
1945
+ }
1946
+ };
1947
+ const buildSnapshot = (cb) => {
1948
+ const consent = cb.consent;
1949
+ if (!consent || typeof consent !== "object") return null;
1950
+ if (cb.hasResponse !== true) return null;
1951
+ const categories = {};
1952
+ for (const { field, category } of CONSENT_FIELD_TO_CATEGORY) {
1953
+ const value = consent[field];
1954
+ if (typeof value === "boolean") categories[category] = value;
1955
+ }
1956
+ if (Object.keys(categories).length === 0) return null;
1957
+ return {
1958
+ categories,
1959
+ signals: {},
1960
+ source: SOURCE_NAME,
1961
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1962
+ sourceMetadata: { method: typeof consent.method === "string" ? consent.method : void 0 }
1963
+ };
1964
+ };
1965
+ /**
1966
+ * Construct the Cookiebot source. Always returns a source; `read()` yields
1967
+ * `null` until `window.Cookiebot` exists and the user has responded.
1968
+ */
1969
+ const createCookiebotSource = () => {
1970
+ return {
1971
+ name: SOURCE_NAME,
1972
+ priority: PRIORITY,
1973
+ read() {
1974
+ try {
1975
+ const cb = getCookiebot();
1976
+ if (!cb) return null;
1977
+ return buildSnapshot(cb);
1978
+ } catch (_unused2) {
1979
+ return null;
1980
+ }
1981
+ },
1982
+ subscribe(cb) {
1983
+ const notify = () => {
1984
+ try {
1985
+ cb();
1986
+ } catch (_unused3) {}
1987
+ };
1988
+ return listenToWindowEvents([
1989
+ "CookiebotOnConsentReady",
1990
+ "CookiebotOnAccept",
1991
+ "CookiebotOnDecline"
1992
+ ], notify);
1993
+ }
1994
+ };
1995
+ };
1996
+ //#endregion
1997
+ //#region src/consent-sources/index.ts
1998
+ /**
1999
+ * Consent-source registry.
2000
+ *
2001
+ * Maps the source names a customer puts in `config.consentSources` (plumbed from
2002
+ * the CDN wrapper's `data-consent-source` attribute) to their factory. Accepts
2003
+ * both the canonical source ids and a couple of friendly aliases.
2004
+ */
2005
+ const SOURCE_FACTORIES = {
2006
+ concord: createConcordSource,
2007
+ onetrust: createOneTrustSource,
2008
+ usercentrics: createUsercentricsSource,
2009
+ cookiebot: createCookiebotSource,
2010
+ gcm: createGcmReaderSource,
2011
+ google_consent_mode: createGcmReaderSource
2012
+ };
2013
+ const normalizeName = (name) => name.trim().toLowerCase();
2014
+ /**
2015
+ * Instantiate the consent sources named in `names`. Unknown names are skipped
2016
+ * (logged once); a factory returning `null` is skipped silently. Duplicate names
2017
+ * collapse to a single instance. Never throws.
2018
+ */
2019
+ const instantiateConsentSources = (names) => {
2020
+ const seen = /* @__PURE__ */ new Set();
2021
+ const sources = [];
2022
+ for (const rawName of names) {
2023
+ const key = normalizeName(rawName);
2024
+ if (!key || seen.has(key)) continue;
2025
+ seen.add(key);
2026
+ const factory = SOURCE_FACTORIES[key];
2027
+ if (!factory) {
2028
+ try {
2029
+ console.warn(`Leapio - unknown consent source "${rawName}" ignored`);
2030
+ } catch (_unused) {}
2031
+ continue;
2032
+ }
2033
+ try {
2034
+ const source = factory();
2035
+ if (source) sources.push(source);
2036
+ } catch (_unused2) {}
2037
+ }
2038
+ return sources;
2039
+ };
2040
+ /**
2041
+ * Owns the lifecycle of the registered consent sources. On `start()` it reads
2042
+ * each source's current snapshot, applies it through `applySnapshot` (the store
2043
+ * enforces priority, so order does not matter), and subscribes to each source's
2044
+ * change events — re-reading and re-applying on every callback. `stop()` tears
2045
+ * down every subscription (used by `reset()`).
2046
+ *
2047
+ * `applySnapshot` is the SDK seam (`leapio.applyConsentSnapshot`): it returns a
2048
+ * promise resolving to whether the snapshot became authoritative. The registry
2049
+ * fires it and does not block source setup on the resulting `/consent` POST.
2050
+ */
2051
+ var ConsentSourceRegistry = class {
2052
+ constructor(sources, applySnapshot) {
2053
+ this.applySnapshot = applySnapshot;
2054
+ this.unsubscribers = [];
2055
+ this.started = false;
2056
+ this.sources = sources;
2057
+ }
2058
+ /** Number of registered sources (0 means the registry is a no-op). */
2059
+ get size() {
2060
+ return this.sources.length;
2061
+ }
2062
+ /**
2063
+ * Read each source's current state and subscribe for changes. Safe to call
2064
+ * when no vendor global is present yet: `read()` returns `null` (nothing
2065
+ * applied) and the subscriptions stay live so a later vendor load is picked
2066
+ * up. Idempotent — a second call is a no-op.
2067
+ */
2068
+ start() {
2069
+ if (this.started) return;
2070
+ this.started = true;
2071
+ for (const source of this.sources) {
2072
+ try {
2073
+ const unsubscribe = source.subscribe(() => {
2074
+ this.applyFrom(source);
2075
+ });
2076
+ if (typeof unsubscribe === "function") this.unsubscribers.push(unsubscribe);
2077
+ } catch (_unused3) {}
2078
+ this.applyFrom(source);
2079
+ }
2080
+ }
2081
+ /** Read one source and push its snapshot through the priority-aware store. */
2082
+ applyFrom(source) {
2083
+ let snapshot = null;
2084
+ try {
2085
+ snapshot = source.read();
2086
+ } catch (_unused4) {
2087
+ snapshot = null;
2088
+ }
2089
+ if (!snapshot) return;
2090
+ this.applySnapshot(snapshot).catch(() => {});
2091
+ }
2092
+ /** Tear down every subscription. Called by `reset()`. Idempotent. */
2093
+ stop() {
2094
+ while (this.unsubscribers.length > 0) {
2095
+ const off = this.unsubscribers.pop();
2096
+ try {
2097
+ off === null || off === void 0 || off();
2098
+ } catch (_unused5) {}
2099
+ }
2100
+ this.started = false;
2101
+ }
2102
+ };
2103
+ //#endregion
2104
+ //#region src/autocapture.ts
2105
+ /** Attribute used to fire a declarative event on click. */
2106
+ const DECLARATIVE_EVENT_ATTR = "data-leapio-event";
2107
+ const LEGACY_DECLARATIVE_EVENT_ATTR = "data-lgspt-event";
2108
+ const LEGACY_DECLARATIVE_PROP_PREFIX = "data-lgspt-prop-";
2109
+ const LEGACY_SENSITIVE_ATTR = "data-lgspt-sensitive";
2110
+ /** Prefix for declarative metadata props, e.g. `data-leapio-prop-plan="pro"`. */
2111
+ const DECLARATIVE_PROP_PREFIX = "data-leapio-prop-";
2112
+ const SENSITIVE_SELECTOR = `[data-leapio-sensitive],[${LEGACY_SENSITIVE_ATTR}]`;
2113
+ /**
2114
+ * File extensions treated as downloads. A click on an `a[href]` whose path ends
2115
+ * in one of these (case-insensitive) emits `File Downloaded`. Pure metadata —
2116
+ * no PII. (An explicit `download` attribute also qualifies, regardless of
2117
+ * extension.) This is the SDK's built-in default; the server config can replace
2118
+ * it per project.
2119
+ */
2120
+ const DOWNLOAD_EXTENSIONS = [
2121
+ "pdf",
2122
+ "zip",
2123
+ "csv",
2124
+ "xlsx",
2125
+ "doc",
2126
+ "docx",
2127
+ "ppt",
2128
+ "pptx",
2129
+ "dmg",
2130
+ "pkg",
2131
+ "mp3",
2132
+ "mp4",
2133
+ "wav"
2134
+ ];
2135
+ /**
2136
+ * Per-type defaults. `clicks` and `inputChanges` are noisy, so they default
2137
+ * OFF; `outboundLinks`, `fileDownloads`, and `formSubmissions` are high-signal
2138
+ * and default ON (once autocapture itself is enabled — autocapture is still off
2139
+ * by default). `mediaPlayback` is audience-dependent (only relevant on pages
2140
+ * with native media), so it also defaults OFF.
2141
+ */
2142
+ const AUTOCAPTURE_DEFAULTS = {
2143
+ outboundLinks: true,
2144
+ fileDownloads: true,
2145
+ formSubmissions: true,
2146
+ clicks: false,
2147
+ inputChanges: false,
2148
+ mediaPlayback: false
2149
+ };
2150
+ /** Max characters of captured text before we treat the element as non-atomic. */
2151
+ const MAX_TEXT_LENGTH = 500;
2152
+ /** Truncate captured text to this length in emitted metadata. */
2153
+ const TEXT_SLICE = 200;
2154
+ /**
2155
+ * Playback-progress milestones (as fractions of duration) that emit a discrete
2156
+ * `Media Progress` event the first time playback crosses each one. 100% is the
2157
+ * `ended`/`Media Completed` event, not a progress milestone.
2158
+ */
2159
+ const MEDIA_PROGRESS_THRESHOLDS = [
2160
+ .25,
2161
+ .5,
2162
+ .75
2163
+ ];
2164
+ /** The per-type keys the SDK currently understands (server may carry more). */
2165
+ const AUTOCAPTURE_TYPE_KEYS = [
2166
+ "outboundLinks",
2167
+ "fileDownloads",
2168
+ "formSubmissions",
2169
+ "clicks",
2170
+ "inputChanges",
2171
+ "mediaPlayback"
2172
+ ];
2173
+ /** Any object carrying a `master` or `types` key is the snippet shape. */
2174
+ const isSnippetConfig = (value) => "master" in value || "types" in value;
2175
+ /**
2176
+ * Normalize the three accepted snippet input shapes into a single
2177
+ * `{ master, types }` form that separates master enablement from per-type
2178
+ * overrides:
2179
+ *
2180
+ * - `boolean` → `{ master: <bool>, types: {} }`.
2181
+ * - {@link AutocaptureConfig} (plain object) → `{ master: true, types: <obj> }`
2182
+ * (the direct-`init()` convenience: passing an object means enable).
2183
+ * - {@link AutocaptureSnippetConfig} `{ master, types }` → passed through, with
2184
+ * `master` possibly `undefined` (per-type-only attrs do NOT enable).
2185
+ * - `undefined` → `{ master: undefined, types: {} }`.
2186
+ *
2187
+ * Exported so the override-reporting path (`ac-overrides.ts`) can derive the
2188
+ * EXPLICIT snippet override set from the same normalization the resolver uses.
2189
+ */
2190
+ const normalizeSnippet = (snippet) => {
2191
+ if (typeof snippet === "boolean") return {
2192
+ master: snippet,
2193
+ types: {}
2194
+ };
2195
+ if (typeof snippet !== "object" || snippet === null) return {
2196
+ master: void 0,
2197
+ types: {}
2198
+ };
2199
+ if (isSnippetConfig(snippet)) {
2200
+ var _snippet$types;
2201
+ return {
2202
+ master: snippet.master,
2203
+ types: (_snippet$types = snippet.types) !== null && _snippet$types !== void 0 ? _snippet$types : {}
2204
+ };
2205
+ }
2206
+ return {
2207
+ master: true,
2208
+ types: snippet
2209
+ };
2210
+ };
2211
+ /** Normalize an extension for matching: lowercase + strip a single leading dot. */
2212
+ const normalizeExtension = (ext) => ext.trim().replace(/^\./, "").toLowerCase();
2213
+ /**
2214
+ * Resolve the EFFECTIVE config from three sources, merged **per key**.
2215
+ *
2216
+ * Enablement (master): `snippetMaster ?? server.enabled ?? false`. The snippet's
2217
+ * master is `true`/`false` only when explicitly set (`data-autocapture`, a
2218
+ * plain `init()` object, or `enableAutoClicks`); a bare per-type attribute
2219
+ * leaves it `undefined`, so it can override a type but never flips the master
2220
+ * on. When the master resolves OFF this returns `null` (no listeners attached) —
2221
+ * the fail-safe default when neither snippet nor server enables it.
2222
+ *
2223
+ * Per-type (applies only when the master is on): `snippetType ?? serverType ??
2224
+ * default`. A snippet value wins in either direction (on AND off); else the
2225
+ * server config (delivered on the track response, cached in localStorage); else
2226
+ * {@link AUTOCAPTURE_DEFAULTS}. Only `{ outboundLinks, fileDownloads,
2227
+ * formSubmissions, mediaPlayback }` are read from the server — `clicks` /
2228
+ * `inputChanges` are snippet-only, and any extra server keys (e.g. `notFound`)
2229
+ * are ignored.
2230
+ *
2231
+ * The legacy `enableAutoClicks` flag counts as a snippet-side `clicks: true`.
2232
+ */
2233
+ const resolveEffectiveAutocaptureConfig = (snippet, enableAutoClicks, remote) => {
2234
+ var _ref, _server$types, _server$downloads;
2235
+ const server = remote === null || remote === void 0 ? void 0 : remote.autocapture;
2236
+ const { master: snippetMaster, types: snippetTypes } = normalizeSnippet(snippet);
2237
+ const snippetEnable = snippetMaster !== null && snippetMaster !== void 0 ? snippetMaster : enableAutoClicks ? true : void 0;
2238
+ const serverEnable = typeof (server === null || server === void 0 ? void 0 : server.enabled) === "boolean" ? server.enabled : void 0;
2239
+ if (!((_ref = snippetEnable !== null && snippetEnable !== void 0 ? snippetEnable : serverEnable) !== null && _ref !== void 0 ? _ref : false)) return null;
2240
+ const serverTypes = (_server$types = server === null || server === void 0 ? void 0 : server.types) !== null && _server$types !== void 0 ? _server$types : {};
2241
+ const SERVER_TYPE_KEYS = /* @__PURE__ */ new Set([
2242
+ "outboundLinks",
2243
+ "fileDownloads",
2244
+ "formSubmissions",
2245
+ "mediaPlayback"
2246
+ ]);
2247
+ const resolved = _objectSpread2({}, AUTOCAPTURE_DEFAULTS);
2248
+ for (const key of AUTOCAPTURE_TYPE_KEYS) {
2249
+ const snippetValue = snippetTypes[key];
2250
+ const serverValue = SERVER_TYPE_KEYS.has(key) ? serverTypes[key] : void 0;
2251
+ resolved[key] = typeof snippetValue === "boolean" ? snippetValue : typeof serverValue === "boolean" ? serverValue : AUTOCAPTURE_DEFAULTS[key];
2252
+ }
2253
+ if (enableAutoClicks && snippetTypes.clicks === void 0) resolved.clicks = true;
2254
+ const serverExtensions = server === null || server === void 0 || (_server$downloads = server.downloads) === null || _server$downloads === void 0 ? void 0 : _server$downloads.extensions;
2255
+ const downloadExtensions = Array.isArray(serverExtensions) && serverExtensions.length > 0 ? serverExtensions.map(normalizeExtension).filter(Boolean) : DOWNLOAD_EXTENSIONS;
2256
+ return _objectSpread2(_objectSpread2({}, resolved), {}, { downloadExtensions });
2257
+ };
2258
+ /**
2259
+ * Wire up delegated autocapture listeners. Returns a teardown function that
2260
+ * removes every listener it added. The host passes `track` (already DNT/consent
2261
+ * aware) and `shouldTrack` (re-checked on every event so a later DNT/disable
2262
+ * still short-circuits). Declarative `data-leapio-event` capture is always on
2263
+ * when autocapture is enabled.
2264
+ */
2265
+ const setupAutocapture = (params) => {
2266
+ var _params$doc;
2267
+ const { config, track, shouldTrack } = params;
2268
+ const doc = (_params$doc = params.doc) !== null && _params$doc !== void 0 ? _params$doc : document;
2269
+ const teardowns = [];
2270
+ const onClick = (event) => {
2271
+ if (!shouldTrack()) return;
2272
+ const target = eventTarget(event);
2273
+ if (!target) return;
2274
+ if (target.closest(SENSITIVE_SELECTOR)) return;
2275
+ const declarative = findDeclarativeEvent(target);
2276
+ if (declarative) {
2277
+ track(declarative);
2278
+ return;
2279
+ }
2280
+ if (config.fileDownloads) {
2281
+ const download = buildDownloadEvent(target, doc, config.downloadExtensions);
2282
+ if (download) {
2283
+ track(download);
2284
+ return;
2285
+ }
2286
+ }
2287
+ if (config.outboundLinks) {
2288
+ const outbound = buildOutboundEvent(target, doc);
2289
+ if (outbound) {
2290
+ track(outbound);
2291
+ return;
2292
+ }
2293
+ }
2294
+ if (config.clicks) {
2295
+ const click = buildClickEvent(target);
2296
+ if (click) track(click);
2297
+ }
2298
+ };
2299
+ const onAuxClick = (event) => {
2300
+ if (!(event instanceof MouseEvent) || event.button !== 1) return;
2301
+ if (!shouldTrack()) return;
2302
+ const target = eventTarget(event);
2303
+ if (!target || target.closest(SENSITIVE_SELECTOR)) return;
2304
+ if (config.fileDownloads) {
2305
+ const download = buildDownloadEvent(target, doc, config.downloadExtensions);
2306
+ if (download) {
2307
+ track(download);
2308
+ return;
2309
+ }
2310
+ }
2311
+ if (config.outboundLinks) {
2312
+ const outbound = buildOutboundEvent(target, doc);
2313
+ if (outbound) track(outbound);
2314
+ }
2315
+ };
2316
+ const onSubmit = (event) => {
2317
+ if (!shouldTrack()) return;
2318
+ if (!config.formSubmissions) return;
2319
+ const target = eventTarget(event);
2320
+ const form = target === null || target === void 0 ? void 0 : target.closest("form");
2321
+ if (!form) return;
2322
+ if (form.closest(SENSITIVE_SELECTOR)) return;
2323
+ track(buildFormEvent(form, doc));
2324
+ };
2325
+ const onChange = (event) => {
2326
+ if (!shouldTrack()) return;
2327
+ if (!config.inputChanges) return;
2328
+ const target = eventTarget(event);
2329
+ const field = target === null || target === void 0 ? void 0 : target.closest("input, select, textarea");
2330
+ if (!field) return;
2331
+ if (isSensitiveField(field)) return;
2332
+ track(buildInputEvent(field));
2333
+ };
2334
+ const mediaMilestones = /* @__PURE__ */ new WeakMap();
2335
+ const mediaTarget = (event) => {
2336
+ const target = event.target;
2337
+ return target instanceof HTMLMediaElement ? target : null;
2338
+ };
2339
+ const onMediaPlay = (event) => {
2340
+ if (!shouldTrack()) return;
2341
+ if (!config.mediaPlayback) return;
2342
+ const media = mediaTarget(event);
2343
+ if (!media || isSensitiveMedia(media)) return;
2344
+ track(buildMediaEvent(media, "Media Played"));
2345
+ };
2346
+ const onMediaPause = (event) => {
2347
+ if (!shouldTrack()) return;
2348
+ if (!config.mediaPlayback) return;
2349
+ const media = mediaTarget(event);
2350
+ if (!media || isSensitiveMedia(media)) return;
2351
+ if (media.ended) return;
2352
+ track(buildMediaEvent(media, "Media Paused"));
2353
+ };
2354
+ const onMediaTimeUpdate = (event) => {
2355
+ if (!shouldTrack()) return;
2356
+ if (!config.mediaPlayback) return;
2357
+ const media = mediaTarget(event);
2358
+ if (!media || isSensitiveMedia(media)) return;
2359
+ const duration = media.duration;
2360
+ const currentTime = media.currentTime;
2361
+ if (!Number.isFinite(duration) || duration <= 0) return;
2362
+ let crossed = mediaMilestones.get(media);
2363
+ if (!crossed) {
2364
+ crossed = /* @__PURE__ */ new Set();
2365
+ mediaMilestones.set(media, crossed);
2366
+ }
2367
+ if (currentTime <= 0 && crossed.size > 0) crossed.clear();
2368
+ const percent = currentTime / duration;
2369
+ for (const threshold of MEDIA_PROGRESS_THRESHOLDS) if (percent >= threshold && !crossed.has(threshold)) {
2370
+ crossed.add(threshold);
2371
+ track(buildMediaEvent(media, "Media Progress", String(Math.round(threshold * 100))));
2372
+ }
2373
+ };
2374
+ const onMediaEnded = (event) => {
2375
+ if (!shouldTrack()) return;
2376
+ if (!config.mediaPlayback) return;
2377
+ const media = mediaTarget(event);
2378
+ if (!media || isSensitiveMedia(media)) return;
2379
+ mediaMilestones.delete(media);
2380
+ track(buildMediaEvent(media, "Media Completed", "100"));
2381
+ };
2382
+ doc.addEventListener("click", onClick, true);
2383
+ teardowns.push(() => doc.removeEventListener("click", onClick, true));
2384
+ if (config.fileDownloads || config.outboundLinks) {
2385
+ doc.addEventListener("auxclick", onAuxClick, true);
2386
+ teardowns.push(() => doc.removeEventListener("auxclick", onAuxClick, true));
2387
+ }
2388
+ if (config.formSubmissions) {
2389
+ doc.addEventListener("submit", onSubmit, true);
2390
+ teardowns.push(() => doc.removeEventListener("submit", onSubmit, true));
2391
+ }
2392
+ if (config.inputChanges) {
2393
+ doc.addEventListener("change", onChange, true);
2394
+ teardowns.push(() => doc.removeEventListener("change", onChange, true));
2395
+ }
2396
+ if (config.mediaPlayback) {
2397
+ doc.addEventListener("play", onMediaPlay, true);
2398
+ doc.addEventListener("pause", onMediaPause, true);
2399
+ doc.addEventListener("timeupdate", onMediaTimeUpdate, true);
2400
+ doc.addEventListener("ended", onMediaEnded, true);
2401
+ teardowns.push(() => {
2402
+ doc.removeEventListener("play", onMediaPlay, true);
2403
+ doc.removeEventListener("pause", onMediaPause, true);
2404
+ doc.removeEventListener("timeupdate", onMediaTimeUpdate, true);
2405
+ doc.removeEventListener("ended", onMediaEnded, true);
2406
+ });
2407
+ }
2408
+ return () => {
2409
+ for (const teardown of teardowns) teardown();
2410
+ };
2411
+ };
2412
+ /** Narrow an event's target to an Element, or null for non-element targets. */
2413
+ const eventTarget = (event) => {
2414
+ const target = event.target;
2415
+ return target instanceof Element ? target : null;
2416
+ };
2417
+ /**
2418
+ * Declarative `data-leapio-event` capture. Walks to the nearest ancestor (or
2419
+ * self) carrying the attribute and collects `data-leapio-prop-*` props into
2420
+ * metadata. Supersedes the bare `lgspt-` classname approach.
2421
+ */
2422
+ const findDeclarativeEvent = (target) => {
2423
+ var _el$getAttribute;
2424
+ const el = target.closest(`[${DECLARATIVE_EVENT_ATTR}],[${LEGACY_DECLARATIVE_EVENT_ATTR}]`);
2425
+ if (!(el instanceof HTMLElement)) return null;
2426
+ const eventName = (_el$getAttribute = el.getAttribute("data-leapio-event")) !== null && _el$getAttribute !== void 0 ? _el$getAttribute : el.getAttribute(LEGACY_DECLARATIVE_EVENT_ATTR);
2427
+ if (!eventName) return null;
2428
+ const metadata = {};
2429
+ for (const attr of Array.from(el.attributes)) {
2430
+ const prefix = [DECLARATIVE_PROP_PREFIX, LEGACY_DECLARATIVE_PROP_PREFIX].find((candidate) => attr.name.startsWith(candidate));
2431
+ if (prefix) {
2432
+ const key = attr.name.slice(prefix.length);
2433
+ if (key) metadata[key] = attr.value;
2434
+ }
2435
+ }
2436
+ return {
2437
+ event: eventName,
2438
+ metadata
2439
+ };
2440
+ };
2441
+ /**
2442
+ * Outbound-link capture. Fires only when the closest `a[href]` resolves to a
2443
+ * different host than the current page, and is not a same-page anchor / mailto
2444
+ * / tel link.
2445
+ */
2446
+ const buildOutboundEvent = (target, doc) => {
2447
+ var _doc$location, _elementText;
2448
+ const resolved = resolveNavigableAnchor(target, doc);
2449
+ if (!resolved) return null;
2450
+ const { anchor, url } = resolved;
2451
+ if (!url.host || url.host === ((_doc$location = doc.location) === null || _doc$location === void 0 ? void 0 : _doc$location.host)) return null;
2452
+ return {
2453
+ event: "Outbound Link",
2454
+ metadata: {
2455
+ href: anchor.href || anchor.getAttribute("href") || void 0,
2456
+ text: (_elementText = elementText(anchor)) === null || _elementText === void 0 ? void 0 : _elementText.slice(0, TEXT_SLICE)
2457
+ }
2458
+ };
2459
+ };
2460
+ const resolveNavigableAnchor = (target, doc) => {
2461
+ var _anchor$getAttribute;
2462
+ const anchor = target.closest("a[href]");
2463
+ if (!(anchor instanceof HTMLAnchorElement)) return null;
2464
+ const rawHref = (_anchor$getAttribute = anchor.getAttribute("href")) !== null && _anchor$getAttribute !== void 0 ? _anchor$getAttribute : "";
2465
+ if (!rawHref) return null;
2466
+ if (rawHref.startsWith("#") || /^(mailto:|tel:|sms:|javascript:)/i.test(rawHref)) return null;
2467
+ let url;
2468
+ try {
2469
+ var _doc$location2;
2470
+ url = new URL(anchor.href || rawHref, (_doc$location2 = doc.location) === null || _doc$location2 === void 0 ? void 0 : _doc$location2.href);
2471
+ } catch (_unused) {
2472
+ return null;
2473
+ }
2474
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
2475
+ return {
2476
+ anchor,
2477
+ url
2478
+ };
2479
+ };
2480
+ /**
2481
+ * File-download capture. Fires on a click of an `a[href]` that either carries a
2482
+ * `download` attribute or whose resolved path ends in a known download
2483
+ * extension. The extension allowlist is the effective config's
2484
+ * `downloadExtensions` (server-provided list when present, else the built-in
2485
+ * {@link DOWNLOAD_EXTENSIONS}); both the configured extensions and the URL's
2486
+ * extension are normalized (lowercase, no leading dot) for matching. Pure
2487
+ * metadata — `{ href, fileName, fileExtension }`, no PII. Same-page anchors and
2488
+ * non-navigational schemes (mailto/tel/sms/javascript) are never downloads.
2489
+ */
2490
+ const buildDownloadEvent = (target, doc, extensions) => {
2491
+ const resolved = resolveNavigableAnchor(target, doc);
2492
+ if (!resolved) return null;
2493
+ const { anchor, url } = resolved;
2494
+ const hasDownloadAttr = anchor.hasAttribute("download");
2495
+ const fileExtension = fileExtensionFromPath(url.pathname);
2496
+ const isKnownExtension = !!fileExtension && extensions.some((ext) => normalizeExtension(ext) === fileExtension);
2497
+ if (!hasDownloadAttr && !isKnownExtension) return null;
2498
+ return {
2499
+ event: "File Downloaded",
2500
+ metadata: {
2501
+ href: anchor.href || anchor.getAttribute("href") || void 0,
2502
+ fileName: fileNameFromPath(url.pathname),
2503
+ fileExtension
2504
+ }
2505
+ };
2506
+ };
2507
+ /** Form-submission capture. Metadata only — field values are never read. */
2508
+ const buildFormEvent = (form, doc) => ({
2509
+ event: "Form Submitted",
2510
+ metadata: {
2511
+ id: attr(form, "id"),
2512
+ name: attr(form, "name"),
2513
+ action: formAction(form, doc)
2514
+ }
2515
+ });
2516
+ const formAction = (form, doc) => {
2517
+ const rawAction = attr(form, "action");
2518
+ if (!rawAction) return void 0;
2519
+ try {
2520
+ var _doc$location3;
2521
+ const resolved = new URL(rawAction, (_doc$location3 = doc.location) === null || _doc$location3 === void 0 ? void 0 : _doc$location3.href);
2522
+ return `${resolved.protocol}//${resolved.host}${resolved.pathname}`;
2523
+ } catch (_unused2) {
2524
+ return rawAction.split(/[?#]/)[0] || void 0;
2525
+ }
2526
+ };
2527
+ /**
2528
+ * Generic click capture (off by default). Targets the nearest interactive
2529
+ * element. Skips elements whose visible text exceeds {@link MAX_TEXT_LENGTH}
2530
+ * (the fixed length-guard: compares `.length`, not the string itself).
2531
+ */
2532
+ const buildClickEvent = (target) => {
2533
+ var _target$closest;
2534
+ const el = (_target$closest = target.closest("a, button, [role=button]")) !== null && _target$closest !== void 0 ? _target$closest : target;
2535
+ const text = el.matches("input, textarea, select, optgroup, option") ? void 0 : elementText(el);
2536
+ if (text && text.length > MAX_TEXT_LENGTH) return null;
2537
+ return {
2538
+ event: "Click",
2539
+ metadata: {
2540
+ tag: el.tagName,
2541
+ id: attr(el, "id"),
2542
+ text: text === null || text === void 0 ? void 0 : text.slice(0, TEXT_SLICE),
2543
+ href: el instanceof HTMLAnchorElement ? el.href || void 0 : void 0
2544
+ }
2545
+ };
2546
+ };
2547
+ /** Input-change capture. Metadata only — the value is NEVER captured. */
2548
+ const buildInputEvent = (field) => ({
2549
+ event: "Input Changed",
2550
+ metadata: {
2551
+ name: attr(field, "name"),
2552
+ type: fieldType(field)
2553
+ }
2554
+ });
2555
+ /**
2556
+ * Native `<video>`/`<audio>` engagement capture (metadata only — never a
2557
+ * recording). `percent` is set only for `Media Progress` (the milestone) and
2558
+ * `Media Completed` (`"100"`). `duration`/`current_time` are rounded whole
2559
+ * seconds; `duration` is omitted for non-finite values (e.g. live streams). The
2560
+ * `src` is the element `id` when present, else the basename of `currentSrc`/
2561
+ * `src` (filename only — query string dropped to avoid leaking URLs/PII).
2562
+ */
2563
+ const buildMediaEvent = (media, event, percent) => {
2564
+ const duration = media.duration;
2565
+ return {
2566
+ event,
2567
+ metadata: {
2568
+ media_type: media instanceof HTMLVideoElement ? "video" : "audio",
2569
+ src: mediaSource(media),
2570
+ title: mediaTitle(media),
2571
+ percent,
2572
+ duration: Number.isFinite(duration) ? String(Math.round(duration)) : void 0,
2573
+ current_time: String(Math.round(media.currentTime))
2574
+ }
2575
+ };
2576
+ };
2577
+ /**
2578
+ * Whether a media element is flagged sensitive and must be skipped. Mirrors the
2579
+ * input opt-out: the element itself or any ancestor carrying
2580
+ * `data-leapio-sensitive`.
2581
+ */
2582
+ const isSensitiveMedia = (media) => media.closest(SENSITIVE_SELECTOR) !== null;
2583
+ /**
2584
+ * Stable identifier for a media element: its `id` when present, else the
2585
+ * basename (filename only, query string dropped) of `currentSrc`/`src`. Returns
2586
+ * undefined when there is no usable source — never the full URL, to avoid
2587
+ * leaking PII in query params.
2588
+ */
2589
+ const mediaSource = (media) => {
2590
+ const id = attr(media, "id");
2591
+ if (id) return id;
2592
+ const raw = media.currentSrc || media.src;
2593
+ if (!raw) return void 0;
2594
+ return urlBasename(raw);
2595
+ };
2596
+ /**
2597
+ * Best-effort human label for a media element: `aria-label`, else the `title`
2598
+ * attribute, else the nearest `<label>`/`<figcaption>` text, else the source
2599
+ * filename. Truncated to {@link TEXT_SLICE}. Undefined when nothing is found.
2600
+ */
2601
+ const mediaTitle = (media) => {
2602
+ var _media$closest;
2603
+ const ariaLabel = attr(media, "aria-label");
2604
+ if (ariaLabel) return ariaLabel.slice(0, TEXT_SLICE);
2605
+ const title = attr(media, "title");
2606
+ if (title) return title.slice(0, TEXT_SLICE);
2607
+ const figcaption = (_media$closest = media.closest("figure")) === null || _media$closest === void 0 ? void 0 : _media$closest.querySelector("figcaption");
2608
+ const captionText = figcaption ? elementText(figcaption) : void 0;
2609
+ if (captionText) return captionText.slice(0, TEXT_SLICE);
2610
+ const label = media.closest("label");
2611
+ const labelText = label ? elementText(label) : void 0;
2612
+ if (labelText) return labelText.slice(0, TEXT_SLICE);
2613
+ const raw = media.currentSrc || media.src;
2614
+ return raw ? urlBasename(raw) : void 0;
2615
+ };
2616
+ /**
2617
+ * Filename (last path segment) of a URL, with the query string and hash
2618
+ * dropped. Resolves relative to the page so jsdom and the browser agree.
2619
+ * Returns undefined when there is no meaningful segment.
2620
+ */
2621
+ const urlBasename = (raw) => {
2622
+ try {
2623
+ var _document$location;
2624
+ const url = new URL(raw, (_document$location = document.location) === null || _document$location === void 0 ? void 0 : _document$location.href);
2625
+ return fileNameFromPath(url.pathname);
2626
+ } catch (_unused3) {
2627
+ const withoutQuery = raw.split(/[?#]/)[0];
2628
+ return fileNameFromPath(withoutQuery);
2629
+ }
2630
+ };
2631
+ /**
2632
+ * Whether an input/select/textarea is sensitive and must be skipped. Covers
2633
+ * password fields, one-time codes, credit-card autofill, stored credentials,
2634
+ * and any element (or ancestor) marked `data-leapio-sensitive`.
2635
+ */
2636
+ const isSensitiveField = (field) => {
2637
+ var _field$getAttribute, _field$getAttribute2;
2638
+ if (field.closest(SENSITIVE_SELECTOR)) return true;
2639
+ if (((_field$getAttribute = field.getAttribute("type")) !== null && _field$getAttribute !== void 0 ? _field$getAttribute : "").toLowerCase() === "password") return true;
2640
+ const autocomplete = ((_field$getAttribute2 = field.getAttribute("autocomplete")) !== null && _field$getAttribute2 !== void 0 ? _field$getAttribute2 : "").toLowerCase();
2641
+ if (autocomplete) {
2642
+ if (autocomplete.includes("one-time-code") || autocomplete.includes("current-password") || autocomplete.includes("new-password") || autocomplete.includes("cc-")) return true;
2643
+ }
2644
+ return false;
2645
+ };
2646
+ /**
2647
+ * Last path segment of a URL pathname (the file name), decoded and trimmed of
2648
+ * trailing slashes. Returns undefined when there is no meaningful segment.
2649
+ */
2650
+ const fileNameFromPath = (pathname) => {
2651
+ const segments = pathname.split("/").filter(Boolean);
2652
+ const last = segments[segments.length - 1];
2653
+ if (!last) return void 0;
2654
+ try {
2655
+ return decodeURIComponent(last);
2656
+ } catch (_unused4) {
2657
+ return last;
2658
+ }
2659
+ };
2660
+ /**
2661
+ * Lower-cased extension (without the dot) of a URL pathname's file name, or
2662
+ * undefined when there is no dotted extension.
2663
+ */
2664
+ const fileExtensionFromPath = (pathname) => {
2665
+ const fileName = fileNameFromPath(pathname);
2666
+ if (!fileName) return void 0;
2667
+ const dot = fileName.lastIndexOf(".");
2668
+ if (dot <= 0 || dot === fileName.length - 1) return void 0;
2669
+ return fileName.slice(dot + 1).toLowerCase();
2670
+ };
2671
+ const fieldType = (field) => {
2672
+ const explicit = field.getAttribute("type");
2673
+ if (explicit) return explicit;
2674
+ return field.tagName.toLowerCase();
2675
+ };
2676
+ const attr = (el, name) => {
2677
+ const value = el.getAttribute(name);
2678
+ return value === null || value === "" ? void 0 : value;
2679
+ };
2680
+ /** Visible text of an element, trimmed. Returns undefined when empty. */
2681
+ const elementText = (el) => {
2682
+ var _ref2, _ref3;
2683
+ const text = ((_ref2 = (_ref3 = el instanceof HTMLElement ? el.innerText : null) !== null && _ref3 !== void 0 ? _ref3 : el.textContent) !== null && _ref2 !== void 0 ? _ref2 : "").trim();
2684
+ return text.length ? text : void 0;
2685
+ };
2686
+ //#endregion
2687
+ //#region src/dnt.ts
2688
+ const hasExplicitDNT = () => {
2689
+ const DNT_KEY = "leapio_dnt";
2690
+ const LEGACY_DNT_KEY = "lgspt_dnt";
2691
+ const urlSearchParams = new URLSearchParams(window.location.search);
2692
+ const params = Object.fromEntries(urlSearchParams.entries());
2693
+ if (params.dnt === "false" || params.dnt === "0") {
2694
+ localStorage.removeItem(DNT_KEY);
2695
+ localStorage.removeItem(LEGACY_DNT_KEY);
2696
+ return false;
2697
+ }
2698
+ const queryWithDNT = typeof params.dnt !== "undefined";
2699
+ const disableTracking = queryWithDNT || localStorage.getItem(DNT_KEY) === "1" || localStorage.getItem(LEGACY_DNT_KEY) === "1";
2700
+ if (queryWithDNT) localStorage.setItem(DNT_KEY, "1");
2701
+ return disableTracking;
2702
+ };
2703
+ const hasBrowserDNT = () => {
2704
+ return navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || window.doNotTrack === "1";
2705
+ };
2706
+ const isAutomatedBrowser = () => navigator.webdriver === true;
2707
+ //#endregion
2708
+ //#region src/engagement.ts
2709
+ const ENGAGEMENT_MODES = [
2710
+ "off",
2711
+ "leave",
2712
+ "heartbeat"
2713
+ ];
2714
+ const HEARTBEAT_INTERVAL_MS = 6e4;
2715
+ const MIN_REPORT_MS = 1e3;
2716
+ const isEngagementMode = (value) => ENGAGEMENT_MODES.includes(value);
2717
+ /**
2718
+ * Count visible time on the current page and report it per `mode`. `send`
2719
+ * must capture the page context synchronously (the leave report for an SPA
2720
+ * route change fires before the SDK advances to the new url).
2721
+ */
2722
+ const setupEngagement = (opts) => {
2723
+ var _opts$now;
2724
+ const now = (_opts$now = opts.now) !== null && _opts$now !== void 0 ? _opts$now : Date.now;
2725
+ const isVisible = () => document.visibilityState === "visible";
2726
+ let visibleSince = isVisible() ? now() : null;
2727
+ let unreportedMs = 0;
2728
+ let leaves = 0;
2729
+ let heartbeats = 0;
2730
+ const collect = () => {
2731
+ if (visibleSince !== null) {
2732
+ const at = now();
2733
+ unreportedMs += at - visibleSince;
2734
+ visibleSince = isVisible() ? at : null;
2735
+ }
2736
+ };
2737
+ const report = (event) => {
2738
+ if (unreportedMs < MIN_REPORT_MS) return false;
2739
+ opts.send(event, { engaged_seconds: Math.round(unreportedMs / 1e3) });
2740
+ unreportedMs = 0;
2741
+ return true;
2742
+ };
2743
+ const leave = () => {
2744
+ collect();
2745
+ if (leaves < 10 && report("Page Leave")) leaves++;
2746
+ };
2747
+ const onVisibilityChange = () => {
2748
+ if (isVisible()) {
2749
+ if (visibleSince === null) visibleSince = now();
2750
+ } else leave();
2751
+ };
2752
+ document.addEventListener("visibilitychange", onVisibilityChange);
2753
+ window.addEventListener("pagehide", leave);
2754
+ const timer = opts.mode === "heartbeat" ? setInterval(() => {
2755
+ if (!isVisible() || heartbeats >= 30) return;
2756
+ collect();
2757
+ if (report("Page Heartbeat")) heartbeats++;
2758
+ }, HEARTBEAT_INTERVAL_MS) : null;
2759
+ return {
2760
+ pageChanged: () => {
2761
+ leave();
2762
+ leaves = 0;
2763
+ heartbeats = 0;
2764
+ unreportedMs = 0;
2765
+ visibleSince = isVisible() ? now() : null;
2766
+ },
2767
+ teardown: () => {
2768
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2769
+ window.removeEventListener("pagehide", leave);
2770
+ if (timer !== null) clearInterval(timer);
2771
+ }
2772
+ };
2773
+ };
2774
+ //#endregion
2775
+ //#region src/payment-links.ts
2776
+ const STRIPE_PAYMENT_LINK_HOST = "buy.stripe.com";
2777
+ const CLIENT_REFERENCE_PARAM = "client_reference_id";
2778
+ /** The href with the anonymous id added, or null when it should be left as is. */
2779
+ const tagPaymentLinkHref = (href, anonymousId) => {
2780
+ let url;
2781
+ try {
2782
+ url = new URL(href);
2783
+ } catch (_unused) {
2784
+ return null;
2785
+ }
2786
+ if (url.hostname !== STRIPE_PAYMENT_LINK_HOST) return null;
2787
+ if (url.searchParams.has(CLIENT_REFERENCE_PARAM)) return null;
2788
+ url.searchParams.set(CLIENT_REFERENCE_PARAM, anonymousId);
2789
+ return url.toString();
2790
+ };
2791
+ const ownTags = /* @__PURE__ */ new WeakMap();
2792
+ /**
2793
+ * Tag Stripe Payment Link anchors on press. `pointerdown` runs before the
2794
+ * context menu, so "open in new tab" gets the tagged href too; `click` and
2795
+ * `auxclick` cover keyboard activation and middle-click. Returns a teardown.
2796
+ */
2797
+ const listenForPaymentLinks = (opts) => {
2798
+ const onPress = (event) => {
2799
+ var _target$closest;
2800
+ if (!opts.shouldTag()) return;
2801
+ const target = event.target;
2802
+ const anchor = target === null || target === void 0 || (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, "a[href]");
2803
+ const anonymousId = opts.getAnonymousId();
2804
+ if (!anchor || !anonymousId) return;
2805
+ const own = ownTags.get(anchor);
2806
+ const from = own && own.to === anchor.href ? own.from : anchor.href;
2807
+ const tagged = tagPaymentLinkHref(from, anonymousId);
2808
+ if (tagged) {
2809
+ anchor.href = tagged;
2810
+ ownTags.set(anchor, {
2811
+ from,
2812
+ to: anchor.href
2813
+ });
2814
+ }
2815
+ };
2816
+ const events = [
2817
+ "pointerdown",
2818
+ "click",
2819
+ "auxclick"
2820
+ ];
2821
+ for (const name of events) document.addEventListener(name, onPress, true);
2822
+ return () => {
2823
+ for (const name of events) document.removeEventListener(name, onPress, true);
2824
+ };
2825
+ };
2826
+ //#endregion
2827
+ //#region src/privacy-signals.ts
2828
+ /**
2829
+ * Read GPC. Returns `true` only when the browser explicitly exposes the signal
2830
+ * as enabled; `undefined` when the API is absent (so callers can distinguish
2831
+ * "not present" from "present and false").
2832
+ */
2833
+ const detectGpc = () => {
2834
+ try {
2835
+ if (typeof navigator === "undefined") return void 0;
2836
+ const value = navigator.globalPrivacyControl;
2837
+ if (typeof value === "boolean") return value;
2838
+ return;
2839
+ } catch (_unused) {
2840
+ return;
2841
+ }
2842
+ };
2843
+ const getGppApi = () => {
2844
+ try {
2845
+ if (typeof window === "undefined") return void 0;
2846
+ const api = window.__gpp;
2847
+ return typeof api === "function" ? api : void 0;
2848
+ } catch (_unused2) {
2849
+ return;
2850
+ }
2851
+ };
2852
+ /**
2853
+ * Synchronously probe for GPP via `__gpp('ping', cb)`. The IAB stub invokes the
2854
+ * callback synchronously, so the result is available immediately in practice;
2855
+ * if a host implementation is async, the synchronous read simply returns
2856
+ * "not detected" and a later `subscribeGpp` event updates state.
2857
+ */
2858
+ const detectGpp = () => {
2859
+ const api = getGppApi();
2860
+ if (!api) return { detected: false };
2861
+ let detected = false;
2862
+ let gppString;
2863
+ try {
2864
+ api("ping", (data, success) => {
2865
+ if (!success || !data || typeof data !== "object") return;
2866
+ detected = true;
2867
+ const ping = data;
2868
+ if (typeof ping.gppString === "string" && ping.gppString.length > 0) gppString = ping.gppString;
2869
+ });
2870
+ } catch (_unused3) {
2871
+ detected = true;
2872
+ }
2873
+ return {
2874
+ detected,
2875
+ gppString
2876
+ };
2877
+ };
2878
+ /**
2879
+ * Subscribe to GPP section/string changes via `__gpp('addEventListener', cb)`.
2880
+ * Calls `onChange` whenever the GPP string is (re)computed. Returns an
2881
+ * unsubscribe function (best-effort `removeEventListener`).
2882
+ */
2883
+ const subscribeGpp = (onChange) => {
2884
+ const api = getGppApi();
2885
+ if (!api) return () => {};
2886
+ let listenerId;
2887
+ try {
2888
+ api("addEventListener", (data, success) => {
2889
+ var _event$pingData;
2890
+ if (!success || !data || typeof data !== "object") return;
2891
+ const event = data;
2892
+ if (typeof event.listenerId !== "undefined") listenerId = event.listenerId;
2893
+ onChange({
2894
+ detected: true,
2895
+ gppString: typeof ((_event$pingData = event.pingData) === null || _event$pingData === void 0 ? void 0 : _event$pingData.gppString) === "string" ? event.pingData.gppString : typeof event.gppString === "string" ? event.gppString : void 0
2896
+ });
2897
+ });
2898
+ } catch (_unused4) {
2899
+ return () => {};
2900
+ }
2901
+ return () => {
2902
+ try {
2903
+ if (typeof listenerId !== "undefined") api("removeEventListener", () => {}, listenerId);
2904
+ } catch (_unused5) {}
2905
+ };
2906
+ };
2907
+ /**
2908
+ * Detect all currently-readable browser privacy signals as a `PrivacySignals`
2909
+ * fragment suitable for merging into a consent snapshot. Only signals that are
2910
+ * actually present are included.
2911
+ */
2912
+ const detectPrivacySignals = () => {
2913
+ const signals = {};
2914
+ if (detectGpc() === true) signals.gpc = true;
2915
+ const gpp = detectGpp();
2916
+ if (gpp.detected) {
2917
+ signals.gppDetected = true;
2918
+ if (gpp.gppString) signals.gppString = gpp.gppString;
2919
+ }
2920
+ return signals;
2921
+ };
2922
+ //#endregion
2923
+ //#region src/super-properties.ts
2924
+ var SuperProperties = class {
2925
+ constructor(sdkConfig) {
2926
+ this.sdkConfig = sdkConfig;
2927
+ this.PROPS_COOKIE = "leapio_p";
2928
+ this.LEGACY_PROPS_COOKIE = "lgspt_p";
2929
+ this.expiryDays = 30;
2930
+ try {
2931
+ var _getCookie;
2932
+ const cookie = (_getCookie = getCookie(this.PROPS_COOKIE)) !== null && _getCookie !== void 0 ? _getCookie : getCookie(this.LEGACY_PROPS_COOKIE);
2933
+ this.properties = cookie ? JSON.parse(this.UnicodeDecodeB64(cookie)) : {};
2934
+ } catch (_unused) {
2935
+ this.properties = {};
2936
+ }
2937
+ }
2938
+ register(newProps, days) {
2939
+ this.properties = _objectSpread2(_objectSpread2({}, this.properties), newProps);
2940
+ if (typeof days !== "undefined") this.expiryDays = days;
2941
+ this._save();
2942
+ }
2943
+ unregister(property) {
2944
+ delete this.properties[property];
2945
+ this._save();
2946
+ }
2947
+ clear() {
2948
+ var _this$sdkConfig$cooki, _this$sdkConfig$cooki2;
2949
+ this.properties = {};
2950
+ eraseCookie(this.PROPS_COOKIE, (_this$sdkConfig$cooki = this.sdkConfig.cookieDomain) !== null && _this$sdkConfig$cooki !== void 0 ? _this$sdkConfig$cooki : null);
2951
+ eraseCookie(this.LEGACY_PROPS_COOKIE, (_this$sdkConfig$cooki2 = this.sdkConfig.cookieDomain) !== null && _this$sdkConfig$cooki2 !== void 0 ? _this$sdkConfig$cooki2 : null);
2952
+ }
2953
+ getProperties() {
2954
+ return this.properties;
2955
+ }
2956
+ _save() {
2957
+ var _this$sdkConfig$cooki3;
2958
+ if (this.sdkConfig.cookiesDisabled) return;
2959
+ setCookie({
2960
+ name: this.PROPS_COOKIE,
2961
+ value: this.b64EncodeUnicode(JSON.stringify(this.properties)),
2962
+ expiresInSeconds: this.expiryDays * ONE_DAY_IN_SECONDS,
2963
+ domain: (_this$sdkConfig$cooki3 = this.sdkConfig.cookieDomain) !== null && _this$sdkConfig$cooki3 !== void 0 ? _this$sdkConfig$cooki3 : null
2964
+ });
2965
+ }
2966
+ b64EncodeUnicode(str) {
2967
+ return btoa(encodeURIComponent(str));
2968
+ }
2969
+ UnicodeDecodeB64(str) {
2970
+ return decodeURIComponent(atob(str));
2971
+ }
2972
+ };
2973
+ //#endregion
2974
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/objectWithoutPropertiesLoose.js
2975
+ function _objectWithoutPropertiesLoose(r, e) {
2976
+ if (null == r) return {};
2977
+ var t = {};
2978
+ for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
2979
+ if (e.includes(n)) continue;
2980
+ t[n] = r[n];
2981
+ }
2982
+ return t;
2983
+ }
2984
+ //#endregion
2985
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/objectWithoutProperties.js
2986
+ function _objectWithoutProperties(e, t) {
2987
+ if (null == e) return {};
2988
+ var o, r, i = _objectWithoutPropertiesLoose(e, t);
2989
+ if (Object.getOwnPropertySymbols) {
2990
+ var s = Object.getOwnPropertySymbols(e);
2991
+ for (r = 0; r < s.length; r++) o = s[r], t.includes(o) || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
2992
+ }
2993
+ return i;
2994
+ }
2995
+ //#endregion
2996
+ //#region src/index.ts
2997
+ const _excluded = ["userId"];
2998
+ const _excluded2 = [
2999
+ "currency",
3000
+ "event",
3001
+ "channel",
3002
+ "userId"
3003
+ ];
3004
+ const DEFAULT_CONFIG = {
3005
+ cookiesDisabled: false,
3006
+ enableAutoPageviews: true,
3007
+ enableAutoClicks: false,
3008
+ stickyCampaigns: false,
3009
+ consentBehavior: "notRequired",
3010
+ stripQueryParams: false,
3011
+ legacySpaDetection: false,
3012
+ tagPaymentLinks: true,
3013
+ trackAutomatedBrowsers: false
3014
+ };
3015
+ const MAX_PENDING_EVENTS = 100;
3016
+ /** Default reserved event name marking an event as revenue (server-default). */
3017
+ const DEFAULT_REVENUE_EVENT_NAME = "Payment";
3018
+ const Leapio = () => {
3019
+ let sdkConfig;
3020
+ let disableTracking;
3021
+ let getPageViewPayload;
3022
+ let currentUrl;
3023
+ let currentRef;
3024
+ let identifiedUserId = null;
3025
+ let lastIdentityVerification = null;
3026
+ let anonymousId;
3027
+ let superProperties;
3028
+ let campaign;
3029
+ let consentStore;
3030
+ let consentSourceRegistry = null;
3031
+ let pendingEvents = [];
3032
+ let teardownAutocapture = null;
3033
+ let teardownClassClicks = null;
3034
+ let teardownSpaPageviews = null;
3035
+ let teardownPaymentLinks = null;
3036
+ let engagementTracker = null;
3037
+ let appliedEngagementMode;
3038
+ let appliedAutocaptureKey;
3039
+ let detectedSignals = {};
3040
+ let lastPostedConsentKey = null;
3041
+ let teardownReadyListener = null;
3042
+ let teardownGpp = null;
3043
+ const getStorageOpts = () => {
3044
+ var _sdkConfig$cookieDoma, _sdkConfig$cookieExpi;
3045
+ return {
3046
+ cookiesDisabled: !!(sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.cookiesDisabled),
3047
+ cookieDomain: (_sdkConfig$cookieDoma = sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.cookieDomain) !== null && _sdkConfig$cookieDoma !== void 0 ? _sdkConfig$cookieDoma : null,
3048
+ cookieExpirationInSeconds: (_sdkConfig$cookieExpi = sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.cookieExpirationInSeconds) !== null && _sdkConfig$cookieExpi !== void 0 ? _sdkConfig$cookieExpi : ONE_YEAR_IN_SECONDS
3049
+ };
3050
+ };
3051
+ /** Whether the SDK should hold events until analytics is explicitly granted. */
3052
+ const isExpressMode = () => (sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.consentBehavior) === "express";
3053
+ /** Whether the SDK sends until analytics is explicitly denied. */
3054
+ const isImpliedMode = () => (sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.consentBehavior) === "implied";
3055
+ /** Whether an event captured right now would be sent (not queued/dropped). */
3056
+ const canSendNow = () => {
3057
+ var _consentStore$get;
3058
+ const analytics = consentStore === null || consentStore === void 0 || (_consentStore$get = consentStore.get()) === null || _consentStore$get === void 0 ? void 0 : _consentStore$get.categories.analytics;
3059
+ if (isExpressMode()) return analytics === true;
3060
+ if (isImpliedMode()) return analytics !== false;
3061
+ return true;
3062
+ };
3063
+ const consentPostKey = (snapshot) => snapshot ? JSON.stringify({
3064
+ c: snapshot.categories,
3065
+ s: snapshot.signals,
3066
+ src: snapshot.source
3067
+ }) : null;
3068
+ /** Union browser-detected GPC/GPP into the authoritative snapshot. */
3069
+ const mergeDetectedSignalsIntoStore = () => {
3070
+ const current = consentStore === null || consentStore === void 0 ? void 0 : consentStore.get();
3071
+ if (!current || Object.keys(detectedSignals).length === 0) return;
3072
+ const merged = _objectSpread2(_objectSpread2({}, current.signals), detectedSignals);
3073
+ if (JSON.stringify(merged) !== JSON.stringify(current.signals)) consentStore.mergeSnapshot({ signals: merged }, current.source);
3074
+ };
3075
+ /** POST the authoritative snapshot to /consent, deduped on its wire key. */
3076
+ const pushConsentIfChanged = async (verification) => {
3077
+ var _ref;
3078
+ const snapshot = consentStore === null || consentStore === void 0 ? void 0 : consentStore.get();
3079
+ if (!snapshot) return {
3080
+ ok: true,
3081
+ reason: "no_snapshot"
3082
+ };
3083
+ const key = consentPostKey(snapshot);
3084
+ if (key === lastPostedConsentKey) return {
3085
+ ok: true,
3086
+ reason: "unchanged"
3087
+ };
3088
+ const prevKey = lastPostedConsentKey;
3089
+ lastPostedConsentKey = key;
3090
+ const result = await postConsent(sdkConfig, {
3091
+ anonymousId,
3092
+ userId: identifiedUserId,
3093
+ snapshot,
3094
+ identityVerification: (_ref = verification !== null && verification !== void 0 ? verification : lastIdentityVerification) !== null && _ref !== void 0 ? _ref : void 0
3095
+ });
3096
+ if (!result.ok && lastPostedConsentKey === key) lastPostedConsentKey = prevKey;
3097
+ return result;
3098
+ };
3099
+ const init = async (config) => {
3100
+ var _sdkConfig$consentSou;
3101
+ if (typeof window === "undefined") throw new Error("Leapio - script needs access to window object");
3102
+ disableTracking = hasExplicitDNT() || !!config.enableBrowserDNT && hasBrowserDNT() || !config.trackAutomatedBrowsers && isAutomatedBrowser();
3103
+ if (disableTracking) return;
3104
+ sdkConfig = _objectSpread2(_objectSpread2({}, DEFAULT_CONFIG), config);
3105
+ pendingEvents = [];
3106
+ superProperties = new SuperProperties(sdkConfig);
3107
+ campaign = new Campaign(sdkConfig);
3108
+ const { screen: { width, height }, navigator: { language }, location: { hostname, pathname, search, hash }, document } = window;
3109
+ const screen = `${width}x${height}`;
3110
+ currentUrl = `${pathname}${search}${hash}`;
3111
+ currentRef = document.referrer;
3112
+ anonymousId = ensureAnonymousId(getStorageOpts());
3113
+ consentStore = new ConsentStore(getStorageOpts());
3114
+ lastPostedConsentKey = consentPostKey(consentStore.get());
3115
+ detectedSignals = detectPrivacySignals();
3116
+ mergeDetectedSignalsIntoStore();
3117
+ teardownGpp === null || teardownGpp === void 0 || teardownGpp();
3118
+ teardownGpp = subscribeGpp(({ detected, gppString }) => {
3119
+ if (!detected) return;
3120
+ detectedSignals = _objectSpread2(_objectSpread2({}, detectedSignals), {}, { gppDetected: true }, gppString ? { gppString } : {});
3121
+ mergeDetectedSignalsIntoStore();
3122
+ });
3123
+ consentSourceRegistry === null || consentSourceRegistry === void 0 || consentSourceRegistry.stop();
3124
+ consentSourceRegistry = null;
3125
+ const sourceNames = (_sdkConfig$consentSou = sdkConfig.consentSources) !== null && _sdkConfig$consentSou !== void 0 ? _sdkConfig$consentSou : [];
3126
+ if (sourceNames.length > 0) {
3127
+ consentSourceRegistry = new ConsentSourceRegistry(instantiateConsentSources(sourceNames), applyConsentSnapshot);
3128
+ consentSourceRegistry.start();
3129
+ }
3130
+ getPageViewPayload = () => {
3131
+ return {
3132
+ hostname,
3133
+ screen,
3134
+ language
3135
+ };
3136
+ };
3137
+ if (sdkConfig.onLoad) await sdkConfig.onLoad();
3138
+ const normalizeUrl = (href) => {
3139
+ if (href.substring(0, 4) === "http") return "/" + href.split("/").splice(3).join("/");
3140
+ return href;
3141
+ };
3142
+ const handleRouteChange = () => {
3143
+ const nextUrl = normalizeUrl(document.location.href);
3144
+ if (nextUrl === currentUrl) return;
3145
+ engagementTracker === null || engagementTracker === void 0 || engagementTracker.pageChanged();
3146
+ currentRef = currentUrl;
3147
+ currentUrl = nextUrl;
3148
+ if (sdkConfig.enableAutoPageviews) pageview();
3149
+ };
3150
+ /**
3151
+ * SPA route-change detection. Default (History-API): patch `pushState` /
3152
+ * `replaceState` to emit a route-change signal, and listen to `popstate`
3153
+ * (back/forward) + `hashchange` (hash routers). The History-API patches are
3154
+ * restored on teardown. When `legacySpaDetection` is set, fall back to the
3155
+ * old MutationObserver-on-body heuristic instead. Returns a teardown.
3156
+ */
3157
+ const setupSpaPageviews = () => {
3158
+ if (sdkConfig.legacySpaDetection) {
3159
+ const bodyList = document.querySelector("body");
3160
+ if (!bodyList) return () => {};
3161
+ const observer = new MutationObserver(() => {
3162
+ handleRouteChange();
3163
+ });
3164
+ observer.observe(bodyList, {
3165
+ childList: true,
3166
+ subtree: true
3167
+ });
3168
+ return () => observer.disconnect();
3169
+ }
3170
+ const originalPushState = history.pushState;
3171
+ const originalReplaceState = history.replaceState;
3172
+ const onPopState = () => handleRouteChange();
3173
+ const onHashChange = () => handleRouteChange();
3174
+ const patchedPushState = function(...args) {
3175
+ const result = originalPushState.apply(this, args);
3176
+ handleRouteChange();
3177
+ return result;
3178
+ };
3179
+ const patchedReplaceState = function(...args) {
3180
+ const result = originalReplaceState.apply(this, args);
3181
+ handleRouteChange();
3182
+ return result;
3183
+ };
3184
+ history.pushState = patchedPushState;
3185
+ history.replaceState = patchedReplaceState;
3186
+ window.addEventListener("popstate", onPopState);
3187
+ window.addEventListener("hashchange", onHashChange);
3188
+ return () => {
3189
+ if (history.pushState === patchedPushState) history.pushState = originalPushState;
3190
+ if (history.replaceState === patchedReplaceState) history.replaceState = originalReplaceState;
3191
+ window.removeEventListener("popstate", onPopState);
3192
+ window.removeEventListener("hashchange", onHashChange);
3193
+ };
3194
+ };
3195
+ const update = () => {
3196
+ if (document.readyState === "complete") {
3197
+ if (sdkConfig.enableAutoPageviews) {
3198
+ pageview();
3199
+ if (!teardownSpaPageviews) teardownSpaPageviews = setupSpaPageviews();
3200
+ }
3201
+ }
3202
+ };
3203
+ teardownReadyListener === null || teardownReadyListener === void 0 || teardownReadyListener();
3204
+ document.addEventListener("readystatechange", update, true);
3205
+ teardownReadyListener = () => document.removeEventListener("readystatechange", update, true);
3206
+ update();
3207
+ teardownClassClicks === null || teardownClassClicks === void 0 || teardownClassClicks();
3208
+ teardownClassClicks = listenToClassClicks(({ name }) => {
3209
+ var _sdkConfig$pageviewsC;
3210
+ return track({
3211
+ event: name,
3212
+ channel: (_sdkConfig$pageviewsC = sdkConfig.pageviewsChannel) !== null && _sdkConfig$pageviewsC !== void 0 ? _sdkConfig$pageviewsC : null
3213
+ });
3214
+ });
3215
+ teardownPaymentLinks === null || teardownPaymentLinks === void 0 || teardownPaymentLinks();
3216
+ teardownPaymentLinks = sdkConfig.tagPaymentLinks ? listenForPaymentLinks({
3217
+ getAnonymousId: () => anonymousId,
3218
+ shouldTag: () => !disableTracking && canSendNow()
3219
+ }) : null;
3220
+ applyAutocapture();
3221
+ applyEngagement();
3222
+ };
3223
+ /**
3224
+ * Resolve the engagement mode (snippet ?? dashboard ?? "off") and (re)start
3225
+ * the tracker when it changed. Called on init and after each track response,
3226
+ * like autocapture, so a dashboard change applies without a reload.
3227
+ */
3228
+ const applyEngagement = () => {
3229
+ var _readRemoteConfig, _sdkConfig$engagement;
3230
+ if (!sdkConfig || disableTracking) return;
3231
+ const remote = (_readRemoteConfig = readRemoteConfig(sdkConfig.publicKey)) === null || _readRemoteConfig === void 0 ? void 0 : _readRemoteConfig.engagement;
3232
+ const mode = (_sdkConfig$engagement = sdkConfig.engagement) !== null && _sdkConfig$engagement !== void 0 ? _sdkConfig$engagement : isEngagementMode(remote) ? remote : "off";
3233
+ if (mode === appliedEngagementMode) return;
3234
+ appliedEngagementMode = mode;
3235
+ engagementTracker === null || engagementTracker === void 0 || engagementTracker.teardown();
3236
+ engagementTracker = mode === "off" ? null : setupEngagement({
3237
+ mode,
3238
+ send: (event, metadata) => {
3239
+ var _sdkConfig$pageviewsC2;
3240
+ track({
3241
+ event,
3242
+ channel: (_sdkConfig$pageviewsC2 = sdkConfig.pageviewsChannel) !== null && _sdkConfig$pageviewsC2 !== void 0 ? _sdkConfig$pageviewsC2 : null,
3243
+ metadata
3244
+ });
3245
+ }
3246
+ });
3247
+ };
3248
+ /**
3249
+ * Compute the effective autocapture config (snippet ?? server ?? default) and
3250
+ * (re)attach the delegated listeners. Idempotent: tears down any existing
3251
+ * listeners first, so it is safe to call again when a fresher server config
3252
+ * arrives on a track response. When autocapture resolves OFF, this leaves no
3253
+ * listeners attached (fail-safe), and core pageview tracking is unaffected.
3254
+ */
3255
+ const applyAutocapture = () => {
3256
+ if (!sdkConfig || disableTracking) return;
3257
+ const remote = readRemoteConfig(sdkConfig.publicKey);
3258
+ const autocaptureConfig = resolveEffectiveAutocaptureConfig(sdkConfig.autocapture, sdkConfig.enableAutoClicks, remote);
3259
+ const nextKey = JSON.stringify(autocaptureConfig);
3260
+ if (nextKey === appliedAutocaptureKey) return;
3261
+ appliedAutocaptureKey = nextKey;
3262
+ teardownAutocapture === null || teardownAutocapture === void 0 || teardownAutocapture();
3263
+ teardownAutocapture = null;
3264
+ if (autocaptureConfig) teardownAutocapture = setupAutocapture({
3265
+ config: autocaptureConfig,
3266
+ shouldTrack: () => !disableTracking,
3267
+ track: ({ event, metadata }) => {
3268
+ var _sdkConfig$pageviewsC3;
3269
+ return track({
3270
+ event,
3271
+ channel: (_sdkConfig$pageviewsC3 = sdkConfig.pageviewsChannel) !== null && _sdkConfig$pageviewsC3 !== void 0 ? _sdkConfig$pageviewsC3 : null,
3272
+ metadata
3273
+ });
3274
+ }
3275
+ });
3276
+ };
3277
+ const track = async (data) => {
3278
+ var _consentStore$get2;
3279
+ if (!sdkConfig || disableTracking) return;
3280
+ if (!sdkConfig.externalApiUrl && !sdkConfig.publicKey) {
3281
+ console.error("Leapio - SDK not configured. You need to call: leapio.init({publicKey: 'YOUR_PUBLIC_KEY'})");
3282
+ return;
3283
+ }
3284
+ if (data.value != null && data.value !== 0 && !data.currency) console.warn("Leapio - payment sent without a currency. Revenue reports leave it out unless the project counts payments without a currency. Pass currency, e.g. { currency: 'USD' }.");
3285
+ const analytics = consentStore === null || consentStore === void 0 || (_consentStore$get2 = consentStore.get()) === null || _consentStore$get2 === void 0 ? void 0 : _consentStore$get2.categories.analytics;
3286
+ if (isExpressMode()) {
3287
+ if (analytics === true) {
3288
+ await sendEvent(data);
3289
+ return;
3290
+ }
3291
+ if (analytics === false) return;
3292
+ if (pendingEvents.length >= MAX_PENDING_EVENTS) pendingEvents.shift();
3293
+ pendingEvents.push(_objectSpread2(_objectSpread2({}, data), {}, {
3294
+ url: currentUrl,
3295
+ referrer: currentRef,
3296
+ anonymousId
3297
+ }));
3298
+ return;
3299
+ }
3300
+ if (isImpliedMode() && analytics === false) return;
3301
+ await sendEvent(data);
3302
+ };
3303
+ /**
3304
+ * Compute the observed snippet-override report to attach to the next track
3305
+ * request, if any. Derives the EXPLICIT snippet override set from the same
3306
+ * normalization the autocapture resolver uses, then defers to the localStorage
3307
+ * dedup in `ac-overrides.ts`: returns a report only when the override set
3308
+ * differs from the last one reported for this public key (or, in the CLEAR
3309
+ * case, once after the overrides are removed). Best-effort — any failure
3310
+ * yields null so tracking is never affected.
3311
+ */
3312
+ const computeAcOverridesReport = () => {
3313
+ try {
3314
+ const normalized = normalizeSnippet(sdkConfig.autocapture);
3315
+ const report = buildSnippetOverrideReport(normalized, sdkConfig.enableAutoClicks);
3316
+ let url = null;
3317
+ try {
3318
+ const href = window.location.href;
3319
+ url = href ? href.split("#")[0] : null;
3320
+ if (url && sdkConfig.stripQueryParams) {
3321
+ var _stripUrl;
3322
+ url = (_stripUrl = stripUrl(url, sdkConfig.queryParamAllowlist)) !== null && _stripUrl !== void 0 ? _stripUrl : url;
3323
+ }
3324
+ } catch (_unused) {
3325
+ url = null;
3326
+ }
3327
+ return resolveAcOverridesToReport({
3328
+ publicKey: sdkConfig.publicKey,
3329
+ report,
3330
+ url
3331
+ });
3332
+ } catch (_unused2) {
3333
+ return null;
3334
+ }
3335
+ };
3336
+ /**
3337
+ * Build the wire payload for one event and send it, stamping the current
3338
+ * consent snapshot. Shared by `track()` and the express-mode flush.
3339
+ */
3340
+ const sendEvent = async (data) => {
3341
+ var _data$url, _ref2, _ref3, _data$userId, _data$anonymousId, _data$referrer, _data$metadata, _consentStore$get3;
3342
+ const payload = getPageViewPayload();
3343
+ const _superProperties$getP = superProperties.getProperties(), { userId: propsUserId } = _superProperties$getP, props = _objectWithoutProperties(_superProperties$getP, _excluded);
3344
+ const campaignParams = campaign.getCampaignParams();
3345
+ const cleanedUrl = removeQueryParamsFromUrl((_data$url = data.url) !== null && _data$url !== void 0 ? _data$url : currentUrl, CAMPAIGN_KEYWORDS);
3346
+ const resolvedUserId = (_ref2 = (_ref3 = (_data$userId = data.userId) !== null && _data$userId !== void 0 ? _data$userId : propsUserId ? `${propsUserId}` : null) !== null && _ref3 !== void 0 ? _ref3 : identifiedUserId) !== null && _ref2 !== void 0 ? _ref2 : null;
3347
+ await trackEvent(sdkConfig, {
3348
+ event: data.event,
3349
+ channel: data.channel,
3350
+ message: data.message,
3351
+ notify: data.notify,
3352
+ userId: resolvedUserId,
3353
+ anonymousId: (_data$anonymousId = data.anonymousId) !== null && _data$anonymousId !== void 0 ? _data$anonymousId : anonymousId,
3354
+ groupId: data.groupId,
3355
+ groupType: data.groupType,
3356
+ hostname: payload.hostname,
3357
+ language: payload.language,
3358
+ referrer: (_data$referrer = data.referrer) !== null && _data$referrer !== void 0 ? _data$referrer : currentRef,
3359
+ screen: payload.screen,
3360
+ url: cleanedUrl,
3361
+ value: data.value,
3362
+ currency: data.currency,
3363
+ metadata: _objectSpread2(_objectSpread2(_objectSpread2({}, campaignParams), props), (_data$metadata = data.metadata) !== null && _data$metadata !== void 0 ? _data$metadata : {})
3364
+ }, (_consentStore$get3 = consentStore === null || consentStore === void 0 ? void 0 : consentStore.get()) !== null && _consentStore$get3 !== void 0 ? _consentStore$get3 : null, computeAcOverridesReport());
3365
+ applyAutocapture();
3366
+ applyEngagement();
3367
+ };
3368
+ /**
3369
+ * Flush the express-mode queue once a snapshot is known. If analytics is
3370
+ * explicitly denied in the snapshot, drop the queue instead (the server is
3371
+ * the enforcement backstop, but we avoid sending events we know will be
3372
+ * dropped). Called after any snapshot change.
3373
+ */
3374
+ const flushPendingEvents = async () => {
3375
+ if (!pendingEvents.length) return;
3376
+ const queued = pendingEvents;
3377
+ pendingEvents = [];
3378
+ for (let i = 0; i < queued.length; i++) {
3379
+ var _consentStore$get4;
3380
+ const analytics = consentStore === null || consentStore === void 0 || (_consentStore$get4 = consentStore.get()) === null || _consentStore$get4 === void 0 ? void 0 : _consentStore$get4.categories.analytics;
3381
+ if (analytics === true) await sendEvent(queued[i]);
3382
+ else if (analytics === false) return;
3383
+ else {
3384
+ pendingEvents.push(...queued.slice(i));
3385
+ return;
3386
+ }
3387
+ }
3388
+ };
3389
+ /**
3390
+ * Public API: track a revenue event. Thin wrapper over `track()` that emits
3391
+ * the reserved revenue event name (default "Payment") with the monetary
3392
+ * `value` (MAJOR units, e.g. dollars) and ISO-4217 `currency`. Any other
3393
+ * options become event metadata (e.g. product, plan, interval, quantity).
3394
+ *
3395
+ * For Stripe attribution, pass `leapio.getAnonymousId()` (or the
3396
+ * `leapio_anonymous_id` cookie) into your Stripe Checkout session metadata as
3397
+ * `leapio_anonymous_id` so the connected-account webhook can link the payment
3398
+ * back to this visitor.
3399
+ */
3400
+ const revenue = async (amount, options = {}) => {
3401
+ if (!sdkConfig || disableTracking) return;
3402
+ if (typeof amount !== "number" || !isFinite(amount) || amount < 0) {
3403
+ console.error("Leapio - revenue() requires a finite, non-negative amount");
3404
+ return;
3405
+ }
3406
+ const { currency, event, channel, userId } = options, metadata = _objectWithoutProperties(options, _excluded2);
3407
+ await track({
3408
+ event: event !== null && event !== void 0 ? event : DEFAULT_REVENUE_EVENT_NAME,
3409
+ channel,
3410
+ userId,
3411
+ value: amount,
3412
+ currency,
3413
+ metadata
3414
+ });
3415
+ };
3416
+ const pageview = async (data) => {
3417
+ var _sdkConfig$pageviewsC4;
3418
+ if (!sdkConfig || disableTracking) return;
3419
+ await track({
3420
+ event: "Pageview",
3421
+ channel: (_sdkConfig$pageviewsC4 = sdkConfig.pageviewsChannel) !== null && _sdkConfig$pageviewsC4 !== void 0 ? _sdkConfig$pageviewsC4 : null,
3422
+ userId: data === null || data === void 0 ? void 0 : data.userId,
3423
+ metadata: data === null || data === void 0 ? void 0 : data.metadata
3424
+ });
3425
+ };
3426
+ const register = (properties) => {
3427
+ if (!sdkConfig || disableTracking) return true;
3428
+ return superProperties.register(properties);
3429
+ };
3430
+ const getProperties = () => {
3431
+ if (!sdkConfig || disableTracking) return {};
3432
+ return superProperties.getProperties();
3433
+ };
3434
+ const unregister = (property) => {
3435
+ if (!sdkConfig || disableTracking) return;
3436
+ superProperties.unregister(property);
3437
+ };
3438
+ const identify = async (newUserId, traits, opts) => {
3439
+ var _consentStore$get5;
3440
+ if (!sdkConfig) {
3441
+ console.warn("Leapio - identify() called before init()");
3442
+ return {
3443
+ ok: false,
3444
+ reason: "not_initialized"
3445
+ };
3446
+ }
3447
+ if (disableTracking) return {
3448
+ ok: false,
3449
+ reason: "tracking_disabled"
3450
+ };
3451
+ const normalizedUserId = `${newUserId !== null && newUserId !== void 0 ? newUserId : ""}`.trim();
3452
+ if (!normalizedUserId) {
3453
+ console.error("Leapio - identify requires a userId");
3454
+ return {
3455
+ ok: false,
3456
+ reason: "missing_user_id"
3457
+ };
3458
+ }
3459
+ const firstTouch = sanitizeFirstTouch(campaign.getFirstTouchParams());
3460
+ const mergedTraits = Object.keys(firstTouch).length > 0 ? _objectSpread2({ first_touch: firstTouch }, traits) : traits;
3461
+ const result = await identifyUser(sdkConfig, {
3462
+ anonymousId,
3463
+ userId: normalizedUserId,
3464
+ traits: mergedTraits,
3465
+ options: opts
3466
+ }, (_consentStore$get5 = consentStore === null || consentStore === void 0 ? void 0 : consentStore.get()) !== null && _consentStore$get5 !== void 0 ? _consentStore$get5 : null);
3467
+ if (result.ok) {
3468
+ var _opts$identityVerific;
3469
+ identifiedUserId = normalizedUserId;
3470
+ lastIdentityVerification = (_opts$identityVerific = opts === null || opts === void 0 ? void 0 : opts.identityVerification) !== null && _opts$identityVerific !== void 0 ? _opts$identityVerific : null;
3471
+ superProperties.register({ userId: identifiedUserId });
3472
+ }
3473
+ return result;
3474
+ };
3475
+ /**
3476
+ * Associate the current visitor with a group/account (Segment `group()`), e.g.
3477
+ * a company. Binds to the identified `user_id` when known, otherwise to the
3478
+ * managed `anonymousId` (the platform promotes anon memberships to the identity
3479
+ * on the next identify). Not gated on consent: a customer-directed linkage call
3480
+ * (legitimate interest); the server gates egress by purpose.
3481
+ */
3482
+ const group = async (groupId, traits, opts) => {
3483
+ if (!sdkConfig) {
3484
+ console.warn("Leapio - group() called before init()");
3485
+ return {
3486
+ ok: false,
3487
+ reason: "not_initialized"
3488
+ };
3489
+ }
3490
+ if (disableTracking) return {
3491
+ ok: false,
3492
+ reason: "tracking_disabled"
3493
+ };
3494
+ const normalizedGroupId = `${groupId !== null && groupId !== void 0 ? groupId : ""}`.trim();
3495
+ if (!normalizedGroupId) {
3496
+ console.error("Leapio - group requires a groupId");
3497
+ return {
3498
+ ok: false,
3499
+ reason: "missing_group_id"
3500
+ };
3501
+ }
3502
+ return groupUser(sdkConfig, {
3503
+ anonymousId,
3504
+ userId: identifiedUserId,
3505
+ groupId: normalizedGroupId,
3506
+ type: opts === null || opts === void 0 ? void 0 : opts.type,
3507
+ traits,
3508
+ options: opts
3509
+ });
3510
+ };
3511
+ /** Apply the url-privacy toggle to first-touch urls before they egress. */
3512
+ const sanitizeFirstTouch = (firstTouch) => {
3513
+ if (!(sdkConfig === null || sdkConfig === void 0 ? void 0 : sdkConfig.stripQueryParams)) return firstTouch;
3514
+ const sanitized = _objectSpread2({}, firstTouch);
3515
+ for (const key of ["landing_url", "referrer"]) if (sanitized[key]) {
3516
+ var _stripUrl2;
3517
+ sanitized[key] = (_stripUrl2 = stripUrl(sanitized[key], sdkConfig.queryParamAllowlist)) !== null && _stripUrl2 !== void 0 ? _stripUrl2 : sanitized[key];
3518
+ }
3519
+ return sanitized;
3520
+ };
3521
+ /**
3522
+ * Internal: apply a snapshot from a consent source through the store's
3523
+ * priority-aware `applySnapshot`. If accepted (became/stayed authoritative),
3524
+ * persists, flushes any express-mode queue, and pushes to `/consent`.
3525
+ *
3526
+ * This is the internal seam the consent-source plugins write through: a
3527
+ * source (Concord/OneTrust/GCM reader/...) constructs a
3528
+ * `ConsentSnapshot` with its `source` id and calls this; the store keeps the
3529
+ * highest-priority snapshot authoritative.
3530
+ *
3531
+ * Returns `true` if the snapshot was accepted, `false` if a higher-priority
3532
+ * source already owns the state.
3533
+ */
3534
+ const applyConsentSnapshot = async (snapshot) => {
3535
+ if (!sdkConfig || disableTracking || !consentStore) return false;
3536
+ if (!consentStore.applySnapshot(snapshot)) return false;
3537
+ mergeDetectedSignalsIntoStore();
3538
+ await flushPendingEvents();
3539
+ await pushConsentIfChanged();
3540
+ return true;
3541
+ };
3542
+ /**
3543
+ * Public API: explicit consent (highest-priority source `leapio_api`).
3544
+ * Merge-partial update — unspecified categories keep their prior value.
3545
+ * Persists, stamps subsequent events, flushes the express queue, and POSTs
3546
+ * the new state to `/consent`.
3547
+ */
3548
+ const setConsent = async (input, opts) => {
3549
+ if (!sdkConfig) {
3550
+ console.warn("Leapio - setConsent() called before init()");
3551
+ return {
3552
+ ok: false,
3553
+ reason: "not_initialized"
3554
+ };
3555
+ }
3556
+ if (disableTracking) return {
3557
+ ok: false,
3558
+ reason: "tracking_disabled"
3559
+ };
3560
+ const categories = {};
3561
+ if (typeof input.analytics === "boolean") categories.analytics = input.analytics;
3562
+ if (typeof input.functional === "boolean") categories.functional = input.functional;
3563
+ if (typeof input.marketing === "boolean") categories.marketing = input.marketing;
3564
+ const signals = typeof input.doNotSellShare === "boolean" ? { doNotSellShare: input.doNotSellShare } : {};
3565
+ consentStore.mergeSnapshot({
3566
+ categories,
3567
+ signals
3568
+ }, "leapio_api");
3569
+ mergeDetectedSignalsIntoStore();
3570
+ await flushPendingEvents();
3571
+ return pushConsentIfChanged(opts === null || opts === void 0 ? void 0 : opts.identityVerification);
3572
+ };
3573
+ /** Public API: read the current consent snapshot (or null if none yet). */
3574
+ const getConsent = () => {
3575
+ if (!sdkConfig || disableTracking || !consentStore) return null;
3576
+ return consentStore.get();
3577
+ };
3578
+ const reset = () => {
3579
+ var _sdkConfig$consentSou2;
3580
+ if (!sdkConfig || disableTracking) return;
3581
+ superProperties.clear();
3582
+ identifiedUserId = null;
3583
+ lastIdentityVerification = null;
3584
+ anonymousId = rotateAnonymousId(getStorageOpts());
3585
+ consentStore === null || consentStore === void 0 || consentStore.clear();
3586
+ pendingEvents = [];
3587
+ lastPostedConsentKey = null;
3588
+ consentSourceRegistry === null || consentSourceRegistry === void 0 || consentSourceRegistry.stop();
3589
+ consentSourceRegistry = null;
3590
+ const sourceNames = (_sdkConfig$consentSou2 = sdkConfig.consentSources) !== null && _sdkConfig$consentSou2 !== void 0 ? _sdkConfig$consentSou2 : [];
3591
+ if (sourceNames.length > 0) {
3592
+ consentSourceRegistry = new ConsentSourceRegistry(instantiateConsentSources(sourceNames), applyConsentSnapshot);
3593
+ consentSourceRegistry.start();
3594
+ }
3595
+ };
3596
+ /**
3597
+ * The current anonymous visitor id. This is the value to pass into your
3598
+ * Stripe Checkout session metadata as `leapio_anonymous_id` (it mirrors the
3599
+ * `leapio_anonymous_id` cookie) so Leapio's connected-account webhook can
3600
+ * attribute the resulting payment back to this visitor and their account.
3601
+ * Returns null when tracking is disabled or the SDK is not initialized.
3602
+ */
3603
+ const getAnonymousId = () => {
3604
+ if (!sdkConfig || disableTracking) return null;
3605
+ return anonymousId;
3606
+ };
3607
+ return {
3608
+ init,
3609
+ track,
3610
+ revenue,
3611
+ pageview,
3612
+ register,
3613
+ unregister,
3614
+ reset,
3615
+ identify,
3616
+ group,
3617
+ setConsent,
3618
+ getConsent,
3619
+ applyConsentSnapshot,
3620
+ getProperties,
3621
+ getAnonymousId
3622
+ };
3623
+ };
3624
+ const leapio = Leapio();
3625
+ //#endregion
3626
+ exports.DEFAULT_QUERY_PARAM_ALLOWLIST = DEFAULT_QUERY_PARAM_ALLOWLIST;
3627
+ exports.default = leapio;
3628
+ exports.leapio = leapio;