UNPKG

@trlogic/tracker-web

Version:
3,018 lines 116 kB
'use strict';

var FingerprintJS = require('@fingerprintjs/fingerprintjs');
var Bowser = require('bowser');

/**
 * 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;
//# sourceMappingURL=bundle.js.map