UNPKG

@trlogic/tracker-web

Version:
1,374 lines 51.8 kB
'use strict';

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

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),
};

module.exports = TrackerManager;
//# sourceMappingURL=bundle.js.map