@trlogic/tracker-web
Version:
Formica Tracker Web
1,372 lines • 57.2 kB
JavaScript
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;
}));