UNPKG

@trlogic/tracker-web

Version:
1,372 lines 57.2 kB
define(['bowser', '@fingerprintjs/fingerprintjs'], (function (Bowser, FingerprintJS) { 'use strict';

    var TrackerTriggerTypeMobile;
    (function (TrackerTriggerTypeMobile) {
        TrackerTriggerTypeMobile["APP_STATE_CHANGE"] = "APP_STATE_CHANGE";
        TrackerTriggerTypeMobile["CUSTOM"] = "CUSTOM";
        TrackerTriggerTypeMobile["APPLICATION_OPEN"] = "APPLICATION_OPEN";
        TrackerTriggerTypeMobile["NETWORK_CHANGE"] = "NETWORK_CHANGE";
        TrackerTriggerTypeMobile["APP_CALL_END"] = "APP_CALL_END";
        TrackerTriggerTypeMobile["APP_CALL_START"] = "APP_CALL_START";
        TrackerTriggerTypeMobile["APP_CALL_RING"] = "APP_CALL_RING";
        TrackerTriggerTypeMobile["APP_CALL_REJECT"] = "APP_CALL_REJECT";
        TrackerTriggerTypeMobile["SCREEN_RECORD_END"] = "SCREEN_RECORD_END";
        TrackerTriggerTypeMobile["SCREEN_RECORD_START"] = "SCREEN_RECORD_START";
    })(TrackerTriggerTypeMobile || (TrackerTriggerTypeMobile = {}));
    var TrackerTriggerTypeWeb;
    (function (TrackerTriggerTypeWeb) {
        TrackerTriggerTypeWeb["CLICK"] = "CLICK";
        TrackerTriggerTypeWeb["SCROLL"] = "SCROLL";
        TrackerTriggerTypeWeb["PAGE_VIEW"] = "PAGE_VIEW";
        TrackerTriggerTypeWeb["CUSTOM"] = "CUSTOM";
        TrackerTriggerTypeWeb["RISK_ALERT"] = "RISK_ALERT";
    })(TrackerTriggerTypeWeb || (TrackerTriggerTypeWeb = {}));

    var TrackerVariableWebType;
    (function (TrackerVariableWebType) {
        TrackerVariableWebType["URL"] = "URL";
        TrackerVariableWebType["ELEMENT"] = "ELEMENT";
        TrackerVariableWebType["COOKIE"] = "COOKIE";
        TrackerVariableWebType["JAVASCRIPT"] = "JAVASCRIPT";
        TrackerVariableWebType["EVENT"] = "EVENT";
        TrackerVariableWebType["IP_ADDRESS"] = "IP_ADDRESS";
        TrackerVariableWebType["DEVICE_FINGERPRINT"] = "DEVICE_FINGERPRINT";
        TrackerVariableWebType["SUSPICIOUS_ACTIVITY"] = "SUSPICIOUS_ACTIVITY";
        TrackerVariableWebType["SUSPICIOUS_BROWSER"] = "SUSPICIOUS_BROWSER";
        TrackerVariableWebType["LANGUAGE"] = "LANGUAGE";
        TrackerVariableWebType["TIMEZONE"] = "TIMEZONE";
        TrackerVariableWebType["OS"] = "OS";
        TrackerVariableWebType["BROWSER"] = "BROWSER";
        TrackerVariableWebType["CUSTOM"] = "CUSTOM";
        TrackerVariableWebType["SCREEN_RESOLUTION"] = "SCREEN_RESOLUTION";
        TrackerVariableWebType["USER_AGENT"] = "USER_AGENT";
        TrackerVariableWebType["REMOTE_ACCESS_SCORE"] = "REMOTE_ACCESS_SCORE";
        TrackerVariableWebType["REMOTE_ACCESS_FLAGS_JSON"] = "REMOTE_ACCESS_FLAGS_JSON";
        TrackerVariableWebType["ENVIRONMENT_CHANGE_FLAGS"] = "ENVIRONMENT_CHANGE_FLAGS";
        TrackerVariableWebType["ENVIRONMENT_CHANGE_SCORE"] = "ENVIRONMENT_CHANGE_SCORE";
        TrackerVariableWebType["NO_MOUSE_MOVE_DURATION_MS"] = "NO_MOUSE_MOVE_DURATION_MS";
        TrackerVariableWebType["CLICK_ONLY_PATTERN"] = "CLICK_ONLY_PATTERN";
        TrackerVariableWebType["FAST_PAGE_TRANSITION"] = "FAST_PAGE_TRANSITION";
        TrackerVariableWebType["FORM_FILL_ANOMALY"] = "FORM_FILL_ANOMALY";
        TrackerVariableWebType["FORM_FILL_DURATION_MS"] = "FORM_FILL_DURATION_MS";
        TrackerVariableWebType["NAVIGATION_VELOCITY"] = "NAVIGATION_VELOCITY";
        TrackerVariableWebType["INPUT_PASTE_RATIO"] = "INPUT_PASTE_RATIO";
        TrackerVariableWebType["HEADLESS_INDICATOR_COUNT"] = "HEADLESS_INDICATOR_COUNT";
    })(TrackerVariableWebType || (TrackerVariableWebType = {}));

    // Lightweight dev-only logger. Messages are suppressed in production builds.
    const isDev = (() => {
        var _a, _b;
        try {
            const g = (typeof globalThis !== "undefined" ? globalThis : {});
            const env = (_b = (_a = g.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV;
            return env !== "production";
        }
        catch {
            return true; // default to dev if uncertain
        }
    })();
    const logger = {
        debug: (...args) => {
            if (isDev)
                console.debug("[tracker]", ...args);
        },
        info: (...args) => {
            if (isDev)
                console.info("[tracker]", ...args);
        },
        warn: (...args) => {
            if (isDev)
                console.warn("[tracker]", ...args);
        },
        error: (...args) => {
            if (isDev)
                console.error("[tracker]", ...args);
        },
    };

    function monitorRemoteControlSuspicion(onSuspiciousDetected) {
        try {
            const flags = {
                unnaturalMouseMoves: false,
                bigClipboardPaste: false,
                lowFPSDetected: false,
                delayedClickDetected: false,
            };
            const previousFlags = { ...flags };
            function hasChanged() {
                return (flags.unnaturalMouseMoves !== previousFlags.unnaturalMouseMoves ||
                    flags.bigClipboardPaste !== previousFlags.bigClipboardPaste ||
                    flags.lowFPSDetected !== previousFlags.lowFPSDetected ||
                    flags.delayedClickDetected !== previousFlags.delayedClickDetected);
            }
            function triggerIfChanged() {
                try {
                    if (hasChanged()) {
                        previousFlags.unnaturalMouseMoves = flags.unnaturalMouseMoves;
                        previousFlags.bigClipboardPaste = flags.bigClipboardPaste;
                        previousFlags.lowFPSDetected = flags.lowFPSDetected;
                        previousFlags.delayedClickDetected = flags.delayedClickDetected;
                        onSuspiciousDetected({ ...flags });
                    }
                }
                catch (err) {
                    logger.debug("Error while triggering suspicious change callback", err);
                }
            }
            let lastX = null;
            let lastY = null;
            let unnaturalCount = 0;
            window.addEventListener("mousemove", (e) => {
                try {
                    if (lastX !== null && lastY !== null) {
                        const dx = Math.abs(e.clientX - lastX);
                        const dy = Math.abs(e.clientY - lastY);
                        if (dx > 100 || dy > 100 || (dx === 0 && dy === 0))
                            unnaturalCount++;
                        if (unnaturalCount > 5 && !flags.unnaturalMouseMoves) {
                            flags.unnaturalMouseMoves = true;
                            triggerIfChanged();
                        }
                    }
                    lastX = e.clientX;
                    lastY = e.clientY;
                }
                catch (err) {
                    logger.debug("Error in mousemove handler", err);
                }
            });
            window.addEventListener("paste", (e) => {
                var _a;
                try {
                    const clipboardEvent = e;
                    const text = ((_a = clipboardEvent.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text")) || "";
                    if (text.length > 5000 && !flags.bigClipboardPaste) {
                        flags.bigClipboardPaste = true;
                        triggerIfChanged();
                    }
                }
                catch (err) {
                    logger.debug("Error detecting large clipboard paste", err);
                }
            });
            document.addEventListener("click", () => {
                try {
                    const start = performance.now();
                    setTimeout(() => {
                        try {
                            const delay = performance.now() - start;
                            if (delay > 150 && !flags.delayedClickDetected) {
                                flags.delayedClickDetected = true;
                                triggerIfChanged();
                            }
                        }
                        catch (err) {
                            logger.debug("Error evaluating delayed click", err);
                        }
                    }, 0);
                }
                catch (err) {
                    logger.debug("Error scheduling delayed click evaluation", err);
                }
            });
            let lastFrame = performance.now();
            let lowFPSCount = 0;
            function monitorFPS() {
                try {
                    const now = performance.now();
                    const delta = now - lastFrame;
                    if (delta > 250)
                        lowFPSCount++;
                    if (lowFPSCount > 10 && !flags.lowFPSDetected) {
                        flags.lowFPSDetected = true;
                        triggerIfChanged();
                    }
                    lastFrame = now;
                }
                catch (err) {
                    logger.debug("Error in FPS monitor", err);
                }
                requestAnimationFrame(monitorFPS);
            }
            monitorFPS();
        }
        catch (err) {
            logger.debug("Error initializing remote control suspicion monitor", err);
        }
    }

    /**
     * Risk metrics collection & baseline management
     */
    const BASELINE_KEY_PREFIX = "formica_baseline_";
    class RiskMetricsService {
        constructor() {
            this.lastMouseMoveTs = Date.now();
            this.lastClickTs = 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;
        }
        /** register global listeners (idempotent) */
        initListeners() {
            const w = window;
            if (w.__riskMetricsListenersAttached)
                return;
            w.__riskMetricsListenersAttached = true;
            window.addEventListener("mousemove", () => {
                this.lastMouseMoveTs = Date.now();
                this.mouseMoveCounterWindow++;
            });
            document.addEventListener("click", () => {
                this.lastClickTs = Date.now();
                this.clickCounterWindow++;
            });
            document.addEventListener("paste", (e) => {
                var _a;
                const text = ((_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text")) || "";
                this.pasteCharCount += text.length;
            });
            document.addEventListener("keydown", (e) => {
                if (e.key.length === 1)
                    this.typedCharCount += 1;
            });
            // Basic form tracking
            document.addEventListener("focusin", (e) => {
                var _a;
                const form = (_a = e.target) === null || _a === void 0 ? void 0 : _a.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) => {
                const form = e.target;
                if (!form)
                    return;
                if (this.lastFormInteractionStart) {
                    this.lastFormDurationMs = Date.now() - this.lastFormInteractionStart;
                    this.formFillAnomaly = this.detectFormAnomaly(this.lastFormDurationMs, this.lastFormFieldCount);
                }
                this.lastFormInteractionStart = null;
            });
        }
        /** Called on each page view */
        registerPageView() {
            const now = Date.now();
            if (this.lastPageViewTs && now - this.lastPageViewTs < 1500) {
                this.fastPageTransition = true;
            }
            else {
                this.fastPageTransition = false;
            }
            this.lastPageViewTs = now;
            this.pageViewTimestamps.push(now);
            // keep last 5 minutes
            const cutoff = now - 5 * 60 * 1000;
            this.pageViewTimestamps = this.pageViewTimestamps.filter((t) => t >= cutoff);
        }
        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; // per 5 min -> count/5 gives per minute
            const inputPasteRatio = this.computePasteRatio();
            return {
                headlessIndicatorCount: 0, // will be filled externally if needed
                noMouseMoveDurationMs,
                clickOnlyPattern,
                fastPageTransition: this.fastPageTransition,
                navigationVelocity,
                formFillAnomaly: this.formFillAnomaly,
                formFillDurationMs: this.lastFormDurationMs,
                inputPasteRatio,
            };
        }
        computePasteRatio() {
            const total = this.pasteCharCount + this.typedCharCount;
            if (total === 0)
                return 0;
            return this.pasteCharCount / total;
        }
        resetRollingWindow() {
            this.mouseMoveCounterWindow = 0;
            this.clickCounterWindow = 0;
        }
        detectFormAnomaly(durationMs, fieldCount) {
            if (fieldCount === 0)
                return false;
            const adaptiveThreshold = Math.min(fieldCount * 120, 1500); // ms
            return durationMs < adaptiveThreshold;
        }
        // Baseline
        loadBaseline(tenant, deviceId) {
            try {
                const raw = localStorage.getItem(this.getBaselineKey(tenant, deviceId));
                if (!raw)
                    return null;
                return JSON.parse(raw);
            }
            catch {
                return null;
            }
        }
        ensureBaseline(tenant, deviceId, data) {
            const existing = this.loadBaseline(tenant, deviceId);
            if (existing)
                return existing;
            const baseline = { ...data, firstSeen: Date.now() };
            try {
                localStorage.setItem(this.getBaselineKey(tenant, deviceId), JSON.stringify(baseline));
            }
            catch (err) {
            }
            return baseline;
        }
        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);
        }
        calcRemoteAccessScore(flags, envMismatch, agg) {
            let score = 0;
            if (flags.unnaturalMouseMoves)
                score += 0.2;
            if (flags.bigClipboardPaste)
                score += 0.15;
            if (flags.lowFPSDetected)
                score += 0.15;
            if (flags.delayedClickDetected)
                score += 0.1;
            if (agg.clickOnlyPattern)
                score += 0.15;
            if (agg.headlessIndicatorCount > 0)
                score += Math.min(agg.headlessIndicatorCount * 0.05, 0.25);
            if (envMismatch)
                score += 0.2;
            return Math.min(1, score);
        }
        getBaselineKey(tenant, deviceId) {
            return `${BASELINE_KEY_PREFIX}${tenant}_${deviceId}`;
        }
    }
    const riskMetricsService = new RiskMetricsService();

    /**
     * Browser and system detection utilities using Bowser
     */
    class BrowserDetector {
        static getBrowser() {
            const browser = this.parser.getBrowser();
            return browser.name || null;
        }
        static getBrowserVersion() {
            const browser = this.parser.getBrowser();
            return browser.version || null;
        }
        static getOS() {
            const os = this.parser.getOS();
            return os.name || null;
        }
        static getOSVersion() {
            const os = this.parser.getOS();
            return os.version || null;
        }
        static getPlatform() {
            const platform = this.parser.getPlatform();
            return platform.type || null;
        }
        static getEngine() {
            const engine = this.parser.getEngine();
            return engine.name || null;
        }
        static getLanguage() {
            const nav = navigator;
            return navigator.language || nav.userLanguage || "";
        }
        static getTimezone() {
            try {
                return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
            }
            catch {
                return "";
            }
        }
        static isDesktop() {
            return this.parser.getPlatform().type === "desktop";
        }
        static isMobile() {
            return this.parser.getPlatform().type === "mobile";
        }
        static isTablet() {
            return this.parser.getPlatform().type === "tablet";
        }
        static isBot() {
            // Bowser doesn't have built-in bot detection, so we'll keep some basic checks
            const userAgent = navigator.userAgent.toLowerCase();
            const botPatterns = [
                "bot",
                "crawler",
                "spider",
                "crawling",
                "facebook",
                "google",
                "baidu",
                "bing",
                "msn",
                "duckduckbot",
                "teoma",
                "slurp",
                "yandex",
            ];
            return botPatterns.some((pattern) => userAgent.includes(pattern));
        }
        static async isSuspiciousBrowser() {
            try {
                // Check for webdriver
                if (navigator.webdriver)
                    return true;
                // Check for missing languages
                if (!navigator.languages || navigator.languages.length === 0)
                    return true;
                // Check for missing plugins (less reliable in modern browsers)
                if (!navigator.plugins || navigator.plugins.length === 0)
                    return true;
                // Check WebGL support
                const canvas = document.createElement("canvas");
                const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
                if (!gl)
                    return true;
                canvas.remove();
                // Check notification permissions
                if (navigator.permissions) {
                    try {
                        const result = await navigator.permissions.query({
                            name: "notifications",
                        });
                        if (Notification.permission === "denied" && result.state === "prompt") {
                            return true;
                        }
                    }
                    catch {
                        // Ignore permission errors
                    }
                }
                // Check hardware concurrency
                if (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 1)
                    return true;
                // Check touch support on mobile user agent
                if (this.isMobile() && !("ontouchstart" in window))
                    return true;
                // Check if it's a known bot
                if (this.isBot())
                    return true;
                return false;
            }
            catch {
                return true;
            }
        }
        /**
         * Get detailed browser information
         */
        static getDetailedInfo() {
            return {
                browser: {
                    name: this.getBrowser(),
                    version: this.getBrowserVersion(),
                },
                os: {
                    name: this.getOS(),
                    version: this.getOSVersion(),
                },
                platform: {
                    type: this.getPlatform(),
                    isDesktop: this.isDesktop(),
                    isMobile: this.isMobile(),
                    isTablet: this.isTablet(),
                },
                engine: this.getEngine(),
                language: this.getLanguage(),
                timezone: this.getTimezone(),
                isBot: this.isBot(),
            };
        }
    }
    BrowserDetector.parser = Bowser.getParser(window.navigator.userAgent);

    /**
     * Event mapping and payload building service
     */
    class EventBuilder {
        static buildEvent(eventSchema, trackerVariables) {
            var _a;
            const name = eventSchema.event.name;
            const variables = {};
            Object.entries(eventSchema.event.variableMappings).forEach(([key, value]) => {
                variables[key] = this.resolveMapping(value, trackerVariables);
            });
            return {
                name,
                key: String((_a = variables[eventSchema.event.keyMapping]) !== null && _a !== void 0 ? _a : ""),
                variables,
            };
        }
        static resolveMapping(mapping, trackerVariables) {
            const matches = this.findMatches(mapping, /{.*?}/g);
            // If no matches, check if mapping name matches any tracker variable
            if (matches.length === 0 && Object.prototype.hasOwnProperty.call(trackerVariables, mapping)) {
                return trackerVariables[mapping];
            }
            let resolvedMapping = mapping;
            matches.forEach((match) => {
                var _a;
                const variableName = match.substring(1, match.length - 1);
                const variableValue = String((_a = trackerVariables[variableName]) !== null && _a !== void 0 ? _a : "");
                resolvedMapping = resolvedMapping.replace(match, variableValue);
            });
            return resolvedMapping;
        }
        static findMatches(string, regex) {
            const matches = [];
            let regExpExecArray;
            // Reset regex lastIndex to avoid issues with global regex
            regex.lastIndex = 0;
            while ((regExpExecArray = regex.exec(string)) !== null) {
                matches.push(regExpExecArray[0]);
                // Prevent infinite loop with zero-length matches
                if (regExpExecArray[0].length === 0) {
                    regex.lastIndex++;
                }
            }
            return matches;
        }
    }

    /**
     * HTTP utility functions using fetch API
     */
    class HttpError extends Error {
        constructor(message, status, response) {
            super(message);
            this.status = status;
            this.response = response;
            this.name = "HttpError";
        }
    }
    class HttpClient {
        constructor(defaultHeaders = {}) {
            this.defaultHeaders = defaultHeaders;
        }
        async makeRequest(url, config = {}) {
            const { headers = {}, timeout, method = "GET", body } = config;
            const mergedHeaders = { ...this.defaultHeaders, ...headers };
            const controller = new AbortController();
            const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
            try {
                const fetchConfig = {
                    method,
                    headers: mergedHeaders,
                    signal: controller.signal,
                };
                if (body && method !== "GET") {
                    fetchConfig.body = typeof body === "string" ? body : JSON.stringify(body);
                    if (!mergedHeaders["Content-Type"]) {
                        mergedHeaders["Content-Type"] = "application/json";
                    }
                }
                const response = await fetch(url, fetchConfig);
                if (timeoutId) {
                    clearTimeout(timeoutId);
                }
                if (!response.ok) {
                    throw new HttpError(`HTTP Error: ${response.status} ${response.statusText}`, response.status, response);
                }
                // Handle empty responses
                const contentLength = response.headers.get("Content-Length");
                if (contentLength === "0") {
                    return {};
                }
                const contentType = response.headers.get("Content-Type");
                if (contentType === null || contentType === void 0 ? void 0 : contentType.includes("application/json")) {
                    return await response.json();
                }
                return (await response.text());
            }
            catch (error) {
                if (timeoutId) {
                    clearTimeout(timeoutId);
                }
                if (error instanceof Error && error.name === "AbortError") {
                    throw new HttpError("Request timeout", 408);
                }
                throw error;
            }
        }
        async get(url, config) {
            return this.makeRequest(url, { ...config, method: "GET" });
        }
        async post(url, data, config) {
            return this.makeRequest(url, { ...config, method: "POST", body: data });
        }
        async put(url, data, config) {
            return this.makeRequest(url, { ...config, method: "PUT", body: data });
        }
        async delete(url, config) {
            return this.makeRequest(url, { ...config, method: "DELETE" });
        }
    }

    /**
     * Event queue management service
     */
    class EventQueue {
        constructor(tenant, apiUrl) {
            this.queue = [];
            this.processingInterval = null;
            this.tenant = tenant;
            this.apiUrl = apiUrl;
            this.httpClient = new HttpClient();
        }
        add(payload) {
            this.queue.push(payload);
        }
        async sendSync(payload, timeoutMs) {
            return this.httpClient.post(this.apiUrl, payload, {
                headers: { tenant: this.tenant },
                timeout: timeoutMs,
            });
        }
        startProcessing() {
            if (this.processingInterval)
                return;
            this.processingInterval = window.setInterval(() => {
                this.processQueue();
            }, 3000);
        }
        stopProcessing() {
            if (this.processingInterval) {
                clearInterval(this.processingInterval);
                this.processingInterval = null;
            }
        }
        async processQueue() {
            if (!navigator.onLine || this.queue.length === 0)
                return;
            while (this.queue.length > 0 && navigator.onLine) {
                const event = this.queue.pop();
                if (!event)
                    break;
                try {
                    await this.httpClient.post(this.apiUrl, event, {
                        headers: { tenant: this.tenant },
                    });
                }
                catch (error) {
                    logger.error("Failed to send event:", error);
                    // Re-add the event to the queue for retry
                    this.queue.unshift(event);
                    break;
                }
            }
        }
        getQueueLength() {
            return this.queue.length;
        }
    }

    /**
     * Event validation service
     */
    class EventValidator {
        static validate(event, triggerSchema) {
            switch (triggerSchema.name) {
                case "click":
                    return this.validateClick(event, triggerSchema.option);
                case "scroll":
                    // TODO: Add horizontal and vertical flag checks
                    return true;
                default:
                    return true;
            }
        }
        static validateClick(event, clickOption) {
            if (typeof clickOption !== "undefined" && !clickOption.justLinks)
                return true;
            const target = event.target;
            if (!target)
                return false;
            return target.hasAttribute("href");
        }
    }

    /**
     * Device fingerprinting service
     */
    class FingerprintService {
        static async getDeviceFingerprint() {
            try {
                const fp = await FingerprintJS.load();
                const result = await fp.get();
                return result.visitorId;
            }
            catch (error) {
                console.error("Error generating device fingerprint:", error);
                return "";
            }
        }
    }

    /**
     * IP address detection service
     */
    class IpService {
        static async fetchIp() {
            try {
                const response = await this.httpClient.get("https://api.ipify.org?format=json");
                return response.ip;
            }
            catch (error) {
                console.error("Error fetching IP address:", error);
                return "unknown";
            }
        }
    }
    IpService.httpClient = new HttpClient();

    /**
     * Timer management service
     */
    class TimerService {
        constructor() {
            this.timerInstance = null;
            this.viewDuration = 0;
            this.INTERVAL_MS = 100;
        }
        start() {
            if (this.timerInstance)
                return;
            this.timerInstance = window.setInterval(() => {
                this.viewDuration += this.INTERVAL_MS;
            }, this.INTERVAL_MS);
        }
        stop() {
            if (this.timerInstance) {
                clearInterval(this.timerInstance);
                this.timerInstance = null;
            }
        }
        reset() {
            this.viewDuration = 0;
        }
        getViewDuration() {
            return this.viewDuration;
        }
        isRunning() {
            return this.timerInstance !== null;
        }
    }

    /**
     * Tracker configuration service
     */
    class TrackerConfigService {
        constructor() {
            this.httpClient = new HttpClient();
        }
        async getTrackers(serviceUrl, tenant, apiKey) {
            const apiUrl = `${serviceUrl}/sentinel/v1`;
            const config = await this.httpClient.get(apiUrl, {
                headers: {
                    tenant,
                    "api-key": apiKey,
                    "Content-Type": "application/json",
                    platform: "web",
                },
            });
            return {
                trackers: config.configs,
                apiUrl,
            };
        }
    }

    /**
     * Variable resolution service
     */
    class VariableResolver {
        constructor(ip, deviceFingerprint, suspiciousFlags, globalVariables) {
            this.ip = ip;
            this.deviceFingerprint = deviceFingerprint;
            this.suspiciousFlags = suspiciousFlags;
            this.globalVariables = globalVariables;
        }
        async resolve(trackerVariableSchema, mouseEvent) {
            switch (trackerVariableSchema.type) {
                case TrackerVariableWebType.URL:
                    return this.resolveUrlVariable(trackerVariableSchema);
                case TrackerVariableWebType.COOKIE:
                    return this.resolveCookieVariable(trackerVariableSchema);
                case TrackerVariableWebType.ELEMENT:
                    return this.resolveElementVariable(trackerVariableSchema);
                case TrackerVariableWebType.JAVASCRIPT:
                    return this.resolveJavascriptVariable(trackerVariableSchema);
                case TrackerVariableWebType.EVENT:
                    return this.resolveTriggerVariable(trackerVariableSchema, mouseEvent);
                case TrackerVariableWebType.IP_ADDRESS:
                    return this.resolveIpAddressVariable();
                case TrackerVariableWebType.DEVICE_FINGERPRINT:
                    return this.resolveDeviceFingerprint();
                case TrackerVariableWebType.SUSPICIOUS_ACTIVITY:
                    return this.resolveSuspiciousActivityVariable();
                case TrackerVariableWebType.SUSPICIOUS_BROWSER:
                    return BrowserDetector.isSuspiciousBrowser();
                case TrackerVariableWebType.LANGUAGE:
                    return BrowserDetector.getLanguage();
                case TrackerVariableWebType.TIMEZONE:
                    return BrowserDetector.getTimezone();
                case TrackerVariableWebType.OS:
                    return BrowserDetector.getOS();
                case TrackerVariableWebType.BROWSER:
                    return BrowserDetector.getBrowser();
                case TrackerVariableWebType.CUSTOM:
                    return this.resolveCustomVariable(trackerVariableSchema);
                case TrackerVariableWebType.SCREEN_RESOLUTION:
                    return `${window.screen.width}x${window.screen.height}`;
                case TrackerVariableWebType.USER_AGENT:
                    return navigator.userAgent;
                case TrackerVariableWebType.NO_MOUSE_MOVE_DURATION_MS:
                case TrackerVariableWebType.CLICK_ONLY_PATTERN:
                case TrackerVariableWebType.FAST_PAGE_TRANSITION:
                case TrackerVariableWebType.FORM_FILL_ANOMALY:
                case TrackerVariableWebType.FORM_FILL_DURATION_MS:
                case TrackerVariableWebType.NAVIGATION_VELOCITY:
                case TrackerVariableWebType.INPUT_PASTE_RATIO:
                case TrackerVariableWebType.REMOTE_ACCESS_SCORE:
                case TrackerVariableWebType.REMOTE_ACCESS_FLAGS_JSON:
                case TrackerVariableWebType.ENVIRONMENT_CHANGE_FLAGS:
                case TrackerVariableWebType.ENVIRONMENT_CHANGE_SCORE:
                case TrackerVariableWebType.HEADLESS_INDICATOR_COUNT:
                    return this.resolveRiskVariable(trackerVariableSchema.type);
                default:
                    return "";
            }
        }
        resolveRiskVariable(type) {
            var _a, _b, _c, _d, _e;
            const agg = riskMetricsService.getAggregates();
            const env = window;
            switch (type) {
                case TrackerVariableWebType.NO_MOUSE_MOVE_DURATION_MS:
                    return agg.noMouseMoveDurationMs;
                case TrackerVariableWebType.CLICK_ONLY_PATTERN:
                    return agg.clickOnlyPattern;
                case TrackerVariableWebType.FAST_PAGE_TRANSITION:
                    return agg.fastPageTransition;
                case TrackerVariableWebType.FORM_FILL_ANOMALY:
                    return agg.formFillAnomaly;
                case TrackerVariableWebType.FORM_FILL_DURATION_MS:
                    return agg.formFillDurationMs;
                case TrackerVariableWebType.NAVIGATION_VELOCITY:
                    return agg.navigationVelocity;
                case TrackerVariableWebType.INPUT_PASTE_RATIO:
                    return agg.inputPasteRatio;
                case TrackerVariableWebType.ENVIRONMENT_CHANGE_FLAGS:
                    return ((_a = env.__formicaEnvChange) === null || _a === void 0 ? void 0 : _a.flags) ? JSON.stringify(env.__formicaEnvChange.flags) : "{}";
                case TrackerVariableWebType.ENVIRONMENT_CHANGE_SCORE:
                    return (_c = (_b = env.__formicaEnvChange) === null || _b === void 0 ? void 0 : _b.score) !== null && _c !== void 0 ? _c : 0;
                case TrackerVariableWebType.HEADLESS_INDICATOR_COUNT:
                    return (_d = env.__formicaHeadlessIndicators) !== null && _d !== void 0 ? _d : 0;
                case TrackerVariableWebType.REMOTE_ACCESS_FLAGS_JSON:
                    return env.__formicaRemoteAccessFlags ? JSON.stringify(env.__formicaRemoteAccessFlags) : "{}";
                case TrackerVariableWebType.REMOTE_ACCESS_SCORE:
                    return (_e = env.__formicaRemoteAccessScore) !== null && _e !== void 0 ? _e : 0;
                default:
                    return "";
            }
        }
        resolveCustomVariable(trackerVariableSchema) {
            var _a;
            const variableName = trackerVariableSchema.name;
            return String((_a = this.globalVariables[variableName]) !== null && _a !== void 0 ? _a : "");
        }
        resolveUrlVariable(trackerVariableSchema) {
            const location = window.location;
            const option = trackerVariableSchema.selection;
            switch (option) {
                case "full":
                    return location.href;
                case "host":
                    return location.hostname;
                case "port":
                    return location.port;
                case "path":
                    return location.pathname;
                case "query":
                    return location.search.substring(1);
                case "fragment": {
                    const parts = location.href.split("#");
                    return parts.length > 1 ? parts[1] : "";
                }
                case "protocol":
                    return location.protocol.substring(0, location.protocol.length - 1);
                default:
                    return "";
            }
        }
        resolveCookieVariable(trackerVariableSchema) {
            var _a;
            const cookieName = trackerVariableSchema.cookieName;
            const cookies = document.cookie
                .split(";")
                .map((cookie) => cookie.trim())
                .filter((cookie) => cookie.startsWith(cookieName))
                .map((cookie) => trackerVariableSchema.decodeUrlCookie
                ? decodeURIComponent(cookie.substring(cookieName.length + 1))
                : cookie.substring(cookieName.length + 1));
            return (_a = cookies[0]) !== null && _a !== void 0 ? _a : "";
        }
        resolveElementVariable(trackerVariableSchema) {
            var _a, _b;
            const option = trackerVariableSchema.option;
            const element = document.querySelector(option.cssSelector);
            if (!element)
                return "";
            if (!option.attribute) {
                return (_a = element.textContent) !== null && _a !== void 0 ? _a : "";
            }
            return (_b = element.getAttribute(option.attribute)) !== null && _b !== void 0 ? _b : "";
        }
        resolveJavascriptVariable(trackerVariableSchema) {
            try {
                // Note: eval is still used here as it's part of the original functionality
                // In a production environment, consider using a safer alternative
                const result = eval(trackerVariableSchema.code);
                return String(result !== null && result !== void 0 ? result : "");
            }
            catch (error) {
                console.error("Error executing JavaScript variable:", error);
                return "";
            }
        }
        resolveIpAddressVariable() {
            return this.ip === "unknown" ? "" : this.ip;
        }
        resolveDeviceFingerprint() {
            return this.deviceFingerprint;
        }
        resolveSuspiciousActivityVariable() {
            const flags = this.suspiciousFlags;
            let score = 0;
            if (flags.unnaturalMouseMoves)
                score += 0.25;
            if (flags.bigClipboardPaste)
                score += 0.25;
            if (flags.lowFPSDetected)
                score += 0.25;
            if (flags.delayedClickDetected)
                score += 0.25;
            return score;
        }
        resolveTriggerVariable(trackerVariableSchema, mouseEvent) {
            var _a, _b;
            const elementOption = trackerVariableSchema.selection;
            const parent = document.createElement("div");
            const target = mouseEvent.target;
            if (!target)
                return "";
            parent.appendChild(target.cloneNode(true));
            const element = parent.querySelector(elementOption.cssSelector);
            if (!element)
                return "";
            if (elementOption.attribute) {
                return (_a = element.getAttribute(elementOption.attribute)) !== null && _a !== void 0 ? _a : "";
            }
            return (_b = element.textContent) !== null && _b !== void 0 ? _b : "";
        }
    }

    /**
     * Formica Tracker Web SDK
     * Main tracker implementation with clean architecture
     */
    class FormicaTracker {
        constructor() {
            this.state = {
                tenant: "",
                serviceUrl: "",
                apiKey: "",
            };
            this.config = {
                apiUrl: "",
                trackers: [],
            };
            this.globalVariables = {
                viewDuration: 0,
            };
            this.suspiciousFlags = {
                unnaturalMouseMoves: false,
                bigClipboardPaste: false,
                lowFPSDetected: false,
                delayedClickDetected: false,
            };
            this.timerService = new TimerService();
            this.trackerConfigService = new TrackerConfigService();
            this.lastRiskAlertScore = 0;
            this.lastRiskAlertAt = 0; // epoch ms
            this.riskAlertCooldownMs = 60000; // 60s
            this.riskAlertThreshold = 0.7;
            this.riskAlertEnabled = true;
        }
        /**
         * Initialize the tracker with configuration
         */
        async initialize(args) {
            try {
                this.validateInitArgs(args);
                const { serviceUrl, tenantName, apiKey } = args;
                this.state = {
                    tenant: tenantName,
                    serviceUrl,
                    apiKey,
                };
                await this.setupTracker();
                await this.initializeServices();
                this.setupEventListeners();
                this.startMonitoring();
            }
            catch (error) {
                console.error("Tracker initialization failed:", error);
                throw error;
            }
        }
        /**
         * Trigger a custom event asynchronously
         */
        async triggerCustom(name, variables) {
            const payload = await this.buildCustomTriggerPayload(name, variables);
            if (payload && this.eventQueue) {
                this.eventQueue.add(payload);
            }
        }
        /**
         * Trigger a custom event synchronously
         */
        async triggerCustomSync(name, variables, config) {
            try {
                const payload = await this.buildCustomTriggerPayload(name, variables);
                if (!payload || !this.eventQueue)
                    return null;
                const response = await this.eventQueue.sendSync(payload, config === null || config === void 0 ? void 0 : config.timeoutMs);
                return response;
            }
            catch (error) {
                console.error("Sync trigger failed:", error);
                return null;
            }
        }
        /**
         * Set a user-defined variable
         */
        setUserDefinedVariable(name, value) {
            this.globalVariables[name] = value;
        }
        /**
         * Get a user-defined variable
         */
        getUserDefinedVariable(name) {
            return this.globalVariables[name];
        }
        /**
         * Get a copy of all user-defined variables
         */
        getUserDefinedVariables() {
            return { ...this.globalVariables };
        }
        validateInitArgs(args) {
            var _a, _b, _c;
            if (!args) {
                throw new Error("Tracker initialization args must be provided");
            }
            if (!((_a = args.serviceUrl) === null || _a === void 0 ? void 0 : _a.trim())) {
                throw new Error("Service URL must be provided");
            }
            if (!((_b = args.apiKey) === null || _b === void 0 ? void 0 : _b.trim())) {
                throw new Error("API key must be provided");
            }
            if (!((_c = args.tenantName) === null || _c === void 0 ? void 0 : _c.trim())) {
                throw new Error("Tenant name must be provided");
            }
        }
        async setupTracker() {
            const { trackers, apiUrl } = await this.trackerConfigService.getTrackers(this.state.serviceUrl, this.state.tenant, this.state.apiKey);
            this.config = { trackers, apiUrl };
        }
        async initializeServices() {
            // Initialize IP and fingerprint
            this.state.ip = await IpService.fetchIp();
            this.state.deviceIdFingerPrint = await FingerprintService.getDeviceFingerprint();
            // Initialize event queue
            this.eventQueue = new EventQueue(this.state.tenant, this.config.apiUrl);
            this.eventQueue.startProcessing();
            // Initialize variable resolver
            this.variableResolver = new VariableResolver(this.state.ip || "", this.state.deviceIdFingerPrint || "", this.suspiciousFlags, this.globalVariables);
            // Start timer
            this.timerService.start();
        }
        setupEventListeners() {
            this.config.trackers.forEach((tracker) => {
                tracker.triggers.forEach((triggerSchema) => {
                    this.initListener(triggerSchema, tracker);
                });
            });
        }
        startMonitoring() {
            monitorRemoteControlSuspicion((flags) => {
                this.suspiciousFlags = { ...flags };
                const w = window;
                w.__formicaRemoteAccessFlags = { ...flags };
            });
            riskMetricsService.initListeners();
            this.setupEnvironmentBaseline();
            this.computeHeadlessIndicators();
            window.setInterval(() => this.updateCompositeRiskScores(), 5000);
        }
        setupEnvironmentBaseline() {
            try {
                const w = window;
                const baseline = riskMetricsService.ensureBaseline(this.state.tenant, this.state.deviceIdFingerPrint || "anon", {
                    language: BrowserDetector.getLanguage(),
                    timezone: BrowserDetector.getTimezone(),
                    os: BrowserDetector.getOS(),
                    browser: BrowserDetector.getBrowser(),
                });
                const current = {
                    language: BrowserDetector.getLanguage(),
                    timezone: BrowserDetector.getTimezone(),
                    os: BrowserDetector.getOS(),
                    browser: BrowserDetector.getBrowser(),
                };
                const diff = riskMetricsService.diffEnvironment(baseline, current);
                const score = riskMetricsService.calcEnvironmentChangeScore(diff);
                w.__formicaEnvChange = { flags: diff, score };
            }
            catch {
                logger.warn("Failed to setup environment baseline, continuing without it");
            }
        }
        async computeHeadlessIndicators() {
            try {
                const w = window;
                let count = 0;
                if (navigator.webdriver)
                    count++;
                if (!navigator.languages || navigator.languages.length === 0)
                    count++;
                if (!navigator.plugins || navigator.plugins.length === 0)
                    count++;
                const canvas = document.createElement("canvas");
                const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
                if (!gl)
                    count++;
                canvas.remove();
                if (BrowserDetector.isBot())
                    count++;
                w.__formicaHeadlessIndicators = count;
            }
            catch {
                logger.warn("Failed to compute headless indicators");
            }
        }
        updateCompositeRiskScores() {
            var _a, _b;
            try {
                const w = window;
                const agg = riskMetricsService.getAggregates();
                agg.headlessIndicatorCount = w.__formicaHeadlessIndicators || 0;
                const envMismatch = (((_a = w.__formicaEnvChange) === null || _a === void 0 ? void 0 : _a.score) || 0) > 0.5;
                const remoteAccessScore = riskMetricsService.calcRemoteAccessScore(this.suspiciousFlags, envMismatch, agg);
                w.__formicaRemoteAccessScore = remoteAccessScore;
                w.__formicaRemoteAccessFlags = {
                    ...this.suspiciousFlags,
                    clickOnlyPattern: agg.clickOnlyPattern,
                    fastPageTransition: agg.fastPageTransition,
                    headlessIndicatorCount: agg.headlessIndicatorCount,
                    envChangeScore: ((_b = w.__formicaEnvChange) === null || _b === void 0 ? void 0 : _b.score) || 0,
                };
                this.maybeEmitRiskAlert(remoteAccessScore);
            }
            catch {
                logger.warn("Failed to update composite risk scores");
            }
        }
        async maybeEmitRiskAlert(score) {
            if (!this.riskAlertEnabled)
                return;
            const now = Date.now();
            const crossedUp = this.lastRiskAlertScore < this.riskAlertThreshold && score >= this.riskAlertThreshold;
            const cooldownOk = now - this.lastRiskAlertAt > this.riskAlertCooldownMs;
            this.lastRiskAlertScore = score;
            if (!crossedUp || !cooldownOk)
                return;
            this.lastRiskAlertAt = now;
            if (!this.variableResolver || !this.eventQueue)
                return;
            for (const tracker of this.config.trackers) {
                const alertTrigger = tracker.triggers.find((t) => t.type === TrackerTriggerTypeWeb.RISK_ALERT);
                if (!alertTrigger)
                    continue;
                const vars = { riskScore: score };
                for (const variable of tracker.variables) {
                    if (variable.type === TrackerVariableWebType.CUSTOM)
                        continue;
                    vars[variable.name] = await this.variableResolver.resolve(variable, new MouseEvent(""));
                }
                const payload = EventBuilder.buildEvent(tracker, vars);
                this.eventQueue.add(payload);
            }
        }
        initListener(triggerSchema, tracker) {
            switch (triggerSchema.type) {
                case TrackerTriggerTypeWeb.PAGE_VIEW:
                    this.setupPageViewListener(triggerSchema, tracker);
                    break;
                case TrackerTriggerTypeWeb.RISK_ALERT:
                    // internal emission only; no DOM listener needed
                    break;
                case TrackerTriggerTypeWeb.CUSTOM:
                    this.setupCustomEventListener(triggerSchema, tracker);
                    break;
                default:
                    this.setupDefaultEventListener(triggerSchema, tracker);
                    break;
            }
        }
        setupPageViewListener(triggerSchema, tracker) {
            this.state.previousHref = document.location.href;
            window.addEventListener("load", () => {
                const bodyElement = document.querySelector("body");
                if (!bodyElement)
                    return;
                // initial registration
                riskMetricsService.registerPageView();
                const observer = new MutationObserver(() => {
                    if (this.state.previousHref !== document.location.href) {
                        this.handleEvent(new Event("pageview"), triggerSchema, tracker);
                        this.state.previousHref = document.location.href;
                        riskMetricsService.registerPageView();
                    }
                });
                observer.observe(bodyElement, {
                    childList: true,
                    subtree: true,
                });
            });
        }
        setupCustomEventListener(triggerSchema, tracker) {
            document.addEventListener(triggerSchema.name, (event) => {
                this.handleEvent(event, triggerSchema, tracker);
            });
        }
        setupDefaultEventListener(triggerSchema, tracker) {
            document.addEventListener(triggerSchema.type.toLowerCase(), (event) => {
                this.handleEvent(event, triggerSchema, tracker);
            });
        }
        async handleEvent(event, triggerSchema, tracker) {
            if (!this.variableResolver || !this.eventQueue)
                return;
            const trackerVariables = {};
            const mouseEvent = event;
            // Resolve variables
            for (const variable of tracker.variables) {
                trackerVariables[variable.name] = await this.variableResolver.resolve(variable, mouseEvent);
            }
            // Validate event
            const isValid = EventValidator.validate(mouseEvent, triggerSchema);
            if (!isValid)
                return;
            // Build and send event
            const payload = EventBuilder.buildEvent(tracker, trackerVariables);
            this.eventQueue.add(payload);
        }
        async buildCustomTriggerPayload(name, variables) {
            if (!this.variableResolver)
                return null;
            for (const tracker of this.config.trackers) {
                const event = tracker.triggers.find((t) => {
                    var _a;
                    return t.type === TrackerTriggerTypeWeb.CUSTOM &&
                        (((_a = t.option) === null || _a === void 0 ? void 0 : _a.event) === name || t.name === name);
                });
                if (!event)
                    continue;
                // Resolve variables
                for (const variable of tracker.variables) {
                    if (variable.type === TrackerVariableWebType.CUSTOM) {
                        if (!Object.prototype.hasOwnProperty.call(variables, variable.name)) {
                            variables[variable.name] = await this.variableResolver.resolve(variable, new MouseEvent(""));
                        }
                    }
                    else {
                        variables[variable.name] = await this.variableResolver.resolve(variable, new MouseEvent(""));
                    }
                }
                return EventBuilder.buildEvent(tracker, variables);
            }
            return null;
        }
        /** Public: configure risk alert parameters */
        configureRiskAlert(options) {
            if (typeof options.threshold === "number" && options.threshold >= 0 && options.threshold <= 1) {
                this.riskAlertThreshold = options.threshold;
            }
            if (typeof options.cooldownMs === "number" && options.cooldownMs >= 0) {
                this.riskAlertCooldownMs = options.cooldownMs;
            }
            if (typeof options.enabled === "boolean") {
                this.riskAlertEnabled = options.enabled;
            }
        }
    }
    // Export singleton instance
    const formicaTracker = new FormicaTracker();
    const TrackerManager = {
        initialize: formicaTracker.initialize.bind(formicaTracker),
        triggerCustom: formicaTracker.triggerCustom.bind(formicaTracker),
        triggerCustomSync: formicaTracker.triggerCustomSync.bind(formicaTracker),
        setUserDefinedVariable: formicaTracker.setUserDefinedVariable.bind(formicaTracker),
        getUserDefinedVariable: formicaTracker.getUserDefinedVariable.bind(formicaTracker),
        getUserDefinedVariables: formicaTracker.getUserDefinedVariables.bind(formicaTracker),
        configureRiskAlert: formicaTracker.configureRiskAlert.bind(formicaTracker),
    };

    return TrackerManager;

}));