UNPKG

@trlogic/tracker-web

Version:
3,019 lines 128 kB
var TrackerManager = (function (exports, FingerprintJS, Bowser) {
    'use strict';

    /**
     * All variable types the Web SDK can collect.
     * Matches tracker-web_current VariableTypeDefinitions + Android structure.
     */
    exports.TrackerVariableType = void 0;
    (function (TrackerVariableType) {
        // --- DOM / Browser ---
        TrackerVariableType["URL"] = "URL";
        TrackerVariableType["ELEMENT"] = "ELEMENT";
        TrackerVariableType["COOKIE"] = "COOKIE";
        TrackerVariableType["JAVASCRIPT"] = "JAVASCRIPT";
        TrackerVariableType["EVENT"] = "EVENT";
        // --- Device ---
        TrackerVariableType["IP_ADDRESS"] = "IP_ADDRESS";
        TrackerVariableType["DEVICE_FINGERPRINT"] = "DEVICE_FINGERPRINT";
        TrackerVariableType["SCREEN_RESOLUTION"] = "SCREEN_RESOLUTION";
        TrackerVariableType["USER_AGENT"] = "USER_AGENT";
        TrackerVariableType["BROWSER"] = "BROWSER";
        TrackerVariableType["OS"] = "OS";
        TrackerVariableType["LANGUAGE"] = "LANGUAGE";
        TrackerVariableType["TIMEZONE"] = "TIMEZONE";
        TrackerVariableType["PLATFORM"] = "PLATFORM";
        // --- SDK ---
        TrackerVariableType["SDK_VERSION"] = "SDK_VERSION";
        TrackerVariableType["TIMESTAMP"] = "TIMESTAMP";
        TrackerVariableType["ELAPSED_TIME"] = "ELAPSED_TIME";
        TrackerVariableType["CURRENT_TRIGGER_NAME"] = "CURRENT_TRIGGER_NAME";
        TrackerVariableType["THEME_MODE"] = "THEME_MODE";
        // --- Security / Risk ---
        TrackerVariableType["SUSPICIOUS_ACTIVITY"] = "SUSPICIOUS_ACTIVITY";
        TrackerVariableType["SUSPICIOUS_BROWSER"] = "SUSPICIOUS_BROWSER";
        TrackerVariableType["REMOTE_ACCESS_SCORE"] = "REMOTE_ACCESS_SCORE";
        TrackerVariableType["REMOTE_ACCESS_FLAGS"] = "REMOTE_ACCESS_FLAGS";
        TrackerVariableType["ENVIRONMENT_CHANGE_FLAGS"] = "ENVIRONMENT_CHANGE_FLAGS";
        TrackerVariableType["ENVIRONMENT_CHANGE_SCORE"] = "ENVIRONMENT_CHANGE_SCORE";
        TrackerVariableType["NO_MOUSE_MOVE_DURATION_MS"] = "NO_MOUSE_MOVE_DURATION_MS";
        TrackerVariableType["CLICK_ONLY_PATTERN"] = "CLICK_ONLY_PATTERN";
        TrackerVariableType["FAST_PAGE_TRANSITION"] = "FAST_PAGE_TRANSITION";
        TrackerVariableType["FORM_FILL_ANOMALY"] = "FORM_FILL_ANOMALY";
        TrackerVariableType["FORM_FILL_DURATION_MS"] = "FORM_FILL_DURATION_MS";
        TrackerVariableType["NAVIGATION_VELOCITY"] = "NAVIGATION_VELOCITY";
        TrackerVariableType["INPUT_PASTE_RATIO"] = "INPUT_PASTE_RATIO";
        TrackerVariableType["HEADLESS_INDICATOR_COUNT"] = "HEADLESS_INDICATOR_COUNT";
        TrackerVariableType["REDIRECT_DETECTED"] = "REDIRECT_DETECTED";
        // --- Behavioral Biometrics ---
        TrackerVariableType["MOUSE_DYNAMICS"] = "MOUSE_DYNAMICS";
        TrackerVariableType["KEYSTROKE_DYNAMICS"] = "KEYSTROKE_DYNAMICS";
        TrackerVariableType["SCROLL_BEHAVIOR"] = "SCROLL_BEHAVIOR";
        TrackerVariableType["SESSION_BEHAVIOR"] = "SESSION_BEHAVIOR";
        TrackerVariableType["TOUCH_DYNAMICS"] = "TOUCH_DYNAMICS";
        TrackerVariableType["BEHAVIORAL_BIOMETRIC_SCORE"] = "BEHAVIORAL_BIOMETRIC_SCORE";
        TrackerVariableType["BEHAVIORAL_BIOMETRIC_FLAGS"] = "BEHAVIORAL_BIOMETRIC_FLAGS";
        // --- Activity ---
        TrackerVariableType["VIEW_DURATION"] = "VIEW_DURATION";
        // --- Custom ---
        TrackerVariableType["CUSTOM_TRIGGER_ARGS"] = "CUSTOM_TRIGGER_ARGS";
        // --- Network ---
        TrackerVariableType["REACHABLE"] = "REACHABLE";
    })(exports.TrackerVariableType || (exports.TrackerVariableType = {}));

    /**
     * Base collector with lazy evaluation and variable mapping.
     * Mirrors Android BaseDataCollector.
     *
     * Collectors only evaluate a variable if it is mapped in the backend config.
     */
    class BaseDataCollector {
        /**
         * Only call the supplier if the variable is actually mapped in at least one config.
         * This is the "lazy evaluation" pattern from Android.
         */
        collectIfNeeded(data, variableType, supplier, configs) {
            if (configs && !BaseDataCollector.isVariableMapped(variableType, configs)) {
                return;
            }
            try {
                data.set(variableType, supplier());
            }
            catch {
                // Per-variable error isolation — never crash
            }
        }
        /**
         * Maps collected TrackerVariableType → backend field names using config variableMappings.
         */
        static mapVariablesToBackend(config, variables) {
            const result = {};
            const mappings = config.event.variableMappings;
            for (const [backendField, variableName] of Object.entries(mappings)) {
                const variableType = variableName;
                const value = variables.get(variableType);
                if (variableType === exports.TrackerVariableType.CUSTOM_TRIGGER_ARGS) {
                    // Extract specific arg by backendField (mirrors Android customArgs.get(variableName))
                    if (value && typeof value === "object") {
                        result[backendField] = value[backendField] ?? null;
                    }
                    else {
                        result[backendField] = null;
                    }
                }
                else {
                    result[backendField] = value ?? null;
                }
            }
            return result;
        }
        /**
         * Check if a variable type is mapped in any config's variableMappings.
         */
        static isVariableMapped(variableType, configs) {
            return configs.some((config) => {
                const mappings = config.event.variableMappings;
                return Object.values(mappings).includes(variableType);
            });
        }
    }

    /**
     * Collector with TTL-based caching.
     * Mirrors Android CachedDataCollector.
     *
     * Each variable can have a different cache strategy (FAST: 10s, MEDIUM: 1min, SLOW: 5min, HEAVY: session).
     * If the cached value is still fresh, the supplier is not called.
     */
    class CachedDataCollector extends BaseDataCollector {
        constructor() {
            super(...arguments);
            this.cache = new Map();
        }
        /**
         * Collect with caching: only re-evaluate if cache has expired.
         */
        collectWithCache(data, variableType, strategy, supplier, configs) {
            this.collectIfNeeded(data, variableType, () => {
                const cached = this.cache.get(variableType);
                const now = Date.now();
                if (cached && now - cached.timestamp < cached.ttl) {
                    return cached.value;
                }
                const value = supplier();
                this.cache.set(variableType, { value, timestamp: now, ttl: strategy });
                return value;
            }, configs);
        }
        /**
         * Invalidate a specific cached variable.
         */
        invalidateCache(variableType) {
            this.cache.delete(variableType);
        }
        /**
         * Clear all cached values.
         */
        clearCache() {
            this.cache.clear();
        }
    }

    /**
     * Cache strategy for variable collection.
     * Controls how often a variable is re-collected.
     */
    exports.CacheStrategy = void 0;
    (function (CacheStrategy) {
        /** Re-collect every 10 seconds */
        CacheStrategy[CacheStrategy["FAST"] = 10000] = "FAST";
        /** Re-collect every 1 minute */
        CacheStrategy[CacheStrategy["MEDIUM"] = 60000] = "MEDIUM";
        /** Re-collect every 5 minutes */
        CacheStrategy[CacheStrategy["SLOW"] = 300000] = "SLOW";
        /** Collect once per session (never expires while SDK is alive) */
        CacheStrategy[CacheStrategy["HEAVY"] = 9007199254740991] = "HEAVY";
    })(exports.CacheStrategy || (exports.CacheStrategy = {}));

    /**
     * Welford's online algorithm for computing running statistics in O(1) memory.
     * No arrays needed — maintains mean, variance, min, max incrementally.
     */
    class RunningStats {
        constructor() {
            this.n = 0;
            this.m = 0; // running mean
            this.m2 = 0; // running sum of squared deviations
            this.lo = Infinity;
            this.hi = -Infinity;
        }
        push(value) {
            this.n++;
            const delta = value - this.m;
            this.m += delta / this.n;
            const delta2 = value - this.m;
            this.m2 += delta * delta2;
            if (value < this.lo)
                this.lo = value;
            if (value > this.hi)
                this.hi = value;
        }
        getMean() {
            return this.n === 0 ? 0 : this.m;
        }
        getStddev() {
            return this.n < 2 ? 0 : Math.sqrt(this.m2 / (this.n - 1));
        }
        getMin() {
            return this.n === 0 ? 0 : this.lo;
        }
        getMax() {
            return this.n === 0 ? 0 : this.hi;
        }
        getCount() {
            return this.n;
        }
        reset() {
            this.n = 0;
            this.m = 0;
            this.m2 = 0;
            this.lo = Infinity;
            this.hi = -Infinity;
        }
        toJSON() {
            return {
                mean: this.getMean(),
                stddev: this.getStddev(),
                min: this.getMin(),
                max: this.getMax(),
                count: this.getCount(),
            };
        }
    }

    /**
     * Lightweight trailing-edge throttle for high-frequency DOM events.
     * Drops intermediate calls, only executes at most once per `intervalMs`.
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    function throttle(fn, intervalMs) {
        let lastCall = 0;
        return ((...args) => {
            const now = performance.now();
            if (now - lastCall >= intervalMs) {
                lastCall = now;
                fn(...args);
            }
        });
    }

    const MAX_PATH_SEGMENTS = 20;
    const PAUSE_THRESHOLD_MS = 200;
    const IDLE_THRESHOLD_MS = 5000;
    const IDLE_CHECK_INTERVAL_MS = 5000;
    const INTERACTION_WINDOW_MS = 60000;
    const BURST_WINDOW_MS = 3000;
    const BURST_MIN_EVENTS = 5;
    const TAP_MAX_DURATION_MS = 300;
    const SWIPE_MIN_DISPLACEMENT_PX = 50;
    const INTER_KEY_MAX_GAP_MS = 2000;
    /**
     * Collects behavioral biometric signals from user interactions.
     * Computes aggregate statistics from mouse, keyboard, scroll, session, and touch data.
     *
     * Design principles:
     * - All DOM listeners use `{ passive: true }` to avoid blocking the main thread
     * - High-frequency events (mousemove, scroll, touchmove) are throttled
     * - All statistics use Welford's online algorithm (O(1) memory)
     * - No raw event streams stored — only summary aggregates
     * - `collect()` returns pre-computed summaries; called by EventCoordinator on existing events
     */
    class BehavioralCollector extends CachedDataCollector {
        constructor() {
            super(...arguments);
            // === Mouse tracking state ===
            this.mouseVelocityStats = new RunningStats();
            this.mouseAccelStats = new RunningStats();
            this.lastMouseSample = null;
            this.pathSegments = [];
            this.currentPathLength = 0;
            this.currentPathStartX = 0;
            this.currentPathStartY = 0;
            this.angularVarianceStats = new RunningStats();
            this.lastAngle = null;
            this.lastMouseSampleTs = 0;
            // === Keystroke tracking state ===
            this.lastKeydownTs = 0;
            this.lastKeyupTs = 0;
            this.interKeyStats = new RunningStats();
            this.dwellTimeStats = new RunningStats();
            this.flightTimeStats = new RunningStats();
            this.pendingKeydowns = new Map();
            // === Scroll tracking state ===
            this.lastScrollY = 0;
            this.lastScrollTs = 0;
            this.maxScrollDepthPx = 0;
            this.scrollSpeedStats = new RunningStats();
            this.scrollDirectionChanges = 0;
            this.lastScrollDirection = 0;
            this.scrollTotalDistance = 0;
            this.smoothScrollCount = 0;
            this.totalScrollSamples = 0;
            // === Session tracking state ===
            this.lastAnyInteractionTs = Date.now();
            this.idleCount = 0;
            this.totalIdleMs = 0;
            this.maxIdleMs = 0;
            this.currentIdleStart = null;
            this.tabSwitchCount = 0;
            this.totalHiddenMs = 0;
            this.lastHiddenTs = null;
            this.interactionTimestamps = [];
            this.burstCount = 0;
            // === Touch tracking state ===
            this.touchSupported = false;
            this.touchRadiusStats = new RunningStats();
            this.touchPressureStats = new RunningStats();
            this.swipeVelocityStats = new RunningStats();
            this.tapCount = 0;
            this.swipeCount = 0;
            this.activeTouchStart = null;
            // === Lifecycle ===
            this.listenersAttached = false;
            this.idleCheckIntervalId = null;
            this.initTs = 0;
            // === Bound handlers (for cleanup) ===
            this.boundMouseHandler = null;
            this.boundKeydownHandler = null;
            this.boundKeyupHandler = null;
            this.boundScrollHandler = null;
            this.boundVisibilityHandler = null;
            this.boundTouchStartHandler = null;
            this.boundTouchMoveHandler = null;
            this.boundTouchEndHandler = null;
        }
        collect() {
            const data = new Map();
            const sessionAge = Date.now() - this.initTs;
            this.collectWithCache(data, exports.TrackerVariableType.MOUSE_DYNAMICS, exports.CacheStrategy.FAST, () => this.computeMouseDynamics());
            this.collectWithCache(data, exports.TrackerVariableType.KEYSTROKE_DYNAMICS, exports.CacheStrategy.FAST, () => this.computeKeystrokeDynamics());
            this.collectWithCache(data, exports.TrackerVariableType.SCROLL_BEHAVIOR, exports.CacheStrategy.FAST, () => this.computeScrollBehavior());
            this.collectWithCache(data, exports.TrackerVariableType.SESSION_BEHAVIOR, exports.CacheStrategy.FAST, () => this.computeSessionBehavior());
            if (this.touchSupported) {
                this.collectWithCache(data, exports.TrackerVariableType.TOUCH_DYNAMICS, exports.CacheStrategy.FAST, () => this.computeTouchDynamics());
            }
            else {
                this.collectIfNeeded(data, exports.TrackerVariableType.TOUCH_DYNAMICS, () => null);
            }
            this.collectIfNeeded(data, exports.TrackerVariableType.BEHAVIORAL_BIOMETRIC_SCORE, () => this.computeBiometricScore(sessionAge));
            this.collectIfNeeded(data, exports.TrackerVariableType.BEHAVIORAL_BIOMETRIC_FLAGS, () => this.computeBiometricFlags(sessionAge));
            return data;
        }
        // === Lifecycle ===
        init() {
            this.initTs = Date.now();
            this.lastAnyInteractionTs = Date.now();
            this.touchSupported = "ontouchstart" in window;
            this.attachListeners();
            this.startIdleCheck();
        }
        destroy() {
            this.detachListeners();
            this.stopIdleCheck();
        }
        resetRollingWindow() {
            // Prune interaction timestamps to last 60s
            const cutoff = Date.now() - INTERACTION_WINDOW_MS;
            this.interactionTimestamps = this.interactionTimestamps.filter((t) => t >= cutoff);
        }
        // === Mouse dynamics computation ===
        computeMouseDynamics() {
            const vStats = this.mouseVelocityStats;
            const aStats = this.mouseAccelStats;
            let straightness = 0;
            if (this.pathSegments.length > 0) {
                let totalRatio = 0;
                for (const seg of this.pathSegments) {
                    totalRatio += seg.pathLength > 0 ? seg.displacement / seg.pathLength : 0;
                }
                straightness = totalRatio / this.pathSegments.length;
            }
            const angStats = this.angularVarianceStats;
            const jitter = angStats.getCount() > 1 ? Math.min(1, angStats.getStddev() / Math.PI) : 0;
            return {
                velocity: { mean: vStats.getMean(), max: vStats.getMax(), stddev: vStats.getStddev() },
                acceleration: { mean: aStats.getMean(), max: aStats.getMax() },
                straightness,
                jitter,
                sampleCount: vStats.getCount(),
            };
        }
        // === Keystroke dynamics computation ===
        computeKeystrokeDynamics() {
            const ikStats = this.interKeyStats;
            const mean = ikStats.getMean();
            const stddev = ikStats.getStddev();
            const rhythmConsistency = mean > 0 && ikStats.getCount() > 2 ? Math.max(0, Math.min(1, 1 - stddev / mean)) : 0;
            return {
                interKeyDelay: { mean, stddev, min: ikStats.getMin(), max: ikStats.getMax() },
                dwellTime: { mean: this.dwellTimeStats.getMean(), stddev: this.dwellTimeStats.getStddev() },
                flightTime: { mean: this.flightTimeStats.getMean(), stddev: this.flightTimeStats.getStddev() },
                rhythmConsistency,
                sampleCount: ikStats.getCount(),
            };
        }
        // === Scroll behavior computation ===
        computeScrollBehavior() {
            const pageHeight = document.documentElement.scrollHeight - window.innerHeight;
            const maxDepthPercent = pageHeight > 0 ? Math.min(100, (this.maxScrollDepthPx / pageHeight) * 100) : 0;
            return {
                maxDepthPercent,
                avgSpeed: this.scrollSpeedStats.getMean(),
                directionChanges: this.scrollDirectionChanges,
                totalDistance: this.scrollTotalDistance,
                smoothness: this.totalScrollSamples > 0 ? this.smoothScrollCount / this.totalScrollSamples : 0,
                sampleCount: this.totalScrollSamples,
            };
        }
        // === Session behavior computation ===
        computeSessionBehavior() {
            const now = Date.now();
            const cutoff = now - INTERACTION_WINDOW_MS;
            const recentInteractions = this.interactionTimestamps.filter((t) => t >= cutoff).length;
            return {
                idle: { count: this.idleCount, totalMs: this.totalIdleMs, maxMs: this.maxIdleMs },
                focus: { tabSwitchCount: this.tabSwitchCount, totalHiddenMs: this.totalHiddenMs },
                tempo: { interactionsPerMinute: recentInteractions, burstCount: this.burstCount },
            };
        }
        // === Touch dynamics computation ===
        computeTouchDynamics() {
            return {
                avgTouchRadius: this.touchRadiusStats.getMean(),
                avgPressure: this.touchPressureStats.getMean(),
                swipeVelocity: { mean: this.swipeVelocityStats.getMean(), max: this.swipeVelocityStats.getMax() },
                tapCount: this.tapCount,
                swipeCount: this.swipeCount,
                sampleCount: this.touchRadiusStats.getCount(),
            };
        }
        // === Composite biometric score ===
        computeBiometricScore(sessionAgeMs) {
            let score = 0;
            const minAge = 5000; // Need at least 5s of data
            if (sessionAgeMs < minAge)
                return 0;
            // Mouse anomaly (weight: 0.25)
            const mouseDyn = this.computeMouseDynamics();
            let mouseAnomaly = 0;
            if (mouseDyn.sampleCount > 10) {
                if (mouseDyn.straightness > 0.95)
                    mouseAnomaly += 0.4;
                if (mouseDyn.jitter < 0.05)
                    mouseAnomaly += 0.3;
                if (mouseDyn.velocity.stddev < 0.01 && mouseDyn.velocity.mean > 0)
                    mouseAnomaly += 0.3;
            }
            score += Math.min(1, mouseAnomaly) * 0.25;
            // Keystroke anomaly (weight: 0.20)
            const keyDyn = this.computeKeystrokeDynamics();
            let keystrokeAnomaly = 0;
            if (keyDyn.sampleCount > 10) {
                if (keyDyn.rhythmConsistency > 0.9)
                    keystrokeAnomaly += 0.5;
                if (keyDyn.dwellTime.mean < 10 && keyDyn.dwellTime.mean > 0)
                    keystrokeAnomaly += 0.5;
            }
            score += Math.min(1, keystrokeAnomaly) * 0.2;
            // Scroll anomaly (weight: 0.15)
            const scrollBeh = this.computeScrollBehavior();
            let scrollAnomaly = 0;
            if (scrollBeh.sampleCount > 3) {
                if (scrollBeh.smoothness < 0.2)
                    scrollAnomaly += 0.5;
                if (scrollBeh.directionChanges === 0 && scrollBeh.totalDistance > 500)
                    scrollAnomaly += 0.5;
            }
            score += Math.min(1, scrollAnomaly) * 0.15;
            // Session anomaly (weight: 0.15)
            const sessBeh = this.computeSessionBehavior();
            let sessionAnomaly = 0;
            if (sessionAgeMs > 60000) {
                if (sessBeh.idle.count === 0)
                    sessionAnomaly += 0.3;
                if (sessBeh.focus.tabSwitchCount === 0)
                    sessionAnomaly += 0.2;
            }
            if (sessBeh.tempo.interactionsPerMinute > 200)
                sessionAnomaly += 0.5;
            else if (sessBeh.tempo.interactionsPerMinute < 1 && sessionAgeMs > 30000)
                sessionAnomaly += 0.3;
            score += Math.min(1, sessionAnomaly) * 0.15;
            // Touch anomaly (weight: 0.10)
            if (this.touchSupported) {
                const touchDyn = this.computeTouchDynamics();
                let touchAnomaly = 0;
                if (touchDyn.sampleCount > 5) {
                    if (touchDyn.avgPressure === 0)
                        touchAnomaly += 0.5;
                    if (touchDyn.avgTouchRadius === 0)
                        touchAnomaly += 0.5;
                }
                score += Math.min(1, touchAnomaly) * 0.1;
            }
            // Data absence (weight: 0.15)
            let absenceAnomaly = 0;
            if (sessionAgeMs > 30000) {
                if (mouseDyn.sampleCount === 0 && !this.touchSupported)
                    absenceAnomaly += 0.33;
                if (keyDyn.sampleCount === 0)
                    absenceAnomaly += 0.33;
                if (scrollBeh.sampleCount === 0)
                    absenceAnomaly += 0.34;
            }
            score += Math.min(1, absenceAnomaly) * 0.15;
            return Math.min(1, score);
        }
        // === Biometric flags ===
        computeBiometricFlags(sessionAgeMs) {
            const mouseDyn = this.computeMouseDynamics();
            const keyDyn = this.computeKeystrokeDynamics();
            const scrollBeh = this.computeScrollBehavior();
            const sessBeh = this.computeSessionBehavior();
            return {
                perfectMouseLinearity: mouseDyn.sampleCount > 10 && mouseDyn.straightness > 0.95,
                zeroMouseJitter: mouseDyn.sampleCount > 10 && mouseDyn.jitter < 0.05,
                uniformKeystrokeTiming: keyDyn.sampleCount > 10 && keyDyn.rhythmConsistency > 0.9,
                noKeystrokeData: keyDyn.sampleCount === 0 && sessionAgeMs > 30000,
                programmaticScrolling: scrollBeh.sampleCount > 3 && scrollBeh.smoothness < 0.2,
                noIdlePeriods: sessBeh.idle.count === 0 && sessionAgeMs > 60000,
                noTouchPressure: this.touchSupported && this.touchPressureStats.getCount() > 5 && this.touchPressureStats.getMean() === 0,
                abnormalTempo: sessBeh.tempo.interactionsPerMinute > 200 || (sessBeh.tempo.interactionsPerMinute < 1 && sessionAgeMs > 30000),
            };
        }
        // === DOM Listener attachment ===
        attachListeners() {
            if (this.listenersAttached)
                return;
            this.listenersAttached = true;
            // --- Mouse (throttled 50ms) ---
            this.boundMouseHandler = throttle((e) => {
                this.onMouseSample(e.clientX, e.clientY, performance.now());
                this.recordInteraction();
            }, 50);
            window.addEventListener("mousemove", this.boundMouseHandler, { passive: true });
            // --- Keydown ---
            this.boundKeydownHandler = (e) => {
                if (e.repeat)
                    return;
                const now = performance.now();
                // Inter-key delay
                if (this.lastKeydownTs > 0) {
                    const gap = now - this.lastKeydownTs;
                    if (gap < INTER_KEY_MAX_GAP_MS) {
                        this.interKeyStats.push(gap);
                    }
                }
                this.lastKeydownTs = now;
                // Flight time (keyup → keydown)
                if (this.lastKeyupTs > 0) {
                    const flight = now - this.lastKeyupTs;
                    if (flight < INTER_KEY_MAX_GAP_MS) {
                        this.flightTimeStats.push(flight);
                    }
                }
                this.pendingKeydowns.set(e.code, now);
                this.recordInteraction();
            };
            document.addEventListener("keydown", this.boundKeydownHandler, { passive: true });
            // --- Keyup ---
            this.boundKeyupHandler = (e) => {
                const now = performance.now();
                this.lastKeyupTs = now;
                const downTs = this.pendingKeydowns.get(e.code);
                if (downTs !== undefined) {
                    const dwell = now - downTs;
                    this.dwellTimeStats.push(dwell);
                    this.pendingKeydowns.delete(e.code);
                }
            };
            document.addEventListener("keyup", this.boundKeyupHandler, { passive: true });
            // --- Scroll (throttled 200ms) ---
            this.boundScrollHandler = throttle(() => {
                this.onScrollSample();
                this.recordInteraction();
            }, 200);
            window.addEventListener("scroll", this.boundScrollHandler, { passive: true });
            // --- Visibility change ---
            this.boundVisibilityHandler = () => {
                const now = Date.now();
                if (document.hidden) {
                    this.lastHiddenTs = now;
                }
                else {
                    if (this.lastHiddenTs !== null) {
                        this.totalHiddenMs += now - this.lastHiddenTs;
                        this.lastHiddenTs = null;
                    }
                    this.tabSwitchCount++;
                }
            };
            document.addEventListener("visibilitychange", this.boundVisibilityHandler);
            // --- Touch (conditional) ---
            if (this.touchSupported) {
                this.boundTouchStartHandler = (e) => {
                    const touch = e.touches[0];
                    if (!touch)
                        return;
                    this.activeTouchStart = { x: touch.clientX, y: touch.clientY, t: performance.now() };
                    this.touchRadiusStats.push((touch.radiusX + touch.radiusY) / 2);
                    if (touch.force > 0)
                        this.touchPressureStats.push(touch.force);
                    this.recordInteraction();
                };
                document.addEventListener("touchstart", this.boundTouchStartHandler, { passive: true });
                this.boundTouchMoveHandler = throttle((e) => {
                    const touch = e.touches[0];
                    if (!touch)
                        return;
                    this.touchRadiusStats.push((touch.radiusX + touch.radiusY) / 2);
                    if (touch.force > 0)
                        this.touchPressureStats.push(touch.force);
                }, 100);
                document.addEventListener("touchmove", this.boundTouchMoveHandler, { passive: true });
                this.boundTouchEndHandler = (e) => {
                    if (!this.activeTouchStart)
                        return;
                    const touch = e.changedTouches[0];
                    if (!touch)
                        return;
                    const now = performance.now();
                    const duration = now - this.activeTouchStart.t;
                    const dx = touch.clientX - this.activeTouchStart.x;
                    const dy = touch.clientY - this.activeTouchStart.y;
                    const displacement = Math.sqrt(dx * dx + dy * dy);
                    if (duration < TAP_MAX_DURATION_MS && displacement < SWIPE_MIN_DISPLACEMENT_PX) {
                        this.tapCount++;
                    }
                    else if (displacement >= SWIPE_MIN_DISPLACEMENT_PX) {
                        this.swipeCount++;
                        const velocity = displacement / duration;
                        this.swipeVelocityStats.push(velocity);
                    }
                    this.activeTouchStart = null;
                };
                document.addEventListener("touchend", this.boundTouchEndHandler, { passive: true });
            }
        }
        detachListeners() {
            if (this.boundMouseHandler)
                window.removeEventListener("mousemove", this.boundMouseHandler);
            if (this.boundKeydownHandler)
                document.removeEventListener("keydown", this.boundKeydownHandler);
            if (this.boundKeyupHandler)
                document.removeEventListener("keyup", this.boundKeyupHandler);
            if (this.boundScrollHandler)
                window.removeEventListener("scroll", this.boundScrollHandler);
            if (this.boundVisibilityHandler)
                document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
            if (this.boundTouchStartHandler)
                document.removeEventListener("touchstart", this.boundTouchStartHandler);
            if (this.boundTouchMoveHandler)
                document.removeEventListener("touchmove", this.boundTouchMoveHandler);
            if (this.boundTouchEndHandler)
                document.removeEventListener("touchend", this.boundTouchEndHandler);
            this.listenersAttached = false;
        }
        // === Mouse sample processing ===
        onMouseSample(x, y, t) {
            if (this.lastMouseSample) {
                const dt = t - this.lastMouseSample.t;
                if (dt <= 0)
                    return;
                const dx = x - this.lastMouseSample.x;
                const dy = y - this.lastMouseSample.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const velocity = dist / dt;
                this.mouseVelocityStats.push(velocity);
                // Acceleration
                const dv = Math.abs(velocity - this.lastMouseSample.velocity);
                const accel = dv / dt;
                this.mouseAccelStats.push(accel);
                // Path straightness — track segments between pauses
                const timeSinceLastSample = t - this.lastMouseSampleTs;
                if (timeSinceLastSample > PAUSE_THRESHOLD_MS && this.currentPathLength > 0) {
                    // End previous segment
                    const dispX = this.lastMouseSample.x - this.currentPathStartX;
                    const dispY = this.lastMouseSample.y - this.currentPathStartY;
                    const displacement = Math.sqrt(dispX * dispX + dispY * dispY);
                    this.pushPathSegment({ displacement, pathLength: this.currentPathLength });
                    this.currentPathLength = 0;
                    this.currentPathStartX = x;
                    this.currentPathStartY = y;
                }
                else if (this.currentPathLength === 0) {
                    this.currentPathStartX = this.lastMouseSample.x;
                    this.currentPathStartY = this.lastMouseSample.y;
                }
                this.currentPathLength += dist;
                // Angular jitter
                if (dist > 1) {
                    const angle = Math.atan2(dy, dx);
                    if (this.lastAngle !== null) {
                        let angleDiff = Math.abs(angle - this.lastAngle);
                        if (angleDiff > Math.PI)
                            angleDiff = 2 * Math.PI - angleDiff;
                        this.angularVarianceStats.push(angleDiff);
                    }
                    this.lastAngle = angle;
                }
                this.lastMouseSample = { x, y, t, velocity };
            }
            else {
                this.lastMouseSample = { x, y, t, velocity: 0 };
                this.currentPathStartX = x;
                this.currentPathStartY = y;
            }
            this.lastMouseSampleTs = t;
        }
        pushPathSegment(segment) {
            if (this.pathSegments.length >= MAX_PATH_SEGMENTS) {
                this.pathSegments.shift();
            }
            this.pathSegments.push(segment);
        }
        // === Scroll sample processing ===
        onScrollSample() {
            const scrollY = window.scrollY;
            const now = performance.now();
            if (scrollY > this.maxScrollDepthPx) {
                this.maxScrollDepthPx = scrollY;
            }
            if (this.lastScrollTs > 0) {
                const dt = now - this.lastScrollTs;
                const deltaY = scrollY - this.lastScrollY;
                const absDelta = Math.abs(deltaY);
                if (dt > 0 && absDelta > 0) {
                    const speed = absDelta / dt;
                    this.scrollSpeedStats.push(speed);
                    this.scrollTotalDistance += absDelta;
                    this.totalScrollSamples++;
                    // Smoothness: small increments vs large jumps
                    if (absDelta < 100)
                        this.smoothScrollCount++;
                    // Direction changes
                    const dir = deltaY > 0 ? 1 : -1;
                    if (this.lastScrollDirection !== 0 && dir !== this.lastScrollDirection) {
                        this.scrollDirectionChanges++;
                    }
                    this.lastScrollDirection = dir;
                }
            }
            this.lastScrollY = scrollY;
            this.lastScrollTs = now;
        }
        // === Interaction tracking ===
        recordInteraction() {
            const now = Date.now();
            this.lastAnyInteractionTs = now;
            this.interactionTimestamps.push(now);
            // Close idle period if active
            if (this.currentIdleStart !== null) {
                const idleDuration = now - this.currentIdleStart;
                this.totalIdleMs += idleDuration;
                if (idleDuration > this.maxIdleMs)
                    this.maxIdleMs = idleDuration;
                this.idleCount++;
                this.currentIdleStart = null;
            }
            // Detect bursts
            const burstCutoff = now - BURST_WINDOW_MS;
            const recentCount = this.interactionTimestamps.filter((t) => t >= burstCutoff).length;
            if (recentCount === BURST_MIN_EVENTS) {
                this.burstCount++;
            }
            // Prune old timestamps (keep last 60s)
            if (this.interactionTimestamps.length > 500) {
                const cutoff = now - INTERACTION_WINDOW_MS;
                this.interactionTimestamps = this.interactionTimestamps.filter((t) => t >= cutoff);
            }
        }
        // === Idle check ===
        startIdleCheck() {
            this.idleCheckIntervalId = setInterval(() => {
                const now = Date.now();
                if (now - this.lastAnyInteractionTs > IDLE_THRESHOLD_MS && this.currentIdleStart === null) {
                    this.currentIdleStart = this.lastAnyInteractionTs;
                }
            }, IDLE_CHECK_INTERVAL_MS);
        }
        stopIdleCheck() {
            if (this.idleCheckIntervalId !== null) {
                clearInterval(this.idleCheckIntervalId);
                this.idleCheckIntervalId = null;
            }
        }
    }

    /** SDK version — auto-referenced by collectors. */
    const SDK_VERSION = "2.0.0";

    /**
     * Browser/OS/platform detection using Bowser.
     * Mirrors Android DeviceCollector's device info gathering.
     */
    class BrowserDetector {
        static getParser() {
            if (!BrowserDetector.parser) {
                BrowserDetector.parser = Bowser.getParser(navigator.userAgent);
            }
            return BrowserDetector.parser;
        }
        static getBrowser() {
            const browser = BrowserDetector.getParser().getBrowser();
            return {
                name: browser.name ?? "Unknown",
                version: browser.version ?? "Unknown",
            };
        }
        static getOS() {
            const os = BrowserDetector.getParser().getOS();
            return {
                name: os.name ?? "Unknown",
                version: os.version ?? "Unknown",
            };
        }
        static getPlatform() {
            const type = BrowserDetector.getParser().getPlatformType();
            if (type === "mobile")
                return "mobile";
            if (type === "tablet")
                return "tablet";
            return "desktop";
        }
        static getLanguage() {
            return navigator.language || "unknown";
        }
        static getTimezone() {
            try {
                return Intl.DateTimeFormat().resolvedOptions().timeZone;
            }
            catch {
                return "unknown";
            }
        }
        /**
         * Detect if the browser looks suspicious (bot, headless, automated).
         * Returns true if known bot user-agent patterns are detected.
         */
        static isSuspiciousBrowser() {
            const ua = navigator.userAgent.toLowerCase();
            const botPatterns = [
                "bot",
                "crawler",
                "spider",
                "headless",
                "phantom",
                "selenium",
                "puppeteer",
                "playwright",
                "webdriver",
                "electron",
            ];
            return botPatterns.some((p) => ua.includes(p));
        }
        /**
         * Count headless/bot indicators in the current environment.
         */
        static getHeadlessIndicatorCount() {
            let count = 0;
            // webdriver flag
            if (navigator.webdriver)
                count++;
            // no languages
            if (!navigator.languages || navigator.languages.length === 0)
                count++;
            // no plugins
            if (!navigator.plugins || navigator.plugins.length === 0)
                count++;
            // suspicious user-agent patterns
            if (BrowserDetector.isSuspiciousBrowser())
                count++;
            // WebGL availability check
            try {
                const canvas = document.createElement("canvas");
                const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
                if (!gl)
                    count++;
            }
            catch {
                count++;
            }
            // screen dimensions anomaly (0x0 or very small)
            if (screen.width === 0 || screen.height === 0)
                count++;
            return count;
        }
        /** Force parser re-creation (e.g. after UA change in tests). */
        static reset() {
            BrowserDetector.parser = null;
        }
    }
    BrowserDetector.parser = null;

    /**
     * Collects device/browser/environment information.
     * Mirrors Android DeviceCollector — adapted for web environment.
     */
    class DeviceCollector extends CachedDataCollector {
        constructor() {
            super(...arguments);
            this.fingerprintPromise = null;
            this.resolvedFingerprint = null;
            this.initTimestamp = Date.now();
        }
        collect() {
            const data = new Map();
            this.collectWithCache(data, exports.TrackerVariableType.DEVICE_FINGERPRINT, exports.CacheStrategy.HEAVY, () => this.getFingerprint());
            this.collectWithCache(data, exports.TrackerVariableType.SCREEN_RESOLUTION, exports.CacheStrategy.HEAVY, () => `${screen.width}x${screen.height}`);
            this.collectWithCache(data, exports.TrackerVariableType.USER_AGENT, exports.CacheStrategy.HEAVY, () => navigator.userAgent);
            this.collectWithCache(data, exports.TrackerVariableType.BROWSER, exports.CacheStrategy.HEAVY, () => {
                const b = BrowserDetector.getBrowser();
                return `${b.name} ${b.version}`;
            });
            this.collectWithCache(data, exports.TrackerVariableType.OS, exports.CacheStrategy.HEAVY, () => {
                const os = BrowserDetector.getOS();
                return `${os.name} ${os.version}`;
            });
            this.collectWithCache(data, exports.TrackerVariableType.LANGUAGE, exports.CacheStrategy.MEDIUM, () => BrowserDetector.getLanguage());
            this.collectWithCache(data, exports.TrackerVariableType.TIMEZONE, exports.CacheStrategy.MEDIUM, () => BrowserDetector.getTimezone());
            this.collectWithCache(data, exports.TrackerVariableType.PLATFORM, exports.CacheStrategy.HEAVY, () => BrowserDetector.getPlatform());
            this.collectWithCache(data, exports.TrackerVariableType.SUSPICIOUS_BROWSER, exports.CacheStrategy.SLOW, () => BrowserDetector.isSuspiciousBrowser());
            this.collectWithCache(data, exports.TrackerVariableType.HEADLESS_INDICATOR_COUNT, exports.CacheStrategy.SLOW, () => BrowserDetector.getHeadlessIndicatorCount());
            this.collectWithCache(data, exports.TrackerVariableType.THEME_MODE, exports.CacheStrategy.MEDIUM, () => window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "DARK" : "LIGHT");
            // Non-cached (always fresh)
            this.collectIfNeeded(data, exports.TrackerVariableType.SDK_VERSION, () => SDK_VERSION);
            this.collectIfNeeded(data, exports.TrackerVariableType.TIMESTAMP, () => Date.now());
            this.collectIfNeeded(data, exports.TrackerVariableType.ELAPSED_TIME, () => Date.now() - this.initTimestamp);
            return data;
        }
        /**
         * Returns the resolved fingerprint synchronously if available,
         * otherwise returns the pending Promise.
         */
        getFingerprint() {
            if (this.resolvedFingerprint)
                return this.resolvedFingerprint;
            if (!this.fingerprintPromise) {
                this.fingerprintPromise = FingerprintJS.load()
                    .then((fp) => fp.get())
                    .then((result) => {
                    this.resolvedFingerprint = result.visitorId;
                    return result.visitorId;
                })
                    .catch(() => {
                    this.resolvedFingerprint = "unavailable";
                    return "unavailable";
                });
            }
            return this.fingerprintPromise;
        }
        /**
         * Pre-load fingerprint so it's ready when collect() is called.
         * Must be awaited before the first event fires.
         */
        async initFingerprint() {
            const result = this.getFingerprint();
            if (result instanceof Promise) {
                const resolved = await result;
                this.resolvedFingerprint = resolved;
                this.invalidateCache(exports.TrackerVariableType.DEVICE_FINGERPRINT);
                return resolved;
            }
            return result;
        }
    }

    /**
     * SDK logger. Disabled by default. Enable via `debug: true` in configuration.
     * Mirrors Android Logger util.
     */
    class Logger {
        static setEnabled(enabled) {
            Logger.enabled = enabled;
        }
        static debug(...args) {
            if (Logger.enabled)
                console.debug(Logger.PREFIX, ...args);
        }
        static info(...args) {
            if (Logger.enabled)
                console.info(Logger.PREFIX, ...args);
        }
        static warn(...args) {
            if (Logger.enabled)
                console.warn(Logger.PREFIX, ...args);
        }
        static error(...args) {
            if (Logger.enabled)
                console.error(Logger.PREFIX, ...args);
        }
    }
    Logger.PREFIX = "[tracker]";
    Logger.enabled = false;

    class HttpError extends Error {
        constructor(message, status) {
            super(message);
            this.status = status;
            this.name = "HttpError";
        }
    }
    /**
     * Fetch API wrapper with timeout, JSON serialization, and error handling.
     * Mirrors Android DefaultNetworkService HTTP layer.
     */
    class HttpClient {
        static async get(url, config) {
            return HttpClient.request(url, { method: "GET", ...config });
        }
        static async post(url, body, config) {
            return HttpClient.request(url, { method: "POST", body, ...config });
        }
        static async request(url, options) {
            const controller = new AbortController();
            const timeoutMs = options.timeout ?? 30000;
            const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
            try {
                const headers = {
                    ...options.headers,
                };
                // Only set Content-Type for requests with a body (POST/PUT/PATCH).
                // Adding it to GET triggers CORS preflight on cross-origin APIs.
                if (options.body !== undefined) {
                    headers["Content-Type"] = "application/json";
                }
                const fetchOptions = {
                    method: options.method,
                    headers,
                    signal: controller.signal,
                };
                if (options.body !== undefined) {
                    fetchOptions.body = JSON.stringify(options.body);
                }
                const response = await fetch(url, fetchOptions);
                if (!response.ok) {
                    throw new HttpError(`HTTP Error ${response.status}: ${response.statusText}`, response.status);
                }
                const contentType = response.headers.get("content-type") ?? "";
                if (contentType.includes("application/json")) {
                    return (await response.json());
                }
                return (await response.text());
            }
            catch (error) {
                if (error instanceof HttpError)
                    throw error;
                if (error instanceof DOMException && error.name === "AbortError") {
                    throw new HttpError(`Request timeout after ${timeoutMs}ms`, 408);
                }
                Logger.error("HTTP request failed:", error);
                throw error;
            }
            finally {
                clearTimeout(timeoutId);
            }
        }
    }

    const IP_API_URL = "https://api.ipify.org?format=json";
    /**
     * Collects network-related information.
     * Mirrors Android NetworkCollector — adapted for web.
     */
    class NetworkCollector extends CachedDataCollector {
        constructor() {
            super(...arguments);
            this.resolvedIp = null;
        }
        collect() {
            const data = new Map();
            this.collectWithCache(data, exports.TrackerVariableType.IP_ADDRESS, exports.CacheStrategy.SLOW, () => this.resolvedIp ?? "unknown");
            this.collectIfNeeded(data, exports.TrackerVariableType.REACHABLE, () => navigator.onLine);
            return data;
        }
        /**
         * Fetch the client's public IP address.
         * Called during initialization; result is cached.
         */
        async fetchIp() {
            try {
                const response = await HttpClient.get(IP_API_URL, { timeout: 5000 });
                this.resolvedIp = response.ip;
                return this.resolvedIp;
            }
            catch (error) {
                Logger.warn("Failed to fetch IP address:", error);
                this.resolvedIp = "unknown";
                return this.resolvedIp;
            }
        }
    }

    /**
     * Collects screen/page activity variables: URL parts, cookies, elements, JS evaluation, view duration.
     * Mirrors Android ScreenActivityCollector + web-specific variable resolution from tracker-web_current.
     */
    class ScreenActivityCollector extends BaseDataCollector {
        constructor() {
            super(...arguments);
            this.viewDurationMs = 0;
            this.timerIntervalId = null;
        }
        collect() {
            const data = new Map();
            this.collectIfNeeded(data, exports.TrackerVariableType.VIEW_DURATION, () => this.viewDurationMs);
            return data;
        }
        /**
         * Resolve a variable from the current page context using the backend config's variable config.
         * This handles URL, ELEMENT, COOKIE, JAVASCRIPT, EVENT types.
         */
        resolveVariable(varConfig, eventData) {
            try {
                switch (varConfig.type) {
                    case exports.TrackerVariableType.URL:
                        return this.resolveUrl(varConfig.option);
                    case exports.TrackerVariableType.ELEMENT:
                        return this.resolveElement(varConfig.option);
                    case exports.TrackerVariableType.COOKIE:
                        return this.resolveCookie(varConfig.option);
                    case exports.TrackerVariableType.JAVASCRIPT:
                        return this.resolveJavascript(varConfig.option);
                    case exports.TrackerVariableType.EVENT:
                        return this.resolveEvent(varConfig, eventData);
                    default:
                        return null;
                }
            }
            catch (error) {
                Logger.warn(`Failed to resolve variable ${varConfig.name}:`, error);
                return null;
            }
        }
        // === Timer (view duration) ===
        startTimer() {
            if (this.timerIntervalId !== null)
                return;
            this.viewDurationMs = 0;
            this.timerIntervalId = setInterval(() => {
                this.viewDurationMs += 100;
            }, 100);
        }
        stopTimer() {
            if (this.timerIntervalId !== null) {
                clearInterval(this.timerIntervalId);
                this.timerIntervalId = null;
            }
        }
        resetTimer() {
            this.viewDurationMs = 0;
        }
        // === URL Resolution ===
        resolveUrl(option) {
            const selection = option?.selection ?? "full";
            const url = new URL(window.location.href);
            switch (selection) {
                case "host":
                    return url.hostname;
                case "port":
                    return url.port;
                case "path":
                    return url.pathname;
                case "query":
                    return url.search;
                case "fragment":
                    return url.hash;
                case "protocol":
                    return url.protocol;
                case "full":
                default:
                    return url.href;
            }
        }
        // === Element Resolution ===
        resolveElement(option) {
            if (!option?.cssSelector)
                return null;
            const el = document.querySelector(option.cssSelector);
            if (!el)
                return null;
            if (option.attribute) {
                return el.getAttribute(option.attribute);
            }
            return el.textContent;
        }
        // === Cookie Resolution ===
        resolveCookie(option) {
            if (!option?.cookieName)
                return null;
            const cookies = document.cookie.split(";");
            for (const cookie of cookies) {
                const [name, ...valueParts] = cookie.split("=");
                if (name.trim() === option.cookieName) {
                    const value = valueParts.join("=").trim();
                    return option.decodeUrlCookie ? decodeURIComponent(value) : value;
                }
            }
            return null;
        }
        // === JavaScript Resolution (using new Function instead of eval) ===
        resolveJavascript(option) {
            if (!option?.code)
                return null;
            try {
                // Use new Function() instead of eval() for slightly better isolation
                return new Function(`return (${option.code})`)();
            }
            catch (error) {
                Logger.warn("JavaScript variable evaluation failed:", error);
                return null;
            }
        }
        // === Event Resolution ===
        resolveEvent(varConfig, eventData) {
            if (!eventData)
                return null;
            return eventData[varConfig.name] ?? null;
        }
        /** Resolve all config-driven variables for a given config item. */
        resolveConfigVariables(config, eventData) {
            const data = new Map();
            for (const varConfig of config.variables) {
                const type = varConfig.type;
                // Only resolve DOM/page-context types here
                if (type === exports.TrackerVariableType.URL ||
                    type === exports.TrackerVariableType.ELEMENT ||
                    type === exports.TrackerVariableType.COOKIE ||
                    type === exports.TrackerVariableType.JAVASCRIPT ||
                    type === exports.TrackerVariableType.EVENT) {
                    const value = this.resolveVariable(varConfig, eventData);
                    if (value !== null && value !== undefined) {
                        data.set(type, value);
                    }
                }
            }
            return data;
        }
        destroy() {
            this.stopTimer();
        }
    }

    const BASELINE_KEY_PREFIX = "formica_baseline_";
    /**
     * Collects security & risk metrics.
     * Mirrors Android SecurityCollector + merges tracker-web_current risk-metrics + remote control detection.
     *
     * Responsibilities:
     * 1. Remote control suspicion monitoring (mouse, clipboard, FPS, delayed clicks)
     * 2. Risk metrics aggregation (mouse movement, click patterns, form fill, navigation, paste ratio)
     * 3. Environment baseline tracking (language, timezone, OS, browser)
     * 4. Composite remote access score calculation
     */
    class SecurityCollector extends CachedDataCollector {
        constructor() {
            super(...arguments);
            this.suspiciousFlags = {
                unnaturalMouseMoves: false,
                bigClipboardPaste: false,
                lowFPSDetected: false,
                delayedClickDetected: false,
            };
            // Risk metrics state
            this.lastMouseMoveTs = Date.now();
            this.mouseMoveCounterWindow = 0;
            this.clickCounterWindow = 0;
            this.pageViewTimestamps = [];
            this.pasteCharCount = 0;
            this.typedCharCount = 0;
            this.lastPageViewTs = null;
            this.lastFormInteractionStart = null;
            this.lastFormFieldCount = 0;
            this.lastFormDurationMs = 0;
            this.formFillAnomaly = false;
            this.fastPageTransition = false;
            this.redirectSource = null;
            this.lastUrl = null;
            // Remote control monitoring state
            this.lastMouseX = null;
            this.lastMouseY = null;
            this.unnaturalMoveCount = 0;
            this.lowFPSCount = 0;
            this.lastFrameTs = 0;
            this.fpsRafId = null;
            this.listenersAttached = false;
            // Environment baseline
            this.baseline = null;
            this.envChangeFlags = null;
            this.envChangeScore = 0;
            // Tenant/device context (set during init)
            this.tenant = "";
            this.deviceId = "";
        }
        collect() {
            const data = new Map();
            const agg = this.getAggregates();
            // Suspicious flags
            this.collectWithCache(data, exports.TrackerVariableType.SUSPICIOUS_ACTIVITY, exports.CacheStrategy.FAST, () => ({
                ...this.suspiciousFlags,
            }));
            // Risk aggregates
            this.collectIfNeeded(data, exports.TrackerVariableType.NO_MOUSE_MOVE_DURATION_MS, () => agg.noMouseMoveDurationMs);
            this.collectIfNeeded(data, exports.TrackerVariableType.CLICK_ONLY_PATTERN, () => agg.clickOnlyPattern);
            this.collectIfNeeded(data, exports.TrackerVariableType.FAST_PAGE_TRANSITION, () => agg.fastPageTransition);
            this.collectIfNeeded(data, exports.TrackerVariableType.NAVIGATION_VELOCITY, () => agg.navigationVelocity);
            this.collectIfNeeded(data, exports.TrackerVariableType.FORM_FILL_ANOMALY, () => agg.formFillAnomaly);
            this.collectIfNeeded(data, exports.TrackerVariableType.FORM_FILL_DURATION_MS, () => agg.formFillDurationMs);
            this.collectIfNeeded(data, exports.TrackerVariableType.INPUT_PASTE_RATIO, () => agg.inputPasteRatio);
            this.collectIfNeeded(data, exports.TrackerVariableType.REDIRECT_DETECTED, () => agg.redirectSource);
            // Environment change
            this.collectIfNeeded(data, exports.TrackerVariableType.ENVIRONMENT_CHANGE_FLAGS, () => this.envChangeFlags ? { ...this.envChangeFlags } : null);
            this.collectIfNeeded(data, exports.TrackerVariableType.ENVIRONMENT_CHANGE_SCORE, () => this.envChangeScore);
            // Composite scores
            const headlessCount = BrowserDetector.getHeadlessIndicatorCount();
            const remoteAccessScore = this.calcRemoteAccessScore(headlessCount);
            this.collectIfNeeded(data, exports.TrackerVariableType.REMOTE_ACCESS_SCORE, () => remoteAccessScore);
            this.collectIfNeeded(data, exports.TrackerVariableType.REMOTE_ACCESS_FLAGS, () => ({
                ...this.suspiciousFlags,
                clickOnlyPattern: agg.clickOnlyPattern,
                headlessIndicatorCount: headlessCount,
                environmentMismatch: this.envChangeScore > 0,
            }));
            return data;
        }
        // === Initialization ===
        /**
         * Start monitoring. Called once during SDK initialization.
         */
        init(tenant, deviceId) {
            this.tenant = tenant;
            this.deviceId = deviceId;
            this.attachListeners();
            this.ensureBaseline();
        }
        // === Page view tracking ===
        registerPageView() {
            const now = Date.now();
            this.fastPageTransition = this.lastPageViewTs !== null && now - this.lastPageViewTs < 1500;
            this.lastPageViewTs = now;
            this.pageViewTimestamps.push(now);
            // Keep last 5 minutes only
            const cutoff = now - 5 * 60 * 1000;
            this.pageViewTimestamps = this.pageViewTimestamps.filter((t) => t >= cutoff);
            this.detectRedirect();
        }
        // === Risk score ===
        getRemoteAccessScore() {
            return this.calcRemoteAccessScore(BrowserDetector.getHeadlessIndicatorCount());
        }
        resetRollingWindow() {
            this.mouseMoveCounterWindow = 0;
            this.clickCounterWindow = 0;
        }
        // === Environment baseline ===
        updateEnvironmentDiff() {
            if (!this.baseline)
                return;
            const current = this.getCurrentEnvironment();
            this.envChangeFlags = this.diffEnvironment(this.baseline, current);
            this.envChangeScore = this.calcEnvironmentChangeScore(this.envChangeFlags);
        }
        // === Internal ===
        getAggregates() {
            const now = Date.now();
            const noMouseMoveDurationMs = now - this.lastMouseMoveTs;
            const clickOnlyPattern = noMouseMoveDurationMs > 10000 && this.clickCounterWindow >= 3 && this.mouseMoveCounterWindow < 2;
            const navigationVelocity = this.pageViewTimestamps.length / 5;
            const inputPasteRatio = this.computePasteRatio();
            return {
                noMouseMoveDurationMs,
                clickOnlyPattern,
                fastPageTransition: this.fastPageTransition,
                navigationVelocity,
                formFillAnomaly: this.formFillAnomaly,
                formFillDurationMs: this.lastFormDurationMs,
                inputPasteRatio,
                redirectSource: this.redirectSource,
            };
        }
        computePasteRatio() {
            const total = this.pasteCharCount + this.typedCharCount;
            if (total === 0)
                return 0;
            return this.pasteCharCount / total;
        }
        calcRemoteAccessScore(headlessIndicatorCount) {
            const agg = this.getAggregates();
            let score = 0;
            if (this.suspiciousFlags.unnaturalMouseMoves)
                score += 0.2;
            if (this.suspiciousFlags.bigClipboardPaste)
                score += 0.15;
            if (this.suspiciousFlags.lowFPSDetected)
                score += 0.15;
            if (this.suspiciousFlags.delayedClickDetected)
                score += 0.1;
            if (agg.clickOnlyPattern)
                score += 0.15;
            if (headlessIndicatorCount > 0)
                score += Math.min(headlessIndicatorCount * 0.05, 0.25);
            if (this.envChangeScore > 0)
                score += 0.2;
            return Math.min(1, score);
        }
        // === Remote control suspicion listeners ===
        attachListeners() {
            if (this.listenersAttached)
                return;
            this.listenersAttached = true;
            // Mouse move — risk metrics + unnatural detection
            window.addEventListener("mousemove", (e) => {
                this.lastMouseMoveTs = Date.now();
                this.mouseMoveCounterWindow++;
                if (this.lastMouseX !== null && this.lastMouseY !== null) {
                    const dx = Math.abs(e.clientX - this.lastMouseX);
                    const dy = Math.abs(e.clientY - this.lastMouseY);
                    if (dx > 100 || dy > 100 || (dx === 0 && dy === 0))
                        this.unnaturalMoveCount++;
                    if (this.unnaturalMoveCount > 5)
                        this.suspiciousFlags.unnaturalMouseMoves = true;
                }
                this.lastMouseX = e.clientX;
                this.lastMouseY = e.clientY;
            });
            // Click counter
            document.addEventListener("click", () => {
                this.clickCounterWindow++;
                // Delayed click detection
                const start = performance.now();
                setTimeout(() => {
                    const delay = performance.now() - start;
                    if (delay > 150)
                        this.suspiciousFlags.delayedClickDetected = true;
                }, 0);
            });
            // Paste detection
            document.addEventListener("paste", (e) => {
                const ce = e;
                const text = ce.clipboardData?.getData("text") ?? "";
                this.pasteCharCount += text.length;
                if (text.length > 5000)
                    this.suspiciousFlags.bigClipboardPaste = true;
            });
            // Keydown counting
            document.addEventListener("keydown", (e) => {
                if (e.key.length === 1)
                    this.typedCharCount++;
            });
            // Form tracking
            document.addEventListener("focusin", (e) => {
                const form = e.target?.closest("form");
                if (!form)
                    return;
                if (this.lastFormInteractionStart === null) {
                    this.lastFormInteractionStart = Date.now();
                    this.lastFormFieldCount = form.querySelectorAll("input,textarea,select").length;
                }
            });
            document.addEventListener("submit", (e) => {
                if (!e.target)
                    return;
                if (this.lastFormInteractionStart) {
                    this.lastFormDurationMs = Date.now() - this.lastFormInteractionStart;
                    this.formFillAnomaly = this.detectFormAnomaly(this.lastFormDurationMs, this.lastFormFieldCount);
                }
                this.lastFormInteractionStart = null;
            });
            // FPS monitoring
            this.lastFrameTs = performance.now();
            const monitorFPS = () => {
                const now = performance.now();
                const delta = now - this.lastFrameTs;
                if (delta > 250)
                    this.lowFPSCount++;
                if (this.lowFPSCount > 10)
                    this.suspiciousFlags.lowFPSDetected = true;
                this.lastFrameTs = now;
                this.fpsRafId = requestAnimationFrame(monitorFPS);
            };
            this.fpsRafId = requestAnimationFrame(monitorFPS);
        }
        detectFormAnomaly(durationMs, fieldCount) {
            if (fieldCount === 0)
                return false;
            const adaptiveThreshold = Math.min(fieldCount * 120, 1500);
            return durationMs < adaptiveThreshold;
        }
        detectRedirect() {
            try {
                const currentUrl = window.location.href;
                if (this.lastUrl && this.lastUrl !== currentUrl) {
                    if (document.referrer && document.referrer !== currentUrl) {
                        try {
                            const referrerUrl = new URL(document.referrer);
                            const currentUrlObj = new URL(currentUrl);
                            this.redirectSource = referrerUrl.origin !== currentUrlObj.origin ? referrerUrl.origin : "internal";
                        }
                        catch {
                            this.redirectSource = document.referrer;
                        }
                    }
                    else if (!document.referrer) {
                        this.redirectSource = "direct";
                    }
                    else {
                        this.redirectSource = null;
                    }
                }
                else {
                    this.redirectSource = null;
                }
                this.lastUrl = currentUrl;
            }
            catch {
                this.redirectSource = null;
            }
        }
        // === Baseline ===
        ensureBaseline() {
            try {
                const key = `${BASELINE_KEY_PREFIX}${this.tenant}_${this.deviceId}`;
                const raw = localStorage.getItem(key);
                if (raw) {
                    this.baseline = JSON.parse(raw);
                }
                else {
                    const current = this.getCurrentEnvironment();
                    this.baseline = { ...current, firstSeen: Date.now() };
                    localStorage.setItem(key, JSON.stringify(this.baseline));
                }
                this.updateEnvironmentDiff();
            }
            catch {
                Logger.warn("localStorage unavailable for environment baseline");
            }
        }
        getCurrentEnvironment() {
            const browser = BrowserDetector.getBrowser();
            const os = BrowserDetector.getOS();
            return {
                language: BrowserDetector.getLanguage(),
                timezone: BrowserDetector.getTimezone(),
                os: `${os.name} ${os.version}`,
                browser: `${browser.name} ${browser.version}`,
            };
        }
        diffEnvironment(baseline, current) {
            return {
                languageChanged: baseline.language !== current.language,
                timezoneChanged: baseline.timezone !== current.timezone,
                osChanged: baseline.os !== current.os,
                browserChanged: baseline.browser !== current.browser,
            };
        }
        calcEnvironmentChangeScore(flags) {
            let score = 0;
            if (flags.languageChanged)
                score += 0.25;
            if (flags.timezoneChanged)
                score += 0.25;
            if (flags.osChanged)
                score += 0.2;
            if (flags.browserChanged)
                score += 0.3;
            return Math.min(1, score);
        }
        /** Cleanup FPS monitor on termination. */
        destroy() {
            if (this.fpsRafId !== null) {
                cancelAnimationFrame(this.fpsRafId);
                this.fpsRafId = null;
            }
        }
    }

    /**
     * Web-specific event types that trigger data collection.
     * Mirrors Android TrackerEventType but adapted for browser environment.
     */
    exports.TrackerEventType = void 0;
    (function (TrackerEventType) {
        TrackerEventType["PAGE_VIEW"] = "PAGE_VIEW";
        TrackerEventType["CLICK"] = "CLICK";
        TrackerEventType["SCROLL"] = "SCROLL";
        TrackerEventType["CUSTOM"] = "CUSTOM";
        TrackerEventType["RISK_ALERT"] = "RISK_ALERT";
        TrackerEventType["VISIBILITY_CHANGE"] = "VISIBILITY_CHANGE";
        TrackerEventType["IDLE"] = "IDLE";
        TrackerEventType["FORM_INTERACTION"] = "FORM_INTERACTION";
        TrackerEventType["COPY_PASTE"] = "COPY_PASTE";
    })(exports.TrackerEventType || (exports.TrackerEventType = {}));

    /**
     * Listens for click events on the document.
     * Uses a cooldown to prevent excessive event triggering.
     * Mirrors Android TouchListener.
     */
    class ClickListener {
        constructor(trigger, cooldownMs = 1000) {
            this.started = false;
            this.boundHandler = null;
            this.lastTriggerTs = 0;
            this.trigger = trigger;
            this.cooldownMs = cooldownMs;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundHandler = () => {
                const now = Date.now();
                if (now - this.lastTriggerTs >= this.cooldownMs) {
                    this.lastTriggerTs = now;
                    this.trigger(exports.TrackerEventType.CLICK);
                }
            };
            document.addEventListener("click", this.boundHandler);
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundHandler) {
                document.removeEventListener("click", this.boundHandler);
                this.boundHandler = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.CLICK];
        }
        getVariableTypes() {
            return [exports.TrackerVariableType.ELEMENT];
        }
    }

    /**
     * Triggers COPY_PASTE event when the user pastes content.
     * Uses cooldown to prevent spam from rapid paste operations.
     */
    class CopyPasteListener {
        constructor(trigger, cooldownMs = 2000) {
            this.started = false;
            this.boundHandler = null;
            this.lastTriggerTs = 0;
            this.trigger = trigger;
            this.cooldownMs = cooldownMs;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundHandler = () => {
                const now = Date.now();
                if (now - this.lastTriggerTs >= this.cooldownMs) {
                    this.lastTriggerTs = now;
                    this.trigger(exports.TrackerEventType.COPY_PASTE);
                }
            };
            document.addEventListener("paste", this.boundHandler);
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundHandler) {
                document.removeEventListener("paste", this.boundHandler);
                this.boundHandler = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.COPY_PASTE];
        }
        getVariableTypes() {
            return [exports.TrackerVariableType.INPUT_PASTE_RATIO];
        }
    }

    /**
     * Triggers FORM_INTERACTION event on form focus-in and submit.
     * Uses cooldown to prevent excessive triggering when user tabs
     * between multiple form fields rapidly.
     */
    class FormInteractionListener {
        constructor(trigger, cooldownMs = 3000) {
            this.started = false;
            this.boundFocusHandler = null;
            this.boundSubmitHandler = null;
            this.lastTriggerTs = 0;
            this.trigger = trigger;
            this.cooldownMs = cooldownMs;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundFocusHandler = (e) => {
                const target = e.target;
                if (!target)
                    return;
                const isFormElement = target instanceof HTMLInputElement ||
                    target instanceof HTMLTextAreaElement ||
                    target instanceof HTMLSelectElement;
                if (isFormElement) {
                    this.triggerWithCooldown();
                }
            };
            this.boundSubmitHandler = () => {
                this.triggerWithCooldown();
            };
            document.addEventListener("focusin", this.boundFocusHandler, { passive: true });
            document.addEventListener("submit", this.boundSubmitHandler, { passive: true });
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundFocusHandler) {
                document.removeEventListener("focusin", this.boundFocusHandler);
                this.boundFocusHandler = null;
            }
            if (this.boundSubmitHandler) {
                document.removeEventListener("submit", this.boundSubmitHandler);
                this.boundSubmitHandler = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.FORM_INTERACTION];
        }
        getVariableTypes() {
            return [exports.TrackerVariableType.FORM_FILL_ANOMALY, exports.TrackerVariableType.FORM_FILL_DURATION_MS];
        }
        triggerWithCooldown() {
            const now = Date.now();
            if (now - this.lastTriggerTs >= this.cooldownMs) {
                this.lastTriggerTs = now;
                this.trigger(exports.TrackerEventType.FORM_INTERACTION);
            }
        }
    }

    const ACTIVITY_EVENTS = ["mousemove", "keydown", "scroll", "touchstart", "click"];
    /**
     * Triggers IDLE event when the user has been inactive for a configurable duration.
     * Resets the timer on any user interaction. Uses cooldown to avoid repeated
     * IDLE events during prolonged inactivity.
     */
    class IdleListener {
        constructor(trigger, idleThresholdMs = 30000, cooldownMs = 60000) {
            this.started = false;
            this.idleTimer = null;
            this.boundActivityHandler = null;
            this.lastTriggerTs = 0;
            this.trigger = trigger;
            this.idleThresholdMs = idleThresholdMs;
            this.cooldownMs = cooldownMs;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundActivityHandler = () => this.resetTimer();
            for (const event of ACTIVITY_EVENTS) {
                document.addEventListener(event, this.boundActivityHandler, { passive: true });
            }
            this.resetTimer();
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.idleTimer) {
                clearTimeout(this.idleTimer);
                this.idleTimer = null;
            }
            if (this.boundActivityHandler) {
                for (const event of ACTIVITY_EVENTS) {
                    document.removeEventListener(event, this.boundActivityHandler);
                }
                this.boundActivityHandler = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.IDLE];
        }
        getVariableTypes() {
            return [];
        }
        resetTimer() {
            if (this.idleTimer)
                clearTimeout(this.idleTimer);
            this.idleTimer = setTimeout(() => {
                const now = Date.now();
                if (now - this.lastTriggerTs >= this.cooldownMs) {
                    this.lastTriggerTs = now;
                    this.trigger(exports.TrackerEventType.IDLE);
                }
            }, this.idleThresholdMs);
        }
    }

    /**
     * Listens for page navigation events (popstate, pushState, replaceState).
     * Mirrors Android ActivityLifecycleListener for PAGE_VIEW.
     */
    class PageViewListener {
        constructor(trigger) {
            this.started = false;
            this.boundPopstate = null;
            this.origPushState = null;
            this.origReplaceState = null;
            this.trigger = trigger;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            // popstate
            this.boundPopstate = () => this.trigger(exports.TrackerEventType.PAGE_VIEW);
            window.addEventListener("popstate", this.boundPopstate);
            // Patch pushState/replaceState to detect SPA navigation
            this.origPushState = history.pushState.bind(history);
            this.origReplaceState = history.replaceState.bind(history);
            const origPush = this.origPushState;
            const origReplace = this.origReplaceState;
            const trigger = () => this.trigger(exports.TrackerEventType.PAGE_VIEW);
            history.pushState = function (...args) {
                if (origPush)
                    origPush(...args);
                trigger();
            };
            history.replaceState = function (...args) {
                if (origReplace)
                    origReplace(...args);
                trigger();
            };
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundPopstate) {
                window.removeEventListener("popstate", this.boundPopstate);
                this.boundPopstate = null;
            }
            if (this.origPushState) {
                history.pushState = this.origPushState;
                this.origPushState = null;
            }
            if (this.origReplaceState) {
                history.replaceState = this.origReplaceState;
                this.origReplaceState = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.PAGE_VIEW];
        }
        getVariableTypes() {
            return [exports.TrackerVariableType.URL, exports.TrackerVariableType.VIEW_DURATION];
        }
    }

    /**
     * Listens for scroll events (debounced + cooldown).
     * Uses debounce to wait for scroll to settle, then cooldown to prevent
     * excessive event triggering during continuous scrolling.
     */
    class ScrollListener {
        constructor(trigger, debounceMs = 800, cooldownMs = 2000) {
            this.started = false;
            this.boundHandler = null;
            this.debounceTimer = null;
            this.lastTriggerTs = 0;
            this.trigger = trigger;
            this.debounceMs = debounceMs;
            this.cooldownMs = cooldownMs;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundHandler = () => {
                if (this.debounceTimer)
                    clearTimeout(this.debounceTimer);
                this.debounceTimer = setTimeout(() => {
                    const now = Date.now();
                    if (now - this.lastTriggerTs >= this.cooldownMs) {
                        this.lastTriggerTs = now;
                        this.trigger(exports.TrackerEventType.SCROLL);
                    }
                }, this.debounceMs);
            };
            window.addEventListener("scroll", this.boundHandler, { passive: true });
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundHandler) {
                window.removeEventListener("scroll", this.boundHandler);
                this.boundHandler = null;
            }
            if (this.debounceTimer) {
                clearTimeout(this.debounceTimer);
                this.debounceTimer = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.SCROLL];
        }
        getVariableTypes() {
            return [];
        }
    }

    /**
     * Listens for visibility change events (tab focus/blur).
     * Mirrors Android AppStateListener.
     */
    class VisibilityListener {
        constructor(trigger) {
            this.started = false;
            this.boundHandler = null;
            this.trigger = trigger;
        }
        start() {
            if (this.started)
                return;
            this.started = true;
            this.boundHandler = () => this.trigger(exports.TrackerEventType.VISIBILITY_CHANGE);
            document.addEventListener("visibilitychange", this.boundHandler);
        }
        stop() {
            if (!this.started)
                return;
            this.started = false;
            if (this.boundHandler) {
                document.removeEventListener("visibilitychange", this.boundHandler);
                this.boundHandler = null;
            }
        }
        getTypes() {
            return [exports.TrackerEventType.VISIBILITY_CHANGE];
        }
        getVariableTypes() {
            return [exports.TrackerVariableType.VIEW_DURATION];
        }
    }

    /**
     * Filter operators for variable filtering.
     * Identical to Android/iOS FilterOperator — full set of 24 operators.
     */
    exports.FilterOperator = void 0;
    (function (FilterOperator) {
        FilterOperator["IS_EQUALS"] = "isEquals";
        FilterOperator["IS_EQUALS_IGNORE_CASE"] = "isEqualsIgnoreCase";
        FilterOperator["NOT_EQUALS"] = "notEquals";
        FilterOperator["NOT_EQUALS_IGNORE_CASE"] = "notEqualsIgnoreCase";
        FilterOperator["IS_CONTAINS"] = "isContains";
        FilterOperator["IS_CONTAINS_IGNORE_CASE"] = "isContainsIgnoreCase";
        FilterOperator["NOT_CONTAINS"] = "notContains";
        FilterOperator["NOT_CONTAINS_IGNORE_CASE"] = "notContainsIgnoreCase";
        FilterOperator["IS_STARTS_WITH"] = "isStartsWith";
        FilterOperator["IS_STARTS_WITH_IGNORE_CASE"] = "isStartsWithIgnoreCase";
        FilterOperator["NOT_STARTS_WITH"] = "notStartsWith";
        FilterOperator["NOT_STARTS_WITH_IGNORE_CASE"] = "notStartsWithIgnoreCase";
        FilterOperator["IS_ENDS_WITH"] = "isEndsWith";
        FilterOperator["IS_ENDS_WITH_IGNORE_CASE"] = "isEndsWithIgnoreCase";
        FilterOperator["NOT_ENDS_WITH"] = "notEndsWith";
        FilterOperator["NOT_ENDS_WITH_IGNORE_CASE"] = "notEndsWithIgnoreCase";
        FilterOperator["IS_REGEX_MATCH"] = "isRegexMatch";
        FilterOperator["NOT_REGEX_MATCH"] = "notRegexMatch";
        FilterOperator["LESS_THAN"] = "lessThan";
        FilterOperator["LESS_THAN_OR_EQUALS"] = "lessThanOrEquals";
        FilterOperator["GREATER_THAN"] = "greaterThan";
        FilterOperator["GREATER_THAN_OR_EQUALS"] = "greaterThanOrEquals";
    })(exports.FilterOperator || (exports.FilterOperator = {}));

    /**
     * Applies filter conditions to variable values.
     * Mirrors Android/iOS FilterEngine logic.
     *
     * Trigger filters use AND logic: ALL filters must pass.
     * For arrays/iterables, the filter is applied to each element.
     */
    class FilterEngine {
        /**
         * Returns true if ALL filters pass against the given variables.
         */
        static passesTriggerFilters(filters, variables) {
            if (!filters || filters.length === 0)
                return true;
            return filters.every((filter) => {
                const value = variables.get(filter.field);
                return FilterEngine.applyFilter(value, filter.operator, filter.value);
            });
        }
        /**
         * Apply a single filter operator to a field value.
         */
        static applyFilter(fieldValue, operator, filterValue) {
            if (fieldValue === null || fieldValue === undefined) {
                // Null/undefined values only pass NOT operators
                return FilterEngine.isNegationOperator(operator);
            }
            // Handle arrays: filter passes if ANY element matches
            if (Array.isArray(fieldValue)) {
                const anyMatch = fieldValue.some((item) => FilterEngine.applySingleFilter(String(item), operator, filterValue));
                return FilterEngine.isNegationOperator(operator) ? !anyMatch : anyMatch;
            }
            return FilterEngine.applySingleFilter(String(fieldValue), operator, filterValue);
        }
        static applySingleFilter(value, operator, filterValue) {
            switch (operator) {
                case exports.FilterOperator.IS_EQUALS:
                    return value === filterValue;
                case exports.FilterOperator.IS_EQUALS_IGNORE_CASE:
                    return value.toLowerCase() === filterValue.toLowerCase();
                case exports.FilterOperator.NOT_EQUALS:
                    return value !== filterValue;
                case exports.FilterOperator.NOT_EQUALS_IGNORE_CASE:
                    return value.toLowerCase() !== filterValue.toLowerCase();
                case exports.FilterOperator.IS_CONTAINS:
                    return value.includes(filterValue);
                case exports.FilterOperator.IS_CONTAINS_IGNORE_CASE:
                    return value.toLowerCase().includes(filterValue.toLowerCase());
                case exports.FilterOperator.NOT_CONTAINS:
                    return !value.includes(filterValue);
                case exports.FilterOperator.NOT_CONTAINS_IGNORE_CASE:
                    return !value.toLowerCase().includes(filterValue.toLowerCase());
                case exports.FilterOperator.IS_STARTS_WITH:
                    return value.startsWith(filterValue);
                case exports.FilterOperator.IS_STARTS_WITH_IGNORE_CASE:
                    return value.toLowerCase().startsWith(filterValue.toLowerCase());
                case exports.FilterOperator.NOT_STARTS_WITH:
                    return !value.startsWith(filterValue);
                case exports.FilterOperator.NOT_STARTS_WITH_IGNORE_CASE:
                    return !value.toLowerCase().startsWith(filterValue.toLowerCase());
                case exports.FilterOperator.IS_ENDS_WITH:
                    return value.endsWith(filterValue);
                case exports.FilterOperator.IS_ENDS_WITH_IGNORE_CASE:
                    return value.toLowerCase().endsWith(filterValue.toLowerCase());
                case exports.FilterOperator.NOT_ENDS_WITH:
                    return !value.endsWith(filterValue);
                case exports.FilterOperator.NOT_ENDS_WITH_IGNORE_CASE:
                    return !value.toLowerCase().endsWith(filterValue.toLowerCase());
                case exports.FilterOperator.IS_REGEX_MATCH:
                    return new RegExp(filterValue).test(value);
                case exports.FilterOperator.NOT_REGEX_MATCH:
                    return !new RegExp(filterValue).test(value);
                case exports.FilterOperator.LESS_THAN:
                    return Number(value) < Number(filterValue);
                case exports.FilterOperator.LESS_THAN_OR_EQUALS:
                    return Number(value) <= Number(filterValue);
                case exports.FilterOperator.GREATER_THAN:
                    return Number(value) > Number(filterValue);
                case exports.FilterOperator.GREATER_THAN_OR_EQUALS:
                    return Number(value) >= Number(filterValue);
                default:
                    return false;
            }
        }
        static isNegationOperator(operator) {
            return (operator === exports.FilterOperator.NOT_EQUALS ||
                operator === exports.FilterOperator.NOT_EQUALS_IGNORE_CASE ||
                operator === exports.FilterOperator.NOT_CONTAINS ||
                operator === exports.FilterOperator.NOT_CONTAINS_IGNORE_CASE ||
                operator === exports.FilterOperator.NOT_STARTS_WITH ||
                operator === exports.FilterOperator.NOT_STARTS_WITH_IGNORE_CASE ||
                operator === exports.FilterOperator.NOT_ENDS_WITH ||
                operator === exports.FilterOperator.NOT_ENDS_WITH_IGNORE_CASE ||
                operator === exports.FilterOperator.NOT_REGEX_MATCH);
        }
    }

    /**
     * Coordinates native event listeners and manages their lifecycle.
     * Mirrors Android EventCoordinator — adapted for browser environment.
     */
    class EventCoordinator {
        constructor(eventService, collectors, payload, screenActivityCollector, securityCollector) {
            this.listeners = [];
            this.userDefinedValuesProvider = null;
            this.stopped = true;
            this.nativeEventListener = null;
            // RISK_ALERT auto-trigger state
            this.riskAlertIntervalId = null;
            this.lastRiskAlertTs = 0;
            this.eventService = eventService;
            this.collectors = collectors;
            this.payload = payload;
            this.screenActivityCollector = screenActivityCollector;
            this.securityCollector = securityCollector;
        }
        setUserDefinedValuesProvider(provider) {
            this.userDefinedValuesProvider = provider;
        }
        start() {
            Logger.debug("Starting EventCoordinator");
            this.stopped = false;
            this.initializeListeners();
            this.startRiskAlertEvaluator();
            // Trigger initial PAGE_VIEW event
            this.triggerNativeEvent(exports.TrackerEventType.PAGE_VIEW);
            Logger.debug("EventCoordinator started with", this.listeners.length, "listeners");
        }
        stop() {
            Logger.debug("Stopping EventCoordinator");
            this.stopped = true;
            if (this.riskAlertIntervalId !== null) {
                clearInterval(this.riskAlertIntervalId);
                this.riskAlertIntervalId = null;
            }
            for (const listener of this.listeners) {
                try {
                    listener.stop();
                }
                catch (error) {
                    Logger.error("Error stopping listener:", error);
                }
            }
            this.listeners = [];
            this.nativeEventListener = null;
            Logger.debug("EventCoordinator stopped");
        }
        setNativeEventListener(listener) {
            this.nativeEventListener = listener;
        }
        collectAllVariablesNow() {
            const allVariables = new Map();
            // Get variables from payload
            const payloadVars = this.payload.getCollectedVariables();
            for (const [k, v] of payloadVars) {
                allVariables.set(k, v);
            }
            // Collect from all data collectors
            this.collectFromAllCollectors(allVariables);
            return allVariables;
        }
        triggerNativeEvent(type) {
            if (this.stopped) {
                Logger.warn("Ignoring native event (coordinator stopped):", type);
                return;
            }
            try {
                this.triggerEventInternal(type, null, null);
            }
            catch (error) {
                Logger.error("Error in triggerNativeEvent:", error);
            }
        }
        triggerCustomEvent(eventName, args) {
            try {
                this.triggerEventInternal(exports.TrackerEventType.CUSTOM, eventName, args);
            }
            catch (error) {
                Logger.error("Error in triggerCustomEvent:", error);
            }
        }
        // === Internal ===
        triggerEventInternal(eventType, customEventName, customArguments) {
            const rootConfig = this.payload.getTrackerConfig();
            const isCustomEvent = eventType === exports.TrackerEventType.CUSTOM;
            if (!rootConfig) {
                Logger.error("Config not initialized, cannot trigger event");
                return;
            }
            Logger.debug(isCustomEvent ? `Custom event triggered: ${customEventName}` : `Native event triggered: ${eventType}`);
            // Register page view for risk metrics
            if (eventType === exports.TrackerEventType.PAGE_VIEW) {
                this.securityCollector.registerPageView();
                this.screenActivityCollector.resetTimer();
                this.screenActivityCollector.startTimer();
            }
            // Collect all variables
            const allConfigVariables = this.collectAllConfigVariables();
            // Inject user-defined values into CUSTOM_TRIGGER_ARGS for all events
            const userValues = this.userDefinedValuesProvider?.() ?? {};
            if (isCustomEvent && customEventName) {
                // Merge: custom arguments take precedence, then user-defined values
                const merged = { ...userValues, ...customArguments };
                if (Object.keys(merged).length > 0) {
                    allConfigVariables.set(exports.TrackerVariableType.CUSTOM_TRIGGER_ARGS, merged);
                }
            }
            else if (Object.keys(userValues).length > 0) {
                allConfigVariables.set(exports.TrackerVariableType.CUSTOM_TRIGGER_ARGS, userValues);
            }
            // Process each config
            for (const config of rootConfig.configs) {
                try {
                    const triggerConfig = this.findMatchingTrigger(config, eventType, customEventName);
                    if (!triggerConfig)
                        continue;
                    // Set current trigger name
                    allConfigVariables.set(exports.TrackerVariableType.CURRENT_TRIGGER_NAME, triggerConfig.name);
                    // Check trigger filters
                    if (!this.passesTriggerFilters(config, triggerConfig, allConfigVariables)) {
                        Logger.debug("Trigger", triggerConfig.name, "blocked by filters");
                        continue;
                    }
                    // Resolve screen activity variables (URL, Element, Cookie, JS)
                    const screenVars = this.screenActivityCollector.resolveConfigVariables(config);
                    for (const [k, v] of screenVars) {
                        allConfigVariables.set(k, v);
                    }
                    // Map variables to backend fields
                    const variables = BaseDataCollector.mapVariablesToBackend(config, allConfigVariables);
                    // Create and enqueue event
                    const deviceId = this.payload.getDeviceId();
                    const event = {
                        name: config.event.name,
                        key: deviceId,
                        variables,
                    };
                    this.eventService.enqueueEvent(event);
                    Logger.debug("Event enqueued:", config.event.name);
                }
                catch (error) {
                    Logger.error("Error processing config for event:", error);
                }
            }
            // Notify external listener
            if (this.nativeEventListener) {
                try {
                    const eventData = {
                        type: eventType,
                        args: Object.fromEntries(allConfigVariables),
                        timestamp: Date.now(),
                    };
                    this.nativeEventListener.onNativeEvent(eventData);
                }
                catch (error) {
                    Logger.error("Error notifying native event listener:", error);
                }
            }
        }
        collectAllConfigVariables() {
            const variables = new Map();
            // Get payload variables
            const payloadVars = this.payload.getCollectedVariables();
            for (const [k, v] of payloadVars) {
                variables.set(k, v);
            }
            // Collect from all data collectors
            this.collectFromAllCollectors(variables);
            return variables;
        }
        collectFromAllCollectors(target) {
            for (const collector of this.collectors) {
                try {
                    const collectedData = collector.collect();
                    for (const [k, v] of collectedData) {
                        target.set(k, v);
                    }
                }
                catch (error) {
                    Logger.error("Error collecting from collector:", error);
                }
            }
        }
        findMatchingTrigger(config, eventType, customEventName) {
            if (eventType === exports.TrackerEventType.CUSTOM && customEventName) {
                return config.triggers.find((t) => t.type === exports.TrackerEventType.CUSTOM && t.name === customEventName) ?? null;
            }
            return config.triggers.find((t) => t.type === eventType) ?? null;
        }
        passesTriggerFilters(config, trigger, variables) {
            if (!trigger.filters || trigger.filters.length === 0)
                return true;
            // Build a string-keyed map for filter matching
            const filterData = new Map();
            for (const varConfig of config.variables) {
                const type = varConfig.type;
                const value = variables.get(type);
                if (value !== undefined) {
                    filterData.set(varConfig.name, value);
                }
            }
            return FilterEngine.passesTriggerFilters(trigger.filters, filterData);
        }
        initializeListeners() {
            const config = this.payload.getTrackerConfig();
            if (!config) {
                Logger.error("Config not initialized, cannot initialize listeners");
                return;
            }
            const triggerFn = (type) => this.triggerNativeEvent(type);
            // Register all web listeners
            this.listeners.push(new PageViewListener(triggerFn));
            this.listeners.push(new ClickListener(triggerFn));
            this.listeners.push(new ScrollListener(triggerFn));
            this.listeners.push(new VisibilityListener(triggerFn));
            this.listeners.push(new IdleListener(triggerFn));
            this.listeners.push(new FormInteractionListener(triggerFn));
            this.listeners.push(new CopyPasteListener(triggerFn));
            // Start only listeners whose event types have triggers in config
            for (const listener of this.listeners) {
                const hasTriggers = listener
                    .getTypes()
                    .some((type) => config.configs.some((c) => c.triggers.some((t) => t.type === type)));
                const hasVariables = listener
                    .getVariableTypes()
                    .some((varType) => config.configs.some((c) => Object.values(c.event.variableMappings).includes(varType)));
                if (!hasTriggers && !hasVariables) {
                    Logger.debug("Skipping listener (not configured):", listener.constructor.name);
                    continue;
                }
                try {
                    listener.start();
                    Logger.debug("Started listener:", listener.constructor.name);
                }
                catch (error) {
                    Logger.error("Failed to start listener:", listener.constructor.name, error);
                }
            }
        }
        startRiskAlertEvaluator() {
            const config = this.payload.getTrackerConfig();
            if (!config)
                return;
            // Only run if RISK_ALERT triggers exist in config
            const hasRiskAlertTrigger = config.configs.some((c) => c.triggers.some((t) => t.type === exports.TrackerEventType.RISK_ALERT));
            if (!hasRiskAlertTrigger)
                return;
            this.riskAlertIntervalId = setInterval(() => {
                this.evaluateRiskAlert();
            }, 30000);
        }
        evaluateRiskAlert() {
            if (this.stopped)
                return;
            // 60s cooldown
            if (Date.now() - this.lastRiskAlertTs < 60000)
                return;
            const vars = this.collectAllConfigVariables();
            const biometricScore = vars.get(exports.TrackerVariableType.BEHAVIORAL_BIOMETRIC_SCORE) ?? 0;
            const remoteAccessScore = vars.get(exports.TrackerVariableType.REMOTE_ACCESS_SCORE) ?? 0;
            const combinedScore = biometricScore * 0.6 + remoteAccessScore * 0.4;
            if (combinedScore > 0.6) {
                this.lastRiskAlertTs = Date.now();
                Logger.debug("RISK_ALERT triggered (combined score:", combinedScore, ")");
                this.triggerNativeEvent(exports.TrackerEventType.RISK_ALERT);
            }
        }
    }

    /**
     * Core engine that coordinates all tracker functionality.
     * Mirrors Android TrackerEngine.
     */
    class TrackerEngine {
        constructor(eventService, eventCoordinator, networkService, payload, scheduler, deviceCollector, networkCollector, securityCollector, behavioralCollector, screenActivityCollector) {
            this.userDefinedValues = {};
            this.isConfigLoaded = false;
            this.eventService = eventService;
            this.eventCoordinator = eventCoordinator;
            this.networkService = networkService;
            this.payload = payload;
            this.scheduler = scheduler;
            this.deviceCollector = deviceCollector;
            this.networkCollector = networkCollector;
            this.securityCollector = securityCollector;
            this.behavioralCollector = behavioralCollector;
            this.screenActivityCollector = screenActivityCollector;
            // Wire user-defined values provider to EventCoordinator
            this.eventCoordinator.setUserDefinedValuesProvider(() => ({ ...this.userDefinedValues }));
        }
        async initialize(callback) {
            Logger.debug("Initializing TrackerEngine");
            try {
                const config = await this.networkService.fetchConfiguration();
                if (!config) {
                    callback.onComplete(false, "Config fetch returned null");
                    return;
                }
                this.payload.setTrackerConfig(config);
                this.isConfigLoaded = true;
                // Initialize collectors that need async setup
                await Promise.all([this.deviceCollector.initFingerprint(), this.networkCollector.fetchIp()]);
                // Initialize security collector with context
                const configuration = this.payload.getConfiguration();
                const deviceId = this.payload.getDeviceId();
                this.securityCollector.init(configuration?.tenant ?? "", deviceId);
                // Initialize behavioral collector
                this.behavioralCollector.init();
                // Start event coordinator (manages all listeners)
                this.eventCoordinator.start();
                // Start event service (queue processor)
                this.eventService.start();
                // Start periodic scheduler
                this.scheduler.start();
                // Start screen activity timer
                this.screenActivityCollector.startTimer();
                Logger.debug("TrackerEngine initialized successfully");
                callback.onComplete(true, "Tracker initialized successfully");
            }
            catch (error) {
                Logger.error("Failed to initialize:", error);
                const message = error instanceof Error ? error.message : String(error);
                callback.onComplete(false, `Failed to initialize: ${message}`);
            }
        }
        triggerCustomEvent(eventName, args) {
            if (!this.isConfigLoaded) {
                Logger.error("Config not loaded, cannot trigger custom event");
                return;
            }
            this.eventCoordinator.triggerCustomEvent(eventName, args);
        }
        async triggerCustomEventSync(eventName, args, callback) {
            if (!this.isConfigLoaded) {
                callback.onError(new Error("Config not loaded"));
                return;
            }
            const mergedArgs = { ...this.userDefinedValues, ...args };
            const config = this.payload.getTrackerConfig();
            if (!config) {
                callback.onError(new Error("Config not available"));
                return;
            }
            const allConfigVariables = this.eventCoordinator.collectAllVariablesNow();
            allConfigVariables.set(exports.TrackerVariableType.CUSTOM_TRIGGER_ARGS, mergedArgs);
            for (const configItem of config.configs) {
                try {
                    const trigger = configItem.triggers.find((t) => t.type === exports.TrackerEventType.CUSTOM && t.name === eventName);
                    if (!trigger)
                        continue;
                    allConfigVariables.set(exports.TrackerVariableType.CURRENT_TRIGGER_NAME, trigger.name);
                    // Resolve screen variables
                    const screenVars = this.screenActivityCollector.resolveConfigVariables(configItem);
                    for (const [k, v] of screenVars) {
                        allConfigVariables.set(k, v);
                    }
                    const variables = BaseDataCollector.mapVariablesToBackend(configItem, allConfigVariables);
                    const deviceId = this.payload.getDeviceId();
                    const event = {
                        name: configItem.event.name,
                        key: deviceId,
                        variables,
                    };
                    await this.eventService.sendEventSync(event, callback);
                }
                catch (error) {
                    Logger.error("Error creating event for config:", error);
                    callback.onError(error instanceof Error ? error : new Error(String(error)));
                }
            }
        }
        setUserDefinedValue(key, value) {
            if (value === null || value === undefined) {
                this.userDefinedValues[key] = undefined;
            }
            else {
                this.userDefinedValues[key] = value;
            }
        }
        getUserDefinedValues() {
            return { ...this.userDefinedValues };
        }
        getAllVariables() {
            return this.eventCoordinator.collectAllVariablesNow();
        }
        getConfiguration() {
            return this.payload.getTrackerConfig();
        }
        triggerNativeEvent(eventType) {
            this.eventCoordinator.triggerNativeEvent(eventType);
        }
        setNativeEventListener(listener) {
            this.eventCoordinator.setNativeEventListener(listener);
        }
        terminate() {
            Logger.debug("Terminating TrackerEngine");
            this.isConfigLoaded = false;
            try {
                this.eventCoordinator.stop();
            }
            catch (e) {
                Logger.error("Error stopping coordinator:", e);
            }
            try {
                this.eventService.stop();
            }
            catch (e) {
                Logger.error("Error stopping event service:", e);
            }
            try {
                this.scheduler.stop();
            }
            catch (e) {
                Logger.error("Error stopping scheduler:", e);
            }
            try {
                this.securityCollector.destroy();
            }
            catch (e) {
                Logger.error("Error destroying security:", e);
            }
            try {
                this.behavioralCollector.destroy();
            }
            catch (e) {
                Logger.error("Error destroying behavioral:", e);
            }
            try {
                this.screenActivityCollector.destroy();
            }
            catch (e) {
                Logger.error("Error destroying screen:", e);
            }
            // Clear user values
            Object.keys(this.userDefinedValues).forEach((key) => {
                this.userDefinedValues[key] = undefined;
            });
            Logger.debug("TrackerEngine terminated");
        }
    }

    /**
     * Periodic scheduler for background data collection tasks.
     * Mirrors Android PeriodicDataScheduler.
     */
    class PeriodicDataScheduler {
        constructor(task, intervalMs = 5000) {
            this.task = task;
            this.intervalMs = intervalMs;
            this.intervalId = null;
        }
        start() {
            if (this.intervalId !== null)
                return;
            this.intervalId = setInterval(() => {
                try {
                    this.task();
                }
                catch {
                    // Swallow errors — scheduler must not crash
                }
            }, this.intervalMs);
        }
        stop() {
            if (this.intervalId !== null) {
                clearInterval(this.intervalId);
                this.intervalId = null;
            }
        }
        isRunning() {
            return this.intervalId !== null;
        }
    }

    /**
     * Default network service implementation using Fetch API.
     * Mirrors Android DefaultNetworkService.
     */
    class DefaultNetworkService {
        constructor(configuration) {
            this.configuration = configuration;
        }
        async fetchConfiguration() {
            const url = `${this.configuration.apiUrl}/sentinel/v1`;
            Logger.debug("Fetching configuration from:", url);
            const response = await HttpClient.get(url, {
                headers: {
                    "api-key": this.configuration.apiKey,
                    tenant: this.configuration.tenant,
                    platform: "web",
                },
                timeout: this.configuration.timeout,
            });
            Logger.debug("Received configuration:", JSON.stringify(response, null, 2));
            // Filter config: prefer WEB platform, fall back to first available
            const webConfig = response.configs.find((c) => c.platform === "WEB");
            const config = {
                ...response,
                configs: webConfig ? [webConfig] : response.configs,
            };
            return config;
        }
        async sendEvent(event) {
            // POST to same endpoint as config fetch (mirrors Android SDK behavior)
            const url = `${this.configuration.apiUrl}/sentinel/v1`;
            Logger.debug("Sending event:", event.name);
            // Strip internal-only fields before sending
            const { retryCount: _, ...payload } = event;
            return HttpClient.post(url, payload, {
                headers: {
                    "api-key": this.configuration.apiKey,
                    tenant: this.configuration.tenant,
                    platform: "web",
                },
                timeout: this.configuration.timeout,
            });
        }
        isNetworkAvailable() {
            return navigator.onLine;
        }
    }

    /**
     * Service responsible for event queuing and periodic processing.
     * Mirrors Android EventService.
     */
    class EventService {
        constructor(eventStorage, networkService, configuration) {
            this.scheduler = null;
            this.consecutiveFailures = 0;
            this.eventStorage = eventStorage;
            this.networkService = networkService;
            this.configuration = configuration;
        }
        start() {
            Logger.debug("Starting EventService");
            this.scheduler = new PeriodicDataScheduler(() => this.processQueue(), this.configuration.queueProcessInterval ?? 5000);
            this.scheduler.start();
        }
        stop() {
            Logger.debug("Stopping EventService");
            this.scheduler?.stop();
            this.scheduler = null;
        }
        enqueueEvent(event) {
            Logger.debug("Enqueuing event:", event.name);
            this.eventStorage.saveEvent(event);
        }
        async sendEventSync(event, callback) {
            try {
                Logger.debug("Sending event sync:", event.name);
                const result = await this.networkService.sendEvent(event);
                if (result) {
                    callback.onSuccess(result);
                }
            }
            catch (error) {
                Logger.error("Failed to send event:", event.name, error);
                callback.onError(error instanceof Error ? error : new Error(String(error)));
            }
        }
        async processQueue() {
            try {
                if (!this.networkService.isNetworkAvailable()) {
                    Logger.debug("Network not available, skipping queue processing");
                    return;
                }
                // Stop retrying after too many consecutive failures
                if (this.consecutiveFailures >= EventService.MAX_CONSECUTIVE_FAILURES) {
                    Logger.debug("Too many consecutive failures, pausing queue processing");
                    return;
                }
                const pendingEvents = this.eventStorage.getPendingEvents();
                if (pendingEvents.length === 0)
                    return;
                Logger.debug("Processing queue:", pendingEvents.length, "events");
                for (const event of pendingEvents) {
                    try {
                        await this.networkService.sendEvent(event);
                        Logger.debug("Event sent from queue:", event.name);
                        this.eventStorage.removeEvent(event);
                        this.consecutiveFailures = 0;
                    }
                    catch {
                        this.consecutiveFailures++;
                        // Increment retry count and drop events that exceeded max retries
                        event.retryCount = (event.retryCount ?? 0) + 1;
                        if (event.retryCount >= EventService.MAX_EVENT_RETRIES) {
                            Logger.warn("Dropping event after max retries:", event.name);
                            this.eventStorage.removeEvent(event);
                        }
                        else if (this.consecutiveFailures === 1) {
                            // Only log the first failure, not every retry
                            Logger.warn("Failed to send event, will retry:", event.name);
                        }
                        // Stop processing remaining events on failure
                        if (this.consecutiveFailures >= EventService.MAX_CONSECUTIVE_FAILURES) {
                            Logger.warn("Queue paused after", EventService.MAX_CONSECUTIVE_FAILURES, "consecutive failures");
                            return;
                        }
                    }
                }
            }
            catch (error) {
                Logger.error("Critical error in processQueue:", error);
            }
        }
        /** Reset failure counter (e.g. after network recovery). */
        resetFailureCount() {
            this.consecutiveFailures = 0;
        }
    }
    EventService.MAX_CONSECUTIVE_FAILURES = 5;
    EventService.MAX_EVENT_RETRIES = 3;

    /**
     * In-memory event storage with queue semantics.
     * Mirrors Android InMemoryEventStorage.
     */
    class InMemoryEventStorage {
        constructor() {
            this.events = [];
        }
        saveEvent(event) {
            this.events.push(event);
        }
        getPendingEvents() {
            return [...this.events];
        }
        removeEvent(event) {
            this.events = this.events.filter((e) => e.key !== event.key);
        }
        clear() {
            this.events = [];
        }
    }

    /**
     * Thread-safe (single-threaded in JS) shared state container.
     * Mirrors Android DataPayload — holds config and collected variables.
     */
    class DataPayload {
        constructor() {
            this.trackerConfig = null;
            this.configuration = null;
            this.collectedVariables = new Map();
            this.userDefinedValues = new Map();
        }
        // --- TrackerConfig (backend) ---
        setTrackerConfig(config) {
            this.trackerConfig = config;
        }
        getTrackerConfig() {
            return this.trackerConfig;
        }
        // --- TrackerConfiguration (SDK init) ---
        setConfiguration(config) {
            this.configuration = config;
        }
        getConfiguration() {
            return this.configuration;
        }
        // --- Collected variables ---
        setVariable(type, value) {
            this.collectedVariables.set(type, value);
        }
        getVariable(type) {
            return this.collectedVariables.get(type);
        }
        getAllVariables() {
            return new Map(this.collectedVariables);
        }
        getCollectedVariables() {
            return new Map(this.collectedVariables);
        }
        updateVariables(variables) {
            for (const [key, value] of variables) {
                this.collectedVariables.set(key, value);
            }
        }
        // --- User-defined values ---
        setUserDefinedValue(key, value) {
            this.userDefinedValues.set(key, value);
        }
        getUserDefinedValue(key) {
            return this.userDefinedValues.get(key);
        }
        getUserDefinedValues() {
            return Object.fromEntries(this.userDefinedValues);
        }
        // --- Device ID ---
        getDeviceId() {
            try {
                let id = localStorage.getItem("formica_device_id");
                if (!id) {
                    id = crypto.randomUUID();
                    localStorage.setItem("formica_device_id", id);
                }
                return id;
            }
            catch {
                return `anon_${Date.now()}`;
            }
        }
        // --- Cleanup ---
        clear() {
            this.trackerConfig = null;
            this.collectedVariables.clear();
            this.userDefinedValues.clear();
        }
    }

    /**
     * DI module that wires up all SDK components.
     * Mirrors Android TrackerModule + DaggerTrackerComponent.
     */
    class TrackerModule {
        constructor(configuration) {
            this.configuration = configuration;
            this.build();
        }
        build() {
            // 1. Data payload (shared state)
            this.payload = new DataPayload();
            this.payload.setConfiguration(this.configuration);
            // 2. Collectors
            this.deviceCollector = new DeviceCollector();
            this.networkCollector = new NetworkCollector();
            this.securityCollector = new SecurityCollector();
            this.behavioralCollector = new BehavioralCollector();
            this.screenActivityCollector = new ScreenActivityCollector();
            const collectors = [
                this.deviceCollector,
                this.networkCollector,
                this.securityCollector,
                this.behavioralCollector,
                this.screenActivityCollector,
            ];
            // 3. Network service
            this.networkService = new DefaultNetworkService(this.configuration);
            // 4. Event storage & service
            this.eventStorage = new InMemoryEventStorage();
            this.eventService = new EventService(this.eventStorage, this.networkService, this.configuration);
            // 5. Event coordinator
            this.eventCoordinator = new EventCoordinator(this.eventService, collectors, this.payload, this.screenActivityCollector, this.securityCollector);
            // 6. Periodic scheduler (security + behavioral rolling window reset)
            this.scheduler = new PeriodicDataScheduler(() => {
                this.securityCollector.resetRollingWindow();
                this.behavioralCollector.resetRollingWindow();
            }, 60000);
            // 7. Engine
            this.engine = new TrackerEngine(this.eventService, this.eventCoordinator, this.networkService, this.payload, this.scheduler, this.deviceCollector, this.networkCollector, this.securityCollector, this.behavioralCollector, this.screenActivityCollector);
        }
        getTrackerEngine() {
            return this.engine;
        }
    }

    /**
     * Main entry point for the Tracker Web SDK.
     * Mirrors Android TrackerSDK — Builder pattern, instance-based API.
     *
     * @example
     * ```ts
     * const tracker = new TrackerSDK.Builder()
     *   .setConfiguration({
     *     tenant: 'my-tenant',
     *     apiUrl: 'https://api.example.com',
     *     apiKey: 'my-api-key',
     *   })
     *   .build();
     *
     * tracker.initialize({
     *   onComplete(success, message) {
     *     if (success) console.log('SDK initialized');
     *   },
     * });
     * ```
     */
    class TrackerSDK {
        constructor(configuration) {
            this.isInitialized = false;
            Logger.setEnabled(configuration.debug ?? false);
            const module = new TrackerModule(configuration);
            this.engine = module.getTrackerEngine();
        }
        /**
         * Initializes the Tracker SDK. Fetches configuration from backend,
         * sets up collectors, and starts event listeners.
         */
        async initialize(callback) {
            try {
                if (this.isInitialized) {
                    callback.onComplete(true, "Tracker SDK already initialized");
                    return;
                }
                await this.engine.initialize({
                    onComplete: (success, message) => {
                        if (success) {
                            this.isInitialized = true;
                        }
                        callback.onComplete(success, message);
                    },
                });
            }
            catch (error) {
                try {
                    const message = error instanceof Error ? error.message : String(error);
                    callback.onComplete(false, `Initialization failed: ${message}`);
                }
                catch {
                    // Callback itself threw, nothing we can do
                }
            }
        }
        /** Checks if the SDK is initialized. */
        getIsInitialized() {
            return this.isInitialized;
        }
        /**
         * Triggers a custom event asynchronously. The event will be queued
         * and sent when network is available.
         */
        triggerCustomEvent(eventName, args = {}) {
            if (!this.isInitialized)
                return;
            try {
                this.engine.triggerCustomEvent(eventName, args);
            }
            catch {
                // Never crash on custom events
            }
        }
        /**
         * Triggers a custom event synchronously with a callback.
         * The callback will be invoked with the server response.
         */
        async triggerCustomEventSync(eventName, args, callback) {
            if (!this.isInitialized) {
                callback.onError(new Error("Tracker SDK not initialized"));
                return;
            }
            try {
                await this.engine.triggerCustomEventSync(eventName, args, callback);
            }
            catch (error) {
                try {
                    callback.onError(error instanceof Error ? error : new Error(String(error)));
                }
                catch {
                    // Callback threw
                }
            }
        }
        /** Sets a user-defined value that will be included in all events. */
        setUserDefinedValue(key, value) {
            if (!this.isInitialized)
                return;
            this.engine.setUserDefinedValue(key, value);
        }
        /** Gets all user-defined values. */
        getUserDefinedValues() {
            if (!this.isInitialized)
                return null;
            return this.engine.getUserDefinedValues();
        }
        /** Gets all currently collected variables from all collectors. */
        getAllVariables() {
            if (!this.isInitialized)
                return null;
            return this.engine.getAllVariables();
        }
        /** Gets the current tracker configuration from backend. */
        getConfiguration() {
            if (!this.isInitialized)
                return null;
            return this.engine.getConfiguration();
        }
        /** Triggers a native event programmatically (for testing/debugging). */
        triggerNativeEvent(eventType) {
            if (!this.isInitialized)
                return;
            this.engine.triggerNativeEvent(eventType);
        }
        /** Sets a listener for native tracker events. */
        setNativeEventListener(listener) {
            if (!this.isInitialized)
                return;
            this.engine.setNativeEventListener(listener);
        }
        /**
         * Terminates the SDK and releases all resources.
         * After calling this, you must re-create and re-initialize to use it again.
         */
        terminate() {
            if (!this.isInitialized)
                return;
            try {
                this.engine.terminate();
            }
            catch {
                // Log but continue
            }
            finally {
                this.isInitialized = false;
            }
        }
    }
    /** Builder class for creating TrackerSDK instances. */
    TrackerSDK.Builder = class TrackerSDKBuilder {
        constructor() {
            this.configuration = null;
        }
        setConfiguration(configuration) {
            this.configuration = configuration;
            return this;
        }
        build() {
            if (!this.configuration) {
                throw new Error("Configuration must be set before building TrackerSDK");
            }
            return new TrackerSDK(this.configuration);
        }
    };

    exports.TrackerSDK = TrackerSDK;

    return exports;

})({}, FingerprintJS, Bowser);