1093 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1093 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* eslint-disable unicorn/no-null */
 | 
						||
 | 
						||
// Получение отпечатка Canvas
 | 
						||
const getCanvasFingerprint = async () => {
 | 
						||
    try {
 | 
						||
        const canvas = document.createElement('canvas');
 | 
						||
        const ctx = canvas.getContext('2d');
 | 
						||
        if (!ctx) return null;
 | 
						||
        canvas.width = 220;
 | 
						||
        canvas.height = 30;
 | 
						||
        ctx.textBaseline = 'top';
 | 
						||
        ctx.font = '14px Arial';
 | 
						||
        ctx.fillStyle = '#f60';
 | 
						||
        ctx.fillRect(0, 0, 0, 0);
 | 
						||
        ctx.fillStyle = '#069';
 | 
						||
        ctx.fillText('fingerprint123', 2, 2);
 | 
						||
        return canvas.toDataURL();
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение информации о точности шейдеров WebGL
 | 
						||
const getWebGLPrecision = (gl) => {
 | 
						||
    if (!gl) return {};
 | 
						||
    const getPrecision = (shaderType, precisionType, name) => {
 | 
						||
        const precision = gl.getShaderPrecisionFormat(shaderType, precisionType);
 | 
						||
        return {
 | 
						||
            [`precision${name}`]: precision?.precision ?? null,
 | 
						||
            [`rangeMin${name}`]: precision?.rangeMin ?? null,
 | 
						||
            [`rangeMax${name}`]: precision?.rangeMax ?? null,
 | 
						||
        };
 | 
						||
    };
 | 
						||
 | 
						||
    const HIGH_FLOAT = gl.HIGH_FLOAT;
 | 
						||
    const MEDIUM_FLOAT = gl.MEDIUM_FLOAT;
 | 
						||
    const LOW_FLOAT = gl.LOW_FLOAT;
 | 
						||
    const HIGH_INT = gl.HIGH_INT;
 | 
						||
    const MEDIUM_INT = gl.MEDIUM_INT;
 | 
						||
    const LOW_INT = gl.LOW_INT;
 | 
						||
    const VERTEX_SHADER = gl.VERTEX_SHADER;
 | 
						||
    const FRAGMENT_SHADER = gl.FRAGMENT_SHADER;
 | 
						||
 | 
						||
    return {
 | 
						||
        ...getPrecision(VERTEX_SHADER, HIGH_FLOAT, 'VertexShaderHighFloat'),
 | 
						||
        ...getPrecision(VERTEX_SHADER, MEDIUM_FLOAT, 'VertexShaderMediumFloat'),
 | 
						||
        ...getPrecision(VERTEX_SHADER, LOW_FLOAT, 'VertexShaderLowFloat'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, HIGH_FLOAT, 'FragmentShaderHighFloat'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, MEDIUM_FLOAT, 'FragmentShaderMediumFloat'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, LOW_FLOAT, 'FragmentShaderLowFloat'),
 | 
						||
 | 
						||
        ...getPrecision(VERTEX_SHADER, HIGH_INT, 'VertexShaderHighInt'),
 | 
						||
        ...getPrecision(VERTEX_SHADER, MEDIUM_INT, 'VertexShaderMediumInt'),
 | 
						||
        ...getPrecision(VERTEX_SHADER, LOW_INT, 'VertexShaderLowInt'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, HIGH_INT, 'FragmentShaderHighInt'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, MEDIUM_INT, 'FragmentShaderMediumInt'),
 | 
						||
        ...getPrecision(FRAGMENT_SHADER, LOW_INT, 'FragmentShaderLowInt'),
 | 
						||
    };
 | 
						||
};
 | 
						||
 | 
						||
// Сбор всех данных WebGL (отпечаток и свойства)
 | 
						||
const getWebGLData = async () => {
 | 
						||
    try {
 | 
						||
        const canvas = document.createElement('canvas');
 | 
						||
        const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
 | 
						||
 | 
						||
        // Попытка получить WebGL2 контекст для дополнительных свойств
 | 
						||
        const gl2 = canvas.getContext('webgl2');
 | 
						||
 | 
						||
        if (!gl) return { webgl: null, webgl_properties: null, webgl_precision: {} };
 | 
						||
 | 
						||
        const getParameter = (p) => gl.getParameter(p);
 | 
						||
        const dbg = gl.getExtension('WEBGL_debug_renderer_info');
 | 
						||
 | 
						||
        // Отпечаток WebGL (более краткий набор свойств)
 | 
						||
        const webgl = {
 | 
						||
            vendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : getParameter(gl.VENDOR),
 | 
						||
            renderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : getParameter(gl.RENDERER),
 | 
						||
            version: getParameter(gl.VERSION),
 | 
						||
            shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION),
 | 
						||
            maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE),
 | 
						||
            aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
 | 
						||
            aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE),
 | 
						||
            redBits: getParameter(gl.RED_BITS),
 | 
						||
            greenBits: getParameter(gl.GREEN_BITS),
 | 
						||
            blueBits: getParameter(gl.BLUE_BITS),
 | 
						||
            alphaBits: getParameter(gl.ALPHA_BITS),
 | 
						||
            depthBits: getParameter(gl.DEPTH_BITS),
 | 
						||
            stencilBits: getParameter(gl.STENCIL_BITS),
 | 
						||
            extensions: gl.getSupportedExtensions(),
 | 
						||
        };
 | 
						||
 | 
						||
        // Подробные свойства WebGL
 | 
						||
        const webgl_properties = {
 | 
						||
            unmaskedVendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null,
 | 
						||
            unmaskedRenderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
 | 
						||
            vendor: getParameter(gl.VENDOR),
 | 
						||
            renderer: getParameter(gl.RENDERER),
 | 
						||
            shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION),
 | 
						||
            version: getParameter(gl.VERSION),
 | 
						||
            maxAnisotropy: gl.getExtension('EXT_texture_filter_anisotropic')
 | 
						||
                ? getParameter(gl.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT)
 | 
						||
                : null,
 | 
						||
 | 
						||
            shadingLanguage2: gl2?.getParameter?.(gl2.SHADING_LANGUAGE_VERSION) ?? null,
 | 
						||
            version2: gl2?.getParameter?.(gl2.VERSION) ?? null,
 | 
						||
 | 
						||
            aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
 | 
						||
            aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE),
 | 
						||
            redBits: getParameter(gl.RED_BITS),
 | 
						||
            greenBits: getParameter(gl.GREEN_BITS),
 | 
						||
            blueBits: getParameter(gl.BLUE_BITS),
 | 
						||
            alphaBits: getParameter(gl.ALPHA_BITS),
 | 
						||
            depthBits: getParameter(gl.DEPTH_BITS),
 | 
						||
            stencilBits: getParameter(gl.STENCIL_BITS),
 | 
						||
            subpixelBits: getParameter(gl.SUBPIXEL_BITS),
 | 
						||
            sampleBuffers: getParameter(gl.SAMPLE_BUFFERS),
 | 
						||
            samples: getParameter(gl.SAMPLES),
 | 
						||
            maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE),
 | 
						||
            maxCubeMapTextureSize: getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE),
 | 
						||
            maxRenderBufferSize: getParameter(gl.MAX_RENDERBUFFER_SIZE),
 | 
						||
            maxVertexAttribs: getParameter(gl.MAX_VERTEX_ATTRIBS),
 | 
						||
            maxVertexTextureImageUnits: getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS),
 | 
						||
            maxTextureImageUnits: getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
 | 
						||
            maxFragmentUniformVectors: getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
 | 
						||
            maxVertexUniformVectors: getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
 | 
						||
 | 
						||
            extensions: gl.getSupportedExtensions()?.join(','),
 | 
						||
        };
 | 
						||
 | 
						||
        const webgl_precision = getWebGLPrecision(gl);
 | 
						||
 | 
						||
        return { webgl, webgl_properties, webgl_precision };
 | 
						||
    } catch {
 | 
						||
        return { webgl: null, webgl_properties: null, webgl_precision: {} };
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Сбор всех данных Audio (отпечаток и свойства AudioContext)
 | 
						||
const getAudioData = async () => {
 | 
						||
    try {
 | 
						||
        const Offline = window.OfflineAudioContext || window.webkitOfflineAudioContext;
 | 
						||
        const Context = window.AudioContext || window.webkitAudioContext;
 | 
						||
 | 
						||
        if (!Offline || !Context) return { audio: null, audio_properties: null };
 | 
						||
 | 
						||
        // 1. Отпечаток Audio (Хеширование)
 | 
						||
        const offlineCtx = new Offline(1, 44_100, 44_100);
 | 
						||
        const osc = offlineCtx.createOscillator();
 | 
						||
        const comp = offlineCtx.createDynamicsCompressor();
 | 
						||
        osc.type = 'triangle';
 | 
						||
        osc.frequency.value = 10_000;
 | 
						||
        osc.connect(comp);
 | 
						||
        comp.connect(offlineCtx.destination);
 | 
						||
        osc.start(0);
 | 
						||
        const buf = await offlineCtx.startRendering();
 | 
						||
        let sum = 0;
 | 
						||
        const data = buf.getChannelData(0);
 | 
						||
        for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]);
 | 
						||
        const audio = sum;
 | 
						||
 | 
						||
        // 2. Свойства AudioContext
 | 
						||
        const ctx = new Context();
 | 
						||
        const properties = {};
 | 
						||
 | 
						||
        // Свойства BaseAudioContext
 | 
						||
        properties.BaseAudioContextSampleRate = ctx.sampleRate;
 | 
						||
        properties.AudioContextBaseLatency = ctx.baseLatency ?? null;
 | 
						||
        properties.AudioContextOutputLatency = ctx.outputLatency ?? null;
 | 
						||
 | 
						||
        // Свойства AudioDestinationNode
 | 
						||
        properties.AudioDestinationNodeMaxChannelCount = ctx.destination?.maxChannelCount ?? null;
 | 
						||
 | 
						||
        // Свойства других узлов
 | 
						||
        const analyzer = ctx.createAnalyser();
 | 
						||
        properties.AnalyzerNodeFftSize = analyzer.fftSize;
 | 
						||
        properties.AnalyzerNodeFrequencyBinCount = analyzer.frequencyBinCount;
 | 
						||
        properties.AnalyzerNodeMinDecibels = analyzer.minDecibels;
 | 
						||
        properties.AnalyzerNodeMaxDecibels = analyzer.maxDecibels;
 | 
						||
 | 
						||
        const waveShaper = ctx.createWaveShaper();
 | 
						||
        properties.WaveShaperNodeMaxCurveLength = waveShaper.maxCurveLength ?? null;
 | 
						||
 | 
						||
        const oscillator = ctx.createOscillator();
 | 
						||
        properties.OscillatorNodeMaxFrequency = oscillator.frequency?.maxValue ?? null;
 | 
						||
 | 
						||
        const panner = ctx.createPanner();
 | 
						||
        properties.PannerNodeMaxDistance = panner.maxDistance ?? null;
 | 
						||
        properties.PannerNodeMaxCone = panner.coneOuterAngle;
 | 
						||
 | 
						||
        properties.numberOfInputs = panner.numberOfInputs;
 | 
						||
        properties.numberOfOutputs = panner.numberOfOutputs;
 | 
						||
        properties.channelCount = panner.channelCount;
 | 
						||
        properties.panningModel = panner.panningModel;
 | 
						||
        properties.distanceModel = panner.distanceModel;
 | 
						||
        properties.coneInnerAngle = panner.coneInnerAngle;
 | 
						||
        properties.coneOuterAngle = panner.coneOuterAngle;
 | 
						||
        properties.coneOuterGain = panner.coneOuterGain;
 | 
						||
 | 
						||
        // Максимальное время задержки DelayNode
 | 
						||
        const delay = ctx.createDelay();
 | 
						||
        properties.maxDelayTime = delay.maxDelayTime;
 | 
						||
 | 
						||
        // Поддерживаемые узлы
 | 
						||
        properties.supported_nodes = [];
 | 
						||
        const audioNodes = [
 | 
						||
            'DynamicsCompressorNode',
 | 
						||
            'DelayNode',
 | 
						||
            'OscillatorNode',
 | 
						||
            'StereoPannerNode',
 | 
						||
            'AnalyserNode',
 | 
						||
            'ConvolverNode',
 | 
						||
            'WaveShaperNode',
 | 
						||
            'PannerNode',
 | 
						||
            'PeriodicWaveNode',
 | 
						||
            'ChannelMergerNode',
 | 
						||
        ];
 | 
						||
 | 
						||
        for (const node of audioNodes) {
 | 
						||
            if (typeof window[node] === 'function') {
 | 
						||
                properties.supported_nodes.push(node);
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        // Свойства каналов и AudioWorkletGlobalScope
 | 
						||
        properties.channel_count_modes = ['max', 'clamped-max', 'explicit'];
 | 
						||
        properties.channel_interpretations = ['speakers', 'discrete'];
 | 
						||
 | 
						||
        if (window.AudioWorkletGlobalScope) {
 | 
						||
            properties.AudioWorkletGlobalScopeMaxChannelCount = window.AudioWorkletGlobalScope.MAX_CHANNEL_COUNT;
 | 
						||
            properties.render_quantum_size = window.AudioWorkletGlobalScope.RENDER_QUANTUM_SIZE;
 | 
						||
        } else {
 | 
						||
            properties.AudioWorkletGlobalScopeMaxChannelCount = null;
 | 
						||
            properties.render_quantum_size = null;
 | 
						||
        }
 | 
						||
 | 
						||
        // Дополнительные свойства контекста
 | 
						||
        properties.sampleRate = ctx.sampleRate;
 | 
						||
        properties.outputLatency = ctx.outputLatency ?? null;
 | 
						||
        properties.inputLatency = ctx.baseLatency ?? null;
 | 
						||
        properties.isResampling = false;
 | 
						||
        properties.contextType = 'playback';
 | 
						||
        properties.hardwareAccelerated = false;
 | 
						||
        properties.shared = true;
 | 
						||
 | 
						||
        await ctx.close();
 | 
						||
 | 
						||
        return { audio, audio_properties: properties };
 | 
						||
    } catch {
 | 
						||
        return { audio: null, audio_properties: null };
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Отпечаток Math
 | 
						||
const getMathFingerprint = async () => {
 | 
						||
    try {
 | 
						||
        const values = {
 | 
						||
            acos: Math.acos(0.123_456_789),
 | 
						||
            asin: Math.asin(0.123_456_789),
 | 
						||
            atan: Math.atan(0.123_456_789),
 | 
						||
            sin: Math.sin(1e37),
 | 
						||
            cos: Math.cos(1e-37),
 | 
						||
            tan: Math.tan(10),
 | 
						||
            exp: Math.exp(1),
 | 
						||
            log: Math.log(10),
 | 
						||
            sqrt: Math.sqrt(2),
 | 
						||
            pow: Math.pow(123_456.789, -0.123),
 | 
						||
            round: (0.1 + 0.2).toFixed(20),
 | 
						||
            toExp: (123_456_789).toExponential(20),
 | 
						||
            oneDivNegZero: 1 / -0,
 | 
						||
        };
 | 
						||
        return values;
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Сбор информации о батарее
 | 
						||
const getBattery = async () => {
 | 
						||
    try {
 | 
						||
        if (!navigator.getBattery) return { has_battery_api: false };
 | 
						||
        const b = await navigator.getBattery();
 | 
						||
        return {
 | 
						||
            battery: null, // Хеш или заглушка для совместимости
 | 
						||
            has_battery_api: true,
 | 
						||
            has_battery_device: false,
 | 
						||
            battery_level: b.level ?? null,
 | 
						||
            battery_charging: b.charging ?? null,
 | 
						||
            battery_discharging_time: b.dischargingTime === Infinity ? null : b.dischargingTime ?? null,
 | 
						||
            battery_charging_time: b.chargingTime === Infinity ? null : b.chargingTime ?? null,
 | 
						||
            battery_status: b.charging
 | 
						||
                ? 'charging'
 | 
						||
                : b.dischargingTime === Infinity
 | 
						||
                ? 'full'
 | 
						||
                : b.dischargingTime === undefined
 | 
						||
                ? null
 | 
						||
                : 'discharging',
 | 
						||
            battery_low_power_mode: false,
 | 
						||
            battery_is_full: b.level === 1 && b.chargingTime === 0,
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Сбор информации о WebGPU
 | 
						||
const getWebGPU = async () => {
 | 
						||
    try {
 | 
						||
        if (!navigator.gpu?.requestAdapter) return null;
 | 
						||
        const adapter = await navigator.gpu.requestAdapter();
 | 
						||
        if (!adapter) return null;
 | 
						||
 | 
						||
        let preferredCanvasFormatValue = null;
 | 
						||
        try {
 | 
						||
            if (navigator.gpu.getPreferredCanvasFormat) {
 | 
						||
                preferredCanvasFormatValue = navigator.gpu.getPreferredCanvasFormat();
 | 
						||
            }
 | 
						||
        } catch {} // eslint-disable-line no-empty
 | 
						||
 | 
						||
        const adapterInfo = await adapter.requestAdapterInfo?.();
 | 
						||
 | 
						||
        return {
 | 
						||
            isEnabled: true,
 | 
						||
            highPerformance: adapter?.powerPreference === 'high-performance' || null,
 | 
						||
            lowPower: adapter?.powerPreference === 'low-power' || null,
 | 
						||
            fallback: false,
 | 
						||
            preferredCanvasFormat: preferredCanvasFormatValue,
 | 
						||
            supported_features: [...(adapter.features?.values?.() ?? [])],
 | 
						||
            deviceLimits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {},
 | 
						||
            powerPreference: adapter.powerPreference ?? null,
 | 
						||
            adapterName: adapterInfo?.description ?? null,
 | 
						||
            vendor: adapterInfo?.vendor ?? null,
 | 
						||
            architecture: adapterInfo?.architecture ?? null,
 | 
						||
            device: adapterInfo?.device ?? null,
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение информации о лимитах хранилища и использовании
 | 
						||
const getStorage = async () => {
 | 
						||
    try {
 | 
						||
        const est = await navigator.storage?.estimate?.();
 | 
						||
        return est ? { quota: est.quota ?? null, usage: est.usage ?? null } : null;
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение списка голосов для Speech Synthesis
 | 
						||
const getVoices = async () => {
 | 
						||
    try {
 | 
						||
        if (!window.speechSynthesis) return null;
 | 
						||
 | 
						||
        const mapVoice = (v) => ({
 | 
						||
            name: v.name,
 | 
						||
            lang: v.lang,
 | 
						||
            voiceURI: v.voiceURI,
 | 
						||
            localService: v.localService,
 | 
						||
            default: v.default,
 | 
						||
            // Простое хеширование URI для id
 | 
						||
            id: v.voiceURI?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
 | 
						||
            vendor: null,
 | 
						||
            device: null,
 | 
						||
        });
 | 
						||
 | 
						||
        const voices = window.speechSynthesis.getVoices();
 | 
						||
        if (voices.length > 0) {
 | 
						||
            return voices.map((v) => mapVoice(v));
 | 
						||
        }
 | 
						||
 | 
						||
        // Асинхронная загрузка, если голоса еще не загружены
 | 
						||
        return new Promise((resolve) => {
 | 
						||
            window.speechSynthesis.onvoiceschanged = () => {
 | 
						||
                const list = window.speechSynthesis.getVoices();
 | 
						||
                resolve(list.map((v) => mapVoice(v)));
 | 
						||
            };
 | 
						||
        });
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение информации о сетевом соединении
 | 
						||
const getNetwork = () => {
 | 
						||
    try {
 | 
						||
        const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
 | 
						||
        if (!c) return null;
 | 
						||
        return {
 | 
						||
            downlink: c.downlink ?? null,
 | 
						||
            effectiveType: c.effectiveType ?? null,
 | 
						||
            rtt: c.rtt ?? null,
 | 
						||
            saveData: c.saveData ?? null,
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение информации о локализации через Intl API
 | 
						||
const getIntlInfo = () => {
 | 
						||
    try {
 | 
						||
        const locales = navigator.languages ?? [navigator.language];
 | 
						||
        const dateSample = new Date(2020, 11, 31, 19, 5, 7);
 | 
						||
        const numSample = 1_234_567.89;
 | 
						||
        return {
 | 
						||
            locales,
 | 
						||
            date: new Intl.DateTimeFormat(locales).format(dateSample),
 | 
						||
            time: new Intl.DateTimeFormat(locales, { hour: 'numeric', minute: 'numeric' }).format(dateSample),
 | 
						||
            num: new Intl.NumberFormat(locales).format(numSample),
 | 
						||
            currency: new Intl.NumberFormat(locales, { style: 'currency', currency: 'USD' }).format(numSample),
 | 
						||
            resolved: {
 | 
						||
                date: new Intl.DateTimeFormat(locales).resolvedOptions(),
 | 
						||
                num: new Intl.NumberFormat(locales).resolvedOptions(),
 | 
						||
            },
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Сбор информации о поддержке различных браузерных API
 | 
						||
const getCommonFeatures = () => {
 | 
						||
    // Длинный список булевых флагов для API и возможностей
 | 
						||
    const features = {
 | 
						||
        SharedWorker: !!window.SharedWorker,
 | 
						||
        OrientationEvent: !!window.DeviceOrientationEvent,
 | 
						||
        WebHID: !!navigator.hid,
 | 
						||
        FileSystemAccess: !!window.showDirectoryPicker,
 | 
						||
        FontAccess: !!window.queryLocalFonts,
 | 
						||
        WebGPU: !!navigator.gpu,
 | 
						||
        FencedFrames: !!document.createElement('iframe').hasAttribute('fence'),
 | 
						||
        TopicsAPI: typeof document.browsingTopics === 'function',
 | 
						||
        PrivacySandboxAdsAPIs: typeof window.adAuction === 'object',
 | 
						||
        AttributionReporting: typeof window.attributionReporting === 'object',
 | 
						||
        ConversionMeasurement: typeof window.attributionReporting === 'object',
 | 
						||
        PrivateAggregation: typeof window.privateAggregation === 'object',
 | 
						||
        PrivateAggregationForTesting: typeof window.privateAggregation?.sendHistogramReport === 'function',
 | 
						||
        SharedStorage: typeof window.sharedStorage === 'object',
 | 
						||
        InterestCohort: typeof document.interestCohort === 'function',
 | 
						||
        AggregateAttestation: false,
 | 
						||
        TrustTokens: !!window.fetch?.trustToken,
 | 
						||
        DocumentPolicy: !!document.featurePolicy,
 | 
						||
        WindowControlsOverlay: !!navigator.windowControlsOverlay,
 | 
						||
        WindowPlacement: !!window.screen.isExtended,
 | 
						||
        WebAssembly: typeof WebAssembly === 'object',
 | 
						||
        WebGL2: !!window.WebGL2RenderingContext,
 | 
						||
        WebXR: !!navigator.xr,
 | 
						||
        WebCodecs: !!window.VideoEncoder,
 | 
						||
        WebNFC: !!window.NFC,
 | 
						||
        WebShare: !!navigator.share,
 | 
						||
        WebUSB: !!navigator.usb,
 | 
						||
        WebVTT: !!window.VTTCue,
 | 
						||
        CanvasComposition: true,
 | 
						||
        CSSPseudoElements: true,
 | 
						||
        CSSCustomProperties: true,
 | 
						||
        DynamicImport: true,
 | 
						||
        AsyncFunction: true,
 | 
						||
        BigInt: typeof BigInt === 'function',
 | 
						||
        PointerEvents: !!window.PointerEvent,
 | 
						||
        ResizeObserver: !!window.ResizeObserver,
 | 
						||
        IntersectionObserver: !!window.IntersectionObserver,
 | 
						||
        requestIdleCallback: !!window.requestIdleCallback,
 | 
						||
        requestAnimationFrame: !!window.requestAnimationFrame,
 | 
						||
        WebAnimations: !!document.body?.animate,
 | 
						||
        BackgroundFetch: !!window.BackgroundFetchManager,
 | 
						||
        BackgroundSync: !!window.SyncManager,
 | 
						||
        Bluetooth: !!navigator.bluetooth,
 | 
						||
        PaymentRequest: !!window.PaymentRequest,
 | 
						||
        PushMessaging: !!window.PushManager,
 | 
						||
        SpeechRecognition: !!window.SpeechRecognition || !!window.webkitSpeechRecognition,
 | 
						||
        SpeechSynthesis: !!window.speechSynthesis,
 | 
						||
        WakeLock: !!navigator.wakeLock,
 | 
						||
        WebWorkers: !!window.Worker,
 | 
						||
        ServiceWorkers: !!navigator.serviceWorker,
 | 
						||
        IndexedDB: !!window.indexedDB,
 | 
						||
        LocalStorage: !!window.localStorage,
 | 
						||
        SessionStorage: !!window.sessionStorage,
 | 
						||
        Cookies: navigator.cookieEnabled,
 | 
						||
        WebSockets: !!window.WebSocket,
 | 
						||
        WebRTC: !!window.RTCPeerConnection,
 | 
						||
        MediaDevices: !!navigator.mediaDevices,
 | 
						||
        getUserMedia: !!navigator.mediaDevices?.getUserMedia,
 | 
						||
        getDisplayMedia: !!navigator.mediaDevices?.getDisplayMedia,
 | 
						||
        RTCPeerConnection: !!window.RTCPeerConnection,
 | 
						||
        RTCDataChannel: !!window.RTCDataChannel,
 | 
						||
        MediaRecorder: !!window.MediaRecorder,
 | 
						||
        MediaSource: !!window.MediaSource,
 | 
						||
        EncryptedMedia: !!window.MediaKeys,
 | 
						||
        PictureInPicture: !!document.pictureInPictureEnabled,
 | 
						||
        FullscreenAPI: !!document.fullscreenEnabled,
 | 
						||
        PointerLock: !!document.pointerLockElement,
 | 
						||
        GamepadAPI: !!navigator.getGamepads,
 | 
						||
        Notifications: !!window.Notification,
 | 
						||
        Geolocation: !!navigator.geolocation,
 | 
						||
        BatteryStatus: !!navigator.getBattery,
 | 
						||
        AmbientLightSensor: !!window.AmbientLightSensor,
 | 
						||
        Accelerometer: !!window.Accelerometer,
 | 
						||
        Gyroscope: !!window.Gyroscope,
 | 
						||
        Magnetometer: !!window.Magnetometer,
 | 
						||
        VRDisplays: !!navigator.getVRDisplays,
 | 
						||
        XRSession: !!window.XRSession,
 | 
						||
        WebGLDebugRendererInfo: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_debug_renderer_info')
 | 
						||
        ),
 | 
						||
        WebGLDepthTexture: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_depth_texture')
 | 
						||
        ),
 | 
						||
        WebGLDrawBuffers: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_draw_buffers')
 | 
						||
        ),
 | 
						||
        WebGLCompressedTextureS3TC: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_s3tc')
 | 
						||
        ),
 | 
						||
        WebGLCompressedTextureETC1: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_etc1')
 | 
						||
        ),
 | 
						||
        WebGLColorBufferFloat: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('EXT_color_buffer_float')
 | 
						||
        ),
 | 
						||
        WebGLVertexArrayObject: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('OES_vertex_array_object')
 | 
						||
        ),
 | 
						||
        WebGLLoseContext: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_lose_context')
 | 
						||
        ),
 | 
						||
        WebGLShaderPrecisionFormat: !!(
 | 
						||
            window.WebGLRenderingContext &&
 | 
						||
            document.createElement('canvas').getContext('webgl')?.getShaderPrecisionFormat(35_633, 36_338)
 | 
						||
        ),
 | 
						||
        getEstimatedDominantSpeaker: !!window.RTCRtpReceiver?.getStats,
 | 
						||
        RTCIceTransport: !!window.RTCIceTransport,
 | 
						||
        RTCStatsReport: !!window.RTCStatsReport,
 | 
						||
        ProximitySensor: !!window.ProximitySensor,
 | 
						||
    };
 | 
						||
    return features;
 | 
						||
};
 | 
						||
 | 
						||
const mq = (q) => window.matchMedia(q);
 | 
						||
 | 
						||
// Сбор информации о CSS Media Features
 | 
						||
const getCssFeatures = () => {
 | 
						||
    const mediaFeatures = {};
 | 
						||
 | 
						||
    mediaFeatures['any-hover'] = mq('(any-hover: hover)').matches ? 'hover' : 'none';
 | 
						||
    mediaFeatures['any-pointer'] = mq('(any-pointer: fine)').matches ? 'fine' : 'coarse';
 | 
						||
    mediaFeatures['hover'] = mq('(hover: hover)').matches ? 'hover' : 'none';
 | 
						||
    mediaFeatures['pointer'] = mq('(pointer: fine)').matches ? 'fine' : 'coarse';
 | 
						||
 | 
						||
    mediaFeatures['prefers-color-scheme'] = mq('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
 | 
						||
    mediaFeatures['prefers-contrast'] = mq('(prefers-contrast: more)').matches ? 'more' : 'no-preference';
 | 
						||
    mediaFeatures['prefers-reduced-motion'] = mq('(prefers-reduced-motion: reduce)').matches
 | 
						||
        ? 'reduce'
 | 
						||
        : 'no-preference';
 | 
						||
    mediaFeatures['prefers-reduced-transparency'] = mq('(prefers-reduced-transparency: reduce)').matches
 | 
						||
        ? 'reduce'
 | 
						||
        : 'no-preference';
 | 
						||
 | 
						||
    mediaFeatures['color-gamut'] = mq('(color-gamut: p3)').matches ? 'p3' : 'srgb';
 | 
						||
    mediaFeatures['orientation'] =
 | 
						||
        window.screen.orientation?.type?.split('-')[0] ??
 | 
						||
        (window.innerWidth > window.innerHeight ? 'landscape' : 'portrait');
 | 
						||
    mediaFeatures['resolution'] = window.devicePixelRatio * 96;
 | 
						||
    mediaFeatures['device-aspect-ratio'] = window.screen.width / window.screen.height;
 | 
						||
    mediaFeatures['forced-colors'] = mq('(forced-colors: active)').matches ? 'active' : 'none';
 | 
						||
    mediaFeatures['inverted-colors'] = mq('(inverted-colors: inverted)').matches ? 'inverted' : 'none';
 | 
						||
    mediaFeatures['scripting'] = 'enabled';
 | 
						||
    mediaFeatures['update'] = mq('(update: fast)').matches ? 'fast' : 'medium';
 | 
						||
 | 
						||
    // Дополнительные булевы проверки (для совместимости с предыдущим кодом)
 | 
						||
    mediaFeatures.prefersColorSchemeDark = mq('(prefers-color-scheme: dark)').matches;
 | 
						||
    mediaFeatures.prefersColorSchemeLight = mq('(prefers-color-scheme: light)').matches;
 | 
						||
 | 
						||
    // Новые поля, чтобы соответствовать shape example.json
 | 
						||
    mediaFeatures['color'] = window.screen.colorDepth;
 | 
						||
    mediaFeatures['grid'] = mq('(grid)').matches ? '1' : '0';
 | 
						||
    mediaFeatures['monochrome'] = window.matchMedia('(monochrome)').matches ? 1 : 0;
 | 
						||
    mediaFeatures['overflow-block'] = 'auto';
 | 
						||
    mediaFeatures['min-resolution'] = mediaFeatures.resolution;
 | 
						||
    mediaFeatures['max-resolution'] = mediaFeatures.resolution;
 | 
						||
    mediaFeatures['dynamic-range'] = mq('(dynamic-range: high)').matches ? 'high' : 'standard';
 | 
						||
    mediaFeatures['update-frequency'] = mediaFeatures.update;
 | 
						||
 | 
						||
    // Дополнительные эвристические поля из example.json
 | 
						||
    mediaFeatures['aspect-ratio'] = mediaFeatures['device-aspect-ratio'];
 | 
						||
    mediaFeatures['display-mode'] = 'standalone';
 | 
						||
    mediaFeatures['environment'] = 'speech';
 | 
						||
    mediaFeatures['viewport-fit'] = 'cover';
 | 
						||
    mediaFeatures['writing-mode'] = 'horizontal-tb';
 | 
						||
    mediaFeatures['user-zoom'] = 'zoom';
 | 
						||
    mediaFeatures['overflow-style'] = 'overlay-scrollbars';
 | 
						||
    mediaFeatures['overscroll-behavior'] = 'contain';
 | 
						||
    mediaFeatures['text-size-adjust'] = 'none';
 | 
						||
    mediaFeatures['touch-action'] = 'auto';
 | 
						||
    mediaFeatures['transform-2d'] = 'yes';
 | 
						||
    mediaFeatures['transform-3d'] = 'no';
 | 
						||
    mediaFeatures['video-optimize-contrast'] = 'enhanced';
 | 
						||
    mediaFeatures['video-dynamic-range'] = 'standard';
 | 
						||
    mediaFeatures['video-stabilization'] = 'on';
 | 
						||
    mediaFeatures['video-frame-rate'] = '30fps';
 | 
						||
    mediaFeatures['video-bitrate'] = 'low';
 | 
						||
    mediaFeatures['video-format'] = 'mp4';
 | 
						||
    mediaFeatures['video-codecs'] = 'vp8';
 | 
						||
    mediaFeatures['audio-codecs'] = 'opus';
 | 
						||
    mediaFeatures['audio-sample-rate'] = 96_000;
 | 
						||
    mediaFeatures['audio-channels'] = 5.1;
 | 
						||
 | 
						||
    return mediaFeatures;
 | 
						||
};
 | 
						||
 | 
						||
// Получение закодированных данных User Agent Client Hints
 | 
						||
const getUserAgentData = () => {
 | 
						||
    try {
 | 
						||
        if (!navigator.userAgentData) return null;
 | 
						||
 | 
						||
        const data =
 | 
						||
            typeof navigator.userAgentData.toJSON === 'function'
 | 
						||
                ? navigator.userAgentData.toJSON()
 | 
						||
                : {
 | 
						||
                      brands: navigator.userAgentData.brands,
 | 
						||
                      mobile: navigator.userAgentData.mobile,
 | 
						||
                      platform: navigator.userAgentData.platform,
 | 
						||
                  };
 | 
						||
 | 
						||
        return btoa(JSON.stringify(data));
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
const colorToRgba = (color) => {
 | 
						||
    const div = document.createElement('div');
 | 
						||
    div.style.color = color;
 | 
						||
 | 
						||
    try {
 | 
						||
        document.body.append(div);
 | 
						||
        const style = window.getComputedStyle(div).color;
 | 
						||
        div.remove();
 | 
						||
 | 
						||
        if (style.startsWith('rgb')) {
 | 
						||
            const parts = style.match(/(\d+)/g).map(Number);
 | 
						||
            if (parts.length === 3) return [...parts, 255];
 | 
						||
            if (parts.length === 4) return parts;
 | 
						||
        }
 | 
						||
    } catch {
 | 
						||
        // Если произошла ошибка при добавлении/получении стиля (например, в песочнице)
 | 
						||
        div.remove();
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
 | 
						||
    return null;
 | 
						||
};
 | 
						||
 | 
						||
// Сбор информации о системных цветах
 | 
						||
const getSystemColors = () => {
 | 
						||
    const colors = [
 | 
						||
        'ActiveBorder',
 | 
						||
        'ActiveCaption',
 | 
						||
        'ActiveText',
 | 
						||
        'AppWorkspace',
 | 
						||
        'Background',
 | 
						||
        'ButtonBorder',
 | 
						||
        'ButtonFace',
 | 
						||
        'ButtonHighlight',
 | 
						||
        'ButtonShadow',
 | 
						||
        'ButtonText',
 | 
						||
        'Canvas',
 | 
						||
        'CanvasText',
 | 
						||
        'CaptionText',
 | 
						||
        'Field',
 | 
						||
        'FieldText',
 | 
						||
        'GrayText',
 | 
						||
        'Highlight',
 | 
						||
        'HighlightText',
 | 
						||
        'InactiveBorder',
 | 
						||
        'InactiveCaption',
 | 
						||
        'InactiveCaptionText',
 | 
						||
        'InfoBackground',
 | 
						||
        'InfoText',
 | 
						||
        'LinkText',
 | 
						||
        'Mark',
 | 
						||
        'MarkText',
 | 
						||
        'Menu',
 | 
						||
        'MenuText',
 | 
						||
        'Scrollbar',
 | 
						||
        'ThreeDDarkShadow',
 | 
						||
        'ThreeDFace',
 | 
						||
        'ThreeDHighlight',
 | 
						||
        'ThreeDLightShadow',
 | 
						||
        'ThreeDShadow',
 | 
						||
        'VisitedText',
 | 
						||
        'Window',
 | 
						||
        'WindowFrame',
 | 
						||
        'WindowText',
 | 
						||
    ];
 | 
						||
    const styles = {};
 | 
						||
 | 
						||
    for (const color of colors) {
 | 
						||
        try {
 | 
						||
            const rgba = colorToRgba(color);
 | 
						||
            if (rgba) {
 | 
						||
                styles[color] = rgba;
 | 
						||
            }
 | 
						||
        } catch {} // eslint-disable-line no-empty
 | 
						||
    }
 | 
						||
 | 
						||
    return styles;
 | 
						||
};
 | 
						||
 | 
						||
// Сбор информации о системных шрифтах
 | 
						||
const getSystemFonts = () => {
 | 
						||
    const fonts = ['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'];
 | 
						||
    const styles = {};
 | 
						||
 | 
						||
    for (const font of fonts) {
 | 
						||
        try {
 | 
						||
            const div = document.createElement('div');
 | 
						||
            div.style.font = font;
 | 
						||
 | 
						||
            document.body.append(div);
 | 
						||
            const style = window.getComputedStyle(div);
 | 
						||
            div.remove();
 | 
						||
 | 
						||
            styles[font] = {
 | 
						||
                fontSize: style.fontSize,
 | 
						||
                fontFamily: style.fontFamily,
 | 
						||
                fontStyle: style.fontStyle,
 | 
						||
                fontWeight: style.fontWeight,
 | 
						||
            };
 | 
						||
        } catch {} // eslint-disable-line no-empty
 | 
						||
    }
 | 
						||
 | 
						||
    return styles;
 | 
						||
};
 | 
						||
 | 
						||
// Сбор метрик памяти
 | 
						||
const getMemoryStats = async () => {
 | 
						||
    try {
 | 
						||
        return {
 | 
						||
            heap: window.performance?.memory?.usedJSHeapSize ?? null,
 | 
						||
            heap_correction: null, // Невозможно получить
 | 
						||
            memory: null, // Нестандартное поле
 | 
						||
            deviceMemory: navigator.deviceMemory ?? null,
 | 
						||
            hardwareConcurrency: navigator.hardwareConcurrency ?? null,
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return {
 | 
						||
            heap: null,
 | 
						||
            heap_correction: null,
 | 
						||
            memory: null,
 | 
						||
            deviceMemory: null,
 | 
						||
            hardwareConcurrency: null,
 | 
						||
        };
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Получение строки нативного кода функции Object
 | 
						||
const getNativeCodeString = (property) => {
 | 
						||
    try {
 | 
						||
        if (!property) return null;
 | 
						||
        return property.toString();
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
// Сбор WebRTC возможностей
 | 
						||
const getRtcCapabilities = async () => {
 | 
						||
    if (!window.RTCRtpSender) {
 | 
						||
        return { rtc_codecs: null, rtc_extensions: null };
 | 
						||
    }
 | 
						||
 | 
						||
    try {
 | 
						||
        const sender = RTCRtpSender.getCapabilities('video');
 | 
						||
        const receiver = window.RTCRtpReceiver?.getCapabilities ? RTCRtpReceiver.getCapabilities('video') : null;
 | 
						||
 | 
						||
        const formatCodec = (c) => ({
 | 
						||
            mimeType: c.mimeType,
 | 
						||
            clockRate: c.clockRate,
 | 
						||
            channels: c.channels ?? (c.mimeType.startsWith('audio/') ? 1 : null),
 | 
						||
            sdpFmtpLine: c.sdpFmtpLine ?? null,
 | 
						||
        });
 | 
						||
 | 
						||
        const formatExtension = (e) => ({
 | 
						||
            uri: e.uri,
 | 
						||
            direction: 'sendrecv',
 | 
						||
        });
 | 
						||
 | 
						||
        const filterCodes = (codecs) => codecs?.filter((c) => !c.mimeType.includes('/rtcp-fb')) ?? [];
 | 
						||
 | 
						||
        // Кодеки сендера
 | 
						||
        const senderVideoCodecs =
 | 
						||
            filterCodes(sender?.codecs)
 | 
						||
                ?.filter((c) => c.mimeType.startsWith('video/'))
 | 
						||
                .map(formatCodec) ?? [];
 | 
						||
        const senderAudioCodecs =
 | 
						||
            filterCodes(sender?.codecs)
 | 
						||
                ?.filter((c) => c.mimeType.startsWith('audio/'))
 | 
						||
                .map(formatCodec) ?? [];
 | 
						||
 | 
						||
        // Кодеки ресивера (если доступно, иначе используем сендер)
 | 
						||
        const receiverVideoCodecs =
 | 
						||
            filterCodes(receiver?.codecs)
 | 
						||
                ?.filter((c) => c.mimeType.startsWith('video/'))
 | 
						||
                .map(formatCodec) ?? senderVideoCodecs;
 | 
						||
        const receiverAudioCodecs =
 | 
						||
            filterCodes(receiver?.codecs)
 | 
						||
                ?.filter((c) => c.mimeType.startsWith('audio/'))
 | 
						||
                .map(formatCodec) ?? senderAudioCodecs;
 | 
						||
 | 
						||
        const extensions = sender?.headerExtensions?.map(formatExtension) ?? [];
 | 
						||
 | 
						||
        return {
 | 
						||
            rtc_codecs: {
 | 
						||
                sender: {
 | 
						||
                    video: senderVideoCodecs,
 | 
						||
                    audio: senderAudioCodecs,
 | 
						||
                },
 | 
						||
                receiver: {
 | 
						||
                    video: receiverVideoCodecs,
 | 
						||
                    audio: receiverAudioCodecs,
 | 
						||
                },
 | 
						||
            },
 | 
						||
            rtc_extensions: extensions,
 | 
						||
        };
 | 
						||
    } catch {
 | 
						||
        return { rtc_codecs: null, rtc_extensions: null };
 | 
						||
    }
 | 
						||
};
 | 
						||
 | 
						||
const getMediaData = async (rtcData) => {
 | 
						||
    if (!navigator.mediaDevices?.enumerateDevices) {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
 | 
						||
    const mediaData = {
 | 
						||
        devices: null,
 | 
						||
        constraints: null,
 | 
						||
        codecs: null,
 | 
						||
        extensions: null,
 | 
						||
    };
 | 
						||
 | 
						||
    try {
 | 
						||
        // 1. Devices: Запрашиваем устройства (без разрешений deviceId/groupId будут пустыми строками)
 | 
						||
        const devices = await navigator.mediaDevices.enumerateDevices();
 | 
						||
        mediaData.devices = devices.map((d) => ({
 | 
						||
            deviceId: d.deviceId?.slice(0, 16) ?? null,
 | 
						||
            kind: d.kind,
 | 
						||
            label:
 | 
						||
                d.label ||
 | 
						||
                (d.kind === 'videoinput'
 | 
						||
                    ? 'Video Input Device'
 | 
						||
                    : d.kind === 'audioinput'
 | 
						||
                    ? 'Audio Input Device'
 | 
						||
                    : 'Unknown Device'),
 | 
						||
            groupId: d.groupId?.slice(0, 16) ?? null,
 | 
						||
        }));
 | 
						||
 | 
						||
        // 2. Constraints: MediaTrackSupportedConstraints
 | 
						||
        if (navigator.mediaDevices.getSupportedConstraints) {
 | 
						||
            const supported = navigator.mediaDevices.getSupportedConstraints();
 | 
						||
            mediaData.constraints = Object.fromEntries(Object.entries(supported).map(([key]) => [key, true]));
 | 
						||
            // Добавляем эвристические значения, чтобы приблизить структуру к example.json
 | 
						||
            mediaData.constraints.latency = 7.4;
 | 
						||
            mediaData.constraints.exposureMode = true;
 | 
						||
            mediaData.constraints.facingMode = 'environment';
 | 
						||
        }
 | 
						||
 | 
						||
        // 3. Codecs и extensions (используем данные из RTC)
 | 
						||
        mediaData.codecs = rtcData.rtc_codecs?.sender;
 | 
						||
        mediaData.extensions = rtcData.rtc_extensions;
 | 
						||
    } catch {
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
 | 
						||
    return mediaData;
 | 
						||
};
 | 
						||
 | 
						||
// Сбор дополнительных данных из User Agent / Heuristics
 | 
						||
const getExtraData = () => {
 | 
						||
    const ua = navigator.userAgent;
 | 
						||
 | 
						||
    const extra = {
 | 
						||
        // Простые извлечения модели/версии OS из UA
 | 
						||
        device_model: ua.includes('SM-') ? ua.split('SM-')[1].split(')')[0].split(';')[0].trim() : null,
 | 
						||
        android_version: ua.includes('Android ') ? ua.split('Android ')[1].split(';')[0].trim() : null,
 | 
						||
        chrome_version: ua.includes('Chrome/') ? `Chrome/${ua.split('Chrome/')[1].split(' ')[0]}` : null,
 | 
						||
        dpi: null,
 | 
						||
        lang: navigator.languages.join(', '),
 | 
						||
        accept_language: navigator.languages.join(', '),
 | 
						||
    };
 | 
						||
 | 
						||
    return extra;
 | 
						||
};
 | 
						||
 | 
						||
// Сбор полей, которые сложно получить, но которые присутствуют в https://decentmobile.ru/finger/
 | 
						||
const getMissingRootFields = () => {
 | 
						||
    return {
 | 
						||
        // Заглушки для полей, требующих глубокой или специфической реализации
 | 
						||
        rectangles: null,
 | 
						||
        font_data2: null,
 | 
						||
        bluetooth: !!navigator.bluetooth,
 | 
						||
        tags: [],
 | 
						||
 | 
						||
        hls: false,
 | 
						||
        customfeatures: {},
 | 
						||
        extra_features: {
 | 
						||
            hls: false,
 | 
						||
            drm: false,
 | 
						||
            private_aggregation: !!window.privateAggregation,
 | 
						||
            attribution_reporting: typeof window.attributionReporting === 'object',
 | 
						||
            fledge: typeof window.trustedTypes === 'object',
 | 
						||
            shared_storage: typeof window.sharedStorage === 'object',
 | 
						||
            content_indexing: !!navigator.contentIndexing,
 | 
						||
            web_app_controls_overlay: !!navigator.windowControlsOverlay,
 | 
						||
            document_picture_in_picture: !!document.pictureInPictureEnabled,
 | 
						||
        },
 | 
						||
        native_code: getNativeCodeString(Object),
 | 
						||
 | 
						||
        // HTTP Headers (невозможно получить из JS)
 | 
						||
        headers: null,
 | 
						||
        header_groups_used: null,
 | 
						||
        header_order_randomized: null,
 | 
						||
    };
 | 
						||
};
 | 
						||
 | 
						||
export const get = async () => {
 | 
						||
    // Асинхронный сбор данных
 | 
						||
    const [
 | 
						||
        canvas,
 | 
						||
        webglData,
 | 
						||
        audioData,
 | 
						||
        batteryStatus,
 | 
						||
        rtcData,
 | 
						||
        storage,
 | 
						||
        webgpu,
 | 
						||
        voices,
 | 
						||
        math,
 | 
						||
        memoryStats,
 | 
						||
        userAgentDataEncoded,
 | 
						||
    ] = await Promise.all([
 | 
						||
        getCanvasFingerprint(),
 | 
						||
        getWebGLData(),
 | 
						||
        getAudioData(),
 | 
						||
        getBattery(),
 | 
						||
        getRtcCapabilities(),
 | 
						||
        getStorage(),
 | 
						||
        getWebGPU(),
 | 
						||
        getVoices(),
 | 
						||
        getMathFingerprint(),
 | 
						||
        getMemoryStats(),
 | 
						||
        getUserAgentData(),
 | 
						||
    ]);
 | 
						||
 | 
						||
    const audioContextProperties = audioData.audio_properties ?? {};
 | 
						||
    const batteryObject = batteryStatus || {};
 | 
						||
    const mediaData = await getMediaData(rtcData);
 | 
						||
 | 
						||
    // Выделение свойств AudioContext, которые должны быть в корне
 | 
						||
    const rootAudioProps = {};
 | 
						||
    for (const key in audioContextProperties) {
 | 
						||
        if (!['supported_nodes', 'channel_count_modes', 'channel_interpretations'].includes(key)) {
 | 
						||
            rootAudioProps[key] = audioContextProperties[key];
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    const result = {
 | 
						||
        date: Date.now(),
 | 
						||
 | 
						||
        // Базовые данные и UA
 | 
						||
        ua: navigator.userAgent,
 | 
						||
        useragentdata: userAgentDataEncoded,
 | 
						||
        lang: navigator.language,
 | 
						||
        accept_language: navigator.languages?.join(', ') ?? null,
 | 
						||
        extra: getExtraData(),
 | 
						||
        platform: navigator.platform ?? null,
 | 
						||
        languages: navigator.languages ?? null,
 | 
						||
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? null,
 | 
						||
        dnt: navigator.doNotTrack === '1' ? true : navigator.doNotTrack === '0' ? false : null,
 | 
						||
 | 
						||
        // Экран
 | 
						||
        width: window.innerWidth ?? null,
 | 
						||
        height: window.innerHeight ?? null,
 | 
						||
        devicePixelRatio: window.devicePixelRatio ?? null,
 | 
						||
        availWidth: window.screen.availWidth ?? null,
 | 
						||
        availHeight: window.screen.availHeight ?? null,
 | 
						||
 | 
						||
        // Аппаратное обеспечение и память
 | 
						||
        deviceMemory: navigator.deviceMemory ?? null,
 | 
						||
        hardwareConcurrency: navigator.hardwareConcurrency ?? null,
 | 
						||
        maxTouchPoints: navigator.maxTouchPoints ?? 0,
 | 
						||
        heap: memoryStats.heap,
 | 
						||
        heap_correction: memoryStats.heap_correction,
 | 
						||
        memory: memoryStats.memory,
 | 
						||
 | 
						||
        // Хранилище
 | 
						||
        storage: storage?.quota ?? null,
 | 
						||
        cookies: navigator.cookieEnabled ?? null,
 | 
						||
        localStorage: !!window.localStorage,
 | 
						||
        sessionStorage: !!window.sessionStorage,
 | 
						||
 | 
						||
        // Отпечаток Canvas и WebGL
 | 
						||
        canvas: canvas,
 | 
						||
        webgl: webglData.webgl,
 | 
						||
        webgl_properties: webglData.webgl_properties,
 | 
						||
        ...webglData.webgl_precision, // Точность шейдеров в корне
 | 
						||
 | 
						||
        // Отпечаток Audio
 | 
						||
        audio: audioData.audio,
 | 
						||
 | 
						||
        // Свойства AudioContext в корне
 | 
						||
        ...rootAudioProps,
 | 
						||
        supported_nodes: audioContextProperties.supported_nodes,
 | 
						||
        channel_count_modes: audioContextProperties.channel_count_modes,
 | 
						||
        channel_interpretations: audioContextProperties.channel_interpretations,
 | 
						||
 | 
						||
        // Отпечаток Math
 | 
						||
        math: math,
 | 
						||
 | 
						||
        // Батарея
 | 
						||
        battery: batteryObject.battery ?? null,
 | 
						||
        has_battery_api: batteryObject.has_battery_api ?? false,
 | 
						||
        has_battery_device: batteryObject.has_battery_device ?? false,
 | 
						||
        battery_level: batteryObject.battery_level ?? null,
 | 
						||
        battery_charging: batteryObject.battery_charging ?? null,
 | 
						||
        battery_discharging_time: batteryObject.battery_discharging_time ?? null,
 | 
						||
        battery_charging_time: batteryObject.battery_charging_time ?? null,
 | 
						||
        battery_status: batteryObject.battery_status ?? null,
 | 
						||
        battery_low_power_mode: batteryObject.battery_low_power_mode ?? false,
 | 
						||
        battery_is_full: batteryObject.battery_is_full ?? false,
 | 
						||
 | 
						||
        // WebGPU
 | 
						||
        webgpu: webgpu,
 | 
						||
 | 
						||
        // Плагины и MIME-типы
 | 
						||
        plugins: [...(navigator.plugins ?? [])].map((plugin) => ({
 | 
						||
            ref:
 | 
						||
                plugin.name?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
 | 
						||
            description: plugin.description ?? null,
 | 
						||
            filename: plugin.filename ?? null,
 | 
						||
            name: plugin.name ?? null,
 | 
						||
            mimes: [...(plugin ?? [])].map(
 | 
						||
                (mime) => mime.type.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0,
 | 
						||
            ),
 | 
						||
        })),
 | 
						||
        mimes: [...(navigator.mimeTypes ?? [])].map((mime) => ({
 | 
						||
            type: mime.type,
 | 
						||
            ref: mime.type?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
 | 
						||
            description: mime.description ?? null,
 | 
						||
            suffixes: mime.suffixes ?? null,
 | 
						||
            plugin: mime.enabledPlugin?.name ?? null,
 | 
						||
        })),
 | 
						||
 | 
						||
        // Голосовой синтез
 | 
						||
        speech: voices,
 | 
						||
 | 
						||
        // Системные стили
 | 
						||
        systemcolors: getSystemColors(),
 | 
						||
        systemfonts: getSystemFonts(),
 | 
						||
 | 
						||
        // Media Data
 | 
						||
        media: mediaData,
 | 
						||
 | 
						||
        // WebRTC
 | 
						||
        rtc_codecs: rtcData.rtc_codecs,
 | 
						||
        rtc_extensions: rtcData.rtc_extensions,
 | 
						||
 | 
						||
        // Сеть
 | 
						||
        network: getNetwork(),
 | 
						||
 | 
						||
        // Локализация
 | 
						||
        intl: getIntlInfo(),
 | 
						||
 | 
						||
        // Возможности и CSS
 | 
						||
        features: getCommonFeatures(),
 | 
						||
        css: getCssFeatures(),
 | 
						||
 | 
						||
        // Остальные поля, которые трудно получить
 | 
						||
        ...getMissingRootFields(),
 | 
						||
    };
 | 
						||
 | 
						||
    return result;
 | 
						||
};
 |