@ipranker/sdk
Version:
Professional IP Intelligence and Device Fingerprinting SDK - Comprehensive fraud detection with single API call
1 lines • 60.4 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/utils/hash.ts","../src/utils/fontDetector.ts","../src/collectors/FingerprintCollector.ts","../src/collectors/behaviorCollector.ts","../src/collectors/deviceCollector.ts","../src/api/client.ts","../src/IPRanker.ts"],"sourcesContent":["/**\r\n * Hashing utilities for fingerprinting\r\n */\r\n\r\n/**\r\n * Hash entropy using SHA-256 (or fallback to simple hash)\r\n */\r\nexport async function hashEntropy(entropy: any): Promise<string> {\r\n const entropyString = JSON.stringify(entropy);\r\n\r\n // Try to use SubtleCrypto (SHA-256) if available\r\n if (window.crypto && window.crypto.subtle) {\r\n try {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(entropyString);\r\n const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);\r\n const hashArray = Array.from(new Uint8Array(hashBuffer));\r\n return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\r\n } catch (e) {\r\n // Fall through to fallback hash\r\n }\r\n }\r\n\r\n // Fallback: Simple hash function\r\n let hash = 0;\r\n for (let i = 0; i < entropyString.length; i++) {\r\n const char = entropyString.charCodeAt(i);\r\n hash = ((hash << 5) - hash) + char;\r\n hash = hash & hash; // Convert to 32-bit integer\r\n }\r\n return Math.abs(hash).toString(36) + Date.now().toString(36);\r\n}\r\n\r\n/**\r\n * Simple string hash function (murmurhash-like)\r\n */\r\nexport function simpleHash(str: string): string {\r\n let h = 0;\r\n for (let i = 0; i < str.length; i++) {\r\n h = Math.imul(31, h) + str.charCodeAt(i) | 0;\r\n }\r\n return Math.abs(h).toString(36);\r\n}\r\n","/**\r\n * @internal\r\n */\r\n\r\nimport { simpleHash } from './hash';\r\n\r\nconst TEST_FONTS = [\r\n 'Arial', 'Arial Black', 'Arial Narrow', 'Calibri', 'Calibri Light', 'Cambria',\r\n 'Candara', 'Century Gothic', 'Comic Sans MS', 'Consolas', 'Constantia', 'Corbel',\r\n 'Courier New', 'Ebrima', 'Franklin Gothic Medium', 'Gabriola', 'Gadugi', 'Georgia',\r\n 'Impact', 'Ink Free', 'Javanese Text', 'Leelawadee UI', 'Lucida Console',\r\n 'Lucida Sans Unicode', 'Malgun Gothic', 'Marlett', 'Microsoft Himalaya',\r\n 'Microsoft JhengHei', 'Microsoft New Tai Lue', 'Microsoft PhagsPa',\r\n 'Microsoft Sans Serif', 'Microsoft Tai Le', 'Microsoft YaHei', 'Microsoft Yi Baiti',\r\n 'MingLiU-ExtB', 'Mongolian Baiti', 'MS Gothic', 'MS PGothic', 'MS UI Gothic',\r\n 'MV Boli', 'Myanmar Text', 'Nirmala UI', 'Palatino Linotype', 'Segoe MDL2 Assets',\r\n 'Segoe Print', 'Segoe Script', 'Segoe UI', 'Segoe UI Emoji', 'Segoe UI Historic',\r\n 'Segoe UI Light', 'Segoe UI Semibold', 'Segoe UI Symbol', 'SimSun', 'Sitka Banner',\r\n 'Sitka Display', 'Sitka Heading', 'Sitka Small', 'Sitka Subheading', 'Sitka Text',\r\n 'Sylfaen', 'Symbol', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',\r\n 'Webdings', 'Wingdings', 'Yu Gothic', 'Yu Gothic UI',\r\n 'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',\r\n 'Apple SD Gothic Neo', 'AppleGothic', 'AppleMyungjo', 'Avenir', 'Avenir Next',\r\n 'Avenir Next Condensed', 'Baskerville', 'Big Caslon', 'Bodoni 72', 'Bodoni 72 Oldstyle',\r\n 'Bodoni 72 Smallcaps', 'Bradley Hand', 'Brush Script MT', 'Chalkboard', 'Chalkboard SE',\r\n 'Chalkduster', 'Charter', 'Cochin', 'Copperplate', 'Corsiva Hebrew', 'Courier',\r\n 'DIN Alternate', 'DIN Condensed', 'Didot', 'Euphemia UCAS', 'Futura', 'Geneva',\r\n 'Gill Sans', 'Helvetica', 'Helvetica Neue', 'Herculanum', 'Hiragino Kaku Gothic Pro',\r\n 'Hiragino Mincho Pro', 'Hoefler Text', 'Kailasa', 'Kannada Sangam MN', 'Khmer Sangam MN',\r\n 'Kohinoor Bangla', 'Kohinoor Devanagari', 'Kohinoor Telugu', 'Lao Sangam MN',\r\n 'Lucida Grande', 'Luminari', 'Malayalam Sangam MN', 'Marker Felt', 'Menlo',\r\n 'Monaco', 'Noteworthy', 'Optima', 'Oriya Sangam MN', 'Osaka', 'Palatino', 'Papyrus',\r\n 'Phosphate', 'PingFang HK', 'PingFang SC', 'PingFang TC', 'Plantagenet Cherokee',\r\n 'PT Mono', 'PT Sans', 'PT Sans Caption', 'PT Sans Narrow', 'PT Serif', 'PT Serif Caption',\r\n 'Rockwell', 'Savoye LET', 'SignPainter', 'Sinhala Sangam MN', 'Skia', 'Snell Roundhand',\r\n 'STIXGeneral', 'Superclarendon', 'Tamil Sangam MN', 'Telugu Sangam MN', 'Times',\r\n 'Trattatello', 'Zapfino',\r\n 'Ubuntu', 'Ubuntu Condensed', 'Ubuntu Light', 'Ubuntu Mono', 'DejaVu Sans',\r\n 'DejaVu Sans Mono', 'DejaVu Serif', 'Liberation Mono', 'Liberation Sans',\r\n 'Liberation Serif', 'Nimbus Mono L', 'Nimbus Roman No9 L', 'Nimbus Sans L',\r\n 'FreeMono', 'FreeSans', 'FreeSerif', 'Noto Sans', 'Noto Serif', 'Noto Mono',\r\n 'Droid Sans', 'Droid Sans Mono', 'Droid Serif',\r\n 'Open Sans', 'Roboto', 'Roboto Condensed', 'Roboto Mono', 'Roboto Slab', 'Lato',\r\n 'Montserrat', 'Source Sans Pro', 'Source Code Pro', 'Source Serif Pro', 'Oswald',\r\n 'Raleway', 'Merriweather', 'Nunito', 'Poppins', 'Playfair Display', 'Quicksand',\r\n 'Rubik', 'Work Sans', 'Fira Sans', 'Fira Mono', 'Fira Code', 'Barlow',\r\n 'Barlow Condensed', 'Inter', 'Manrope', 'DM Sans', 'DM Serif Display',\r\n 'IBM Plex Sans', 'IBM Plex Mono', 'JetBrains Mono', 'Inconsolata', 'Space Mono',\r\n 'Space Grotesk'\r\n];\r\n\r\nconst BASE_FONTS = ['monospace', 'sans-serif', 'serif'];\r\nconst TEST_STRING = 'mmmmmmmmmmlli';\r\nconst TEST_SIZE = '72px';\r\n\r\nexport interface FontFingerprintData {\r\n installedFonts: string[];\r\n fontCount: number;\r\n fontFingerprint: string;\r\n detectionTimeMs: number;\r\n}\r\n\r\nexport async function detectFonts(): Promise<FontFingerprintData> {\r\n const startTime = performance.now();\r\n const installedFonts: string[] = [];\r\n\r\n try {\r\n const canvas = document.createElement('canvas');\r\n const ctx = canvas.getContext('2d');\r\n\r\n if (!ctx) {\r\n return {\r\n installedFonts: [],\r\n fontCount: 0,\r\n fontFingerprint: 'unavailable',\r\n detectionTimeMs: performance.now() - startTime\r\n };\r\n }\r\n\r\n const baseWidths: Record<string, number> = {};\r\n for (const baseFont of BASE_FONTS) {\r\n ctx.font = `${TEST_SIZE} ${baseFont}`;\r\n baseWidths[baseFont] = ctx.measureText(TEST_STRING).width;\r\n }\r\n\r\n for (const font of TEST_FONTS) {\r\n let detected = false;\r\n for (const baseFont of BASE_FONTS) {\r\n ctx.font = `${TEST_SIZE} \"${font}\", ${baseFont}`;\r\n if (ctx.measureText(TEST_STRING).width !== baseWidths[baseFont]) {\r\n detected = true;\r\n break;\r\n }\r\n }\r\n if (detected) installedFonts.push(font);\r\n }\r\n\r\n return {\r\n installedFonts,\r\n fontCount: installedFonts.length,\r\n fontFingerprint: simpleHash(installedFonts.sort().join('|')),\r\n detectionTimeMs: performance.now() - startTime\r\n };\r\n\r\n } catch {\r\n return {\r\n installedFonts: [],\r\n fontCount: 0,\r\n fontFingerprint: 'error',\r\n detectionTimeMs: performance.now() - startTime\r\n };\r\n }\r\n}\r\n\r\nexport default detectFonts;\r\n","/**\r\n * @internal\r\n */\r\n\r\nimport type {\r\n FingerprintData,\r\n FontFingerprintData,\r\n WebRTCFingerprintData,\r\n SensorFingerprintData,\r\n AdvancedBrowserData,\r\n HardwareData,\r\n PermissionsData\r\n} from '../types';\r\nimport { detectFonts } from '../utils/fontDetector';\r\nimport { simpleHash } from '../utils/hash';\r\n\r\nexport class FingerprintCollector {\r\n private readonly fingerprintEndpoint: string;\r\n private readonly apiKey?: string;\r\n\r\n constructor(fingerprintEndpoint: string = 'https://fingerprint.ipranker.com/fingerprint-visitor-id', apiKey?: string) {\r\n this.fingerprintEndpoint = fingerprintEndpoint;\r\n this.apiKey = apiKey;\r\n }\r\n\r\n async collect(): Promise<FingerprintData> {\r\n try {\r\n const completeDeviceData = await this.collectCompleteDeviceData();\r\n\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n 'Accept': 'application/json'\r\n };\r\n\r\n if (this.apiKey) {\r\n headers['x-api-key'] = this.apiKey;\r\n }\r\n\r\n const response = await fetch(this.fingerprintEndpoint, {\r\n method: 'POST',\r\n headers,\r\n credentials: 'omit',\r\n body: JSON.stringify({ deviceData: completeDeviceData })\r\n });\r\n\r\n if (!response.ok) {\r\n return await this.collectBasicFingerprint();\r\n }\r\n\r\n const fingerprintData = await response.json();\r\n\r\n if (!fingerprintData.visitorId && !fingerprintData.fingerprints?.installationId) {\r\n return await this.collectBasicFingerprint();\r\n }\r\n\r\n return this.transformFingerprintResponse(fingerprintData);\r\n\r\n } catch (error) {\r\n return await this.collectBasicFingerprint();\r\n }\r\n }\r\n\r\n private transformFingerprintResponse(data: any): FingerprintData {\r\n const baseFingerprint: FingerprintData = {\r\n installationId: data.visitorId || data.fingerprints?.installationId || data.installationId || null,\r\n canvas: {\r\n text: data.fingerprints?.canvasText || data.canvas?.text || null,\r\n geometry: data.fingerprints?.canvasGeometry || data.canvas?.geometry || null,\r\n blending: data.fingerprints?.canvasBlending || data.canvas?.blending || null\r\n },\r\n webgl: data.fingerprints?.webgl || data.webgl ? {\r\n vendor: (data.fingerprints?.webgl?.vendor || data.webgl?.vendor) || null,\r\n renderer: (data.fingerprints?.webgl?.renderer || data.webgl?.renderer) || null,\r\n extensions: (data.fingerprints?.webgl?.extensions?.length || data.webgl?.extensions?.length || 0)\r\n } : { vendor: null, renderer: null, extensions: 0 },\r\n audio: data.fingerprints?.audio || data.audio || null,\r\n screen: {\r\n width: data.screen?.width || window.screen.width,\r\n height: data.screen?.height || window.screen.height,\r\n colorDepth: data.screen?.colorDepth || window.screen.colorDepth,\r\n pixelRatio: data.screen?.pixelRatio || window.devicePixelRatio,\r\n orientation: data.screen?.orientation || screen.orientation?.type || (window as any).orientation || 'unknown'\r\n },\r\n timezone: data.localization?.timeZone || data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,\r\n language: data.localization?.languages?.[0] || data.language || navigator.language,\r\n platform: data.platform?.platform || data.platform || navigator.platform\r\n };\r\n\r\n if (data.fonts) baseFingerprint.fonts = data.fonts;\r\n if (data.webrtc) baseFingerprint.webrtc = data.webrtc;\r\n if (data.sensors) baseFingerprint.sensors = data.sensors;\r\n if (data.advancedBrowser) baseFingerprint.advancedBrowser = data.advancedBrowser;\r\n if (data.hardware) baseFingerprint.hardware = data.hardware;\r\n if (data.permissions) baseFingerprint.permissions = data.permissions;\r\n\r\n return baseFingerprint;\r\n }\r\n\r\n private async collectBasicFingerprint(): Promise<FingerprintData> {\r\n const [fontData, webrtcData] = await Promise.all([\r\n this.collectFontFingerprint(),\r\n this.collectWebRTCFingerprint()\r\n ]);\r\n\r\n const sensorData = this.collectSensorFingerprint();\r\n const advancedBrowserData = this.collectAdvancedBrowserFeatures();\r\n const hardwareData = this.collectHardwareData();\r\n const permissionsData = await this.collectPermissionsState();\r\n const webglData = this.getWebGLFingerprint();\r\n\r\n return {\r\n installationId: null,\r\n canvas: {\r\n text: this.getCanvasFingerprint(),\r\n geometry: this.getCanvasGeometryFingerprint(),\r\n blending: this.getCanvasBlendingFingerprint()\r\n },\r\n webgl: webglData ? {\r\n vendor: webglData.vendor || null,\r\n renderer: webglData.renderer || null,\r\n extensions: webglData.extensions || 0\r\n } : { vendor: null, renderer: null, extensions: 0 },\r\n audio: null,\r\n screen: {\r\n width: window.screen.width,\r\n height: window.screen.height,\r\n colorDepth: window.screen.colorDepth,\r\n pixelRatio: window.devicePixelRatio,\r\n orientation: screen.orientation?.type || (window as any).orientation || 'unknown'\r\n },\r\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\r\n language: navigator.language,\r\n platform: navigator.platform,\r\n fonts: fontData,\r\n webrtc: webrtcData,\r\n sensors: sensorData,\r\n advancedBrowser: advancedBrowserData,\r\n hardware: hardwareData,\r\n permissions: permissionsData\r\n };\r\n }\r\n\r\n private async collectCompleteDeviceData(): Promise<any> {\r\n const userAgent = navigator.userAgent;\r\n const platform = navigator.platform;\r\n\r\n const [fontData, webrtcData] = await Promise.all([\r\n this.collectFontFingerprint(),\r\n this.collectWebRTCFingerprint()\r\n ]);\r\n\r\n const sensorData = this.collectSensorFingerprint();\r\n const advancedBrowserData = this.collectAdvancedBrowserFeatures();\r\n const hardwareData = this.collectHardwareData();\r\n const permissionsData = await this.collectPermissionsState();\r\n const webglData = this.getWebGLFingerprint();\r\n\r\n return {\r\n platform: {\r\n brands: (navigator as any).userAgentData?.brands || [],\r\n platform: platform,\r\n mobile: /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent),\r\n architecture: (navigator as any).userAgentData?.architecture || 'unknown',\r\n bitness: (navigator as any).userAgentData?.bitness || 'unknown',\r\n browserInfo: {\r\n userAgent: userAgent,\r\n language: navigator.language,\r\n languages: navigator.languages || [navigator.language]\r\n }\r\n },\r\n screen: {\r\n width: window.screen.width,\r\n height: window.screen.height,\r\n colorDepth: window.screen.colorDepth,\r\n pixelDepth: window.screen.pixelDepth || window.screen.colorDepth,\r\n orientation: screen.orientation?.type || (window as any).orientation || 'portrait-primary',\r\n pixelRatio: window.devicePixelRatio || 1\r\n },\r\n hardware: hardwareData,\r\n capabilities: {\r\n cookiesEnabled: navigator.cookieEnabled,\r\n doNotTrack: navigator.doNotTrack === '1' || (window as any).doNotTrack === '1',\r\n pdfViewerEnabled: (navigator as any).pdfViewerEnabled ?? true\r\n },\r\n localization: {\r\n languages: navigator.languages || [navigator.language],\r\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone\r\n },\r\n fingerprints: {\r\n canvasText: this.getCanvasFingerprint(),\r\n canvasBlending: this.getCanvasBlendingFingerprint(),\r\n canvasGeometry: this.getCanvasGeometryFingerprint(),\r\n webgl: webglData,\r\n timingResolution: typeof performance !== 'undefined' && typeof performance.now === 'function' ? 'high' : 'low'\r\n },\r\n fonts: { fontCount: fontData.fontCount, fontFingerprint: fontData.fontFingerprint },\r\n webrtc: webrtcData,\r\n sensors: sensorData,\r\n advancedBrowser: advancedBrowserData,\r\n permissions: permissionsData\r\n };\r\n }\r\n\r\n private async collectFontFingerprint(): Promise<FontFingerprintData> {\r\n try {\r\n return await detectFonts();\r\n } catch {\r\n return { installedFonts: [], fontCount: 0, fontFingerprint: 'error', detectionTimeMs: 0 };\r\n }\r\n }\r\n\r\n private async collectWebRTCFingerprint(): Promise<WebRTCFingerprintData> {\r\n const result: WebRTCFingerprintData = {\r\n webrtcSupported: !!window.RTCPeerConnection,\r\n mediaDevicesCount: 0,\r\n hasCamera: false,\r\n hasMicrophone: false,\r\n hasSpeaker: false,\r\n webrtcFingerprint: ''\r\n };\r\n\r\n try {\r\n if (navigator.mediaDevices?.enumerateDevices) {\r\n const devices = await navigator.mediaDevices.enumerateDevices();\r\n result.mediaDevicesCount = devices.length;\r\n result.hasCamera = devices.some(d => d.kind === 'videoinput');\r\n result.hasMicrophone = devices.some(d => d.kind === 'audioinput');\r\n result.hasSpeaker = devices.some(d => d.kind === 'audiooutput');\r\n }\r\n } catch { /* silent */ }\r\n\r\n result.webrtcFingerprint = simpleHash(JSON.stringify({\r\n supported: result.webrtcSupported,\r\n devices: result.mediaDevicesCount,\r\n camera: result.hasCamera,\r\n mic: result.hasMicrophone,\r\n speaker: result.hasSpeaker\r\n }));\r\n\r\n return result;\r\n }\r\n\r\n private collectSensorFingerprint(): SensorFingerprintData {\r\n const result: SensorFingerprintData = {\r\n accelerometerSupported: 'Accelerometer' in window,\r\n gyroscopeSupported: 'Gyroscope' in window,\r\n magnetometerSupported: 'Magnetometer' in window,\r\n ambientLightSupported: 'AmbientLightSensor' in window,\r\n sensorFingerprint: ''\r\n };\r\n\r\n result.sensorFingerprint = simpleHash(JSON.stringify({\r\n accel: result.accelerometerSupported,\r\n gyro: result.gyroscopeSupported,\r\n mag: result.magnetometerSupported,\r\n light: result.ambientLightSupported\r\n }));\r\n\r\n return result;\r\n }\r\n\r\n private collectAdvancedBrowserFeatures(): AdvancedBrowserData {\r\n const result: AdvancedBrowserData = {\r\n localStorageEnabled: this.isStorageAvailable('localStorage'),\r\n sessionStorageEnabled: this.isStorageAvailable('sessionStorage'),\r\n indexedDBEnabled: !!window.indexedDB,\r\n cacheAPIEnabled: 'caches' in window,\r\n webGL2Supported: !!document.createElement('canvas').getContext('webgl2'),\r\n webAssemblySupported: typeof WebAssembly === 'object',\r\n serviceWorkerSupported: 'serviceWorker' in navigator,\r\n doNotTrack: navigator.doNotTrack === '1' || (window as any).doNotTrack === '1',\r\n pdfViewerEnabled: (navigator as any).pdfViewerEnabled ?? true,\r\n browserFeaturesFingerprint: ''\r\n };\r\n\r\n result.browserFeaturesFingerprint = simpleHash(JSON.stringify({\r\n ls: result.localStorageEnabled,\r\n ss: result.sessionStorageEnabled,\r\n idb: result.indexedDBEnabled,\r\n cache: result.cacheAPIEnabled,\r\n webgl2: result.webGL2Supported,\r\n wasm: result.webAssemblySupported,\r\n sw: result.serviceWorkerSupported,\r\n dnt: result.doNotTrack,\r\n pdf: result.pdfViewerEnabled\r\n }));\r\n\r\n return result;\r\n }\r\n\r\n private isStorageAvailable(type: 'localStorage' | 'sessionStorage'): boolean {\r\n try {\r\n const storage = window[type];\r\n const testKey = '__t__';\r\n storage.setItem(testKey, testKey);\r\n storage.removeItem(testKey);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n private collectHardwareData(): HardwareData {\r\n const webglData = this.getWebGLFingerprint();\r\n return {\r\n cores: navigator.hardwareConcurrency || 4,\r\n memory: (navigator as any).deviceMemory || null,\r\n maxTouchPoints: navigator.maxTouchPoints || 0,\r\n gpuVendor: webglData?.vendor || null,\r\n gpuRenderer: webglData?.renderer || null\r\n };\r\n }\r\n\r\n private async collectPermissionsState(): Promise<PermissionsData> {\r\n const result: PermissionsData = { geolocation: 1, notifications: 1, camera: 1, microphone: 1 };\r\n\r\n const encodeState = (state: string): number => {\r\n switch (state) {\r\n case 'granted': return 2;\r\n case 'prompt': return 1;\r\n case 'denied': return 0;\r\n default: return 1;\r\n }\r\n };\r\n\r\n try {\r\n if (navigator.permissions) {\r\n const names = ['geolocation', 'notifications', 'camera', 'microphone'] as const;\r\n for (const name of names) {\r\n try {\r\n const status = await navigator.permissions.query({ name: name as PermissionName });\r\n result[name] = encodeState(status.state);\r\n } catch { /* unsupported */ }\r\n }\r\n }\r\n } catch { /* silent */ }\r\n\r\n return result;\r\n }\r\n\r\n private getCanvasFingerprint(): string | null {\r\n try {\r\n const canvas = document.createElement('canvas');\r\n const ctx = canvas.getContext('2d');\r\n if (!ctx) return null;\r\n\r\n canvas.width = 240;\r\n canvas.height = 60;\r\n\r\n ctx.textBaseline = 'alphabetic';\r\n ctx.fillStyle = '#f60';\r\n ctx.fillRect(100, 1, 62, 20);\r\n ctx.fillStyle = '#069';\r\n ctx.font = '11pt Arial';\r\n ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 2, 15);\r\n ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\r\n ctx.font = '18pt Arial';\r\n ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 4, 45);\r\n\r\n return canvas.toDataURL();\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private getCanvasBlendingFingerprint(): string | null {\r\n try {\r\n const canvas = document.createElement('canvas');\r\n const ctx = canvas.getContext('2d');\r\n if (!ctx) return null;\r\n\r\n canvas.width = 100;\r\n canvas.height = 100;\r\n\r\n ctx.fillStyle = 'rgb(255,0,255)';\r\n ctx.beginPath();\r\n ctx.arc(50, 50, 50, 0, Math.PI * 2, true);\r\n ctx.closePath();\r\n ctx.fill();\r\n\r\n ctx.fillStyle = 'rgb(0,255,255)';\r\n ctx.globalCompositeOperation = 'multiply';\r\n ctx.beginPath();\r\n ctx.arc(25, 25, 25, 0, Math.PI * 2, true);\r\n ctx.closePath();\r\n ctx.fill();\r\n\r\n return canvas.toDataURL();\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private getCanvasGeometryFingerprint(): string | null {\r\n try {\r\n const canvas = document.createElement('canvas');\r\n const ctx = canvas.getContext('2d');\r\n if (!ctx) return null;\r\n\r\n canvas.width = 122;\r\n canvas.height = 110;\r\n\r\n ctx.globalCompositeOperation = 'multiply';\r\n for (let i = 0; i < 20; i++) {\r\n ctx.fillStyle = `rgb(${Math.floor(255 - i * 10)}, ${Math.floor(i * 10)}, ${Math.floor(i * 5)})`;\r\n ctx.beginPath();\r\n ctx.arc(i * 5, i * 5, i + 10, 0, Math.PI * 2, true);\r\n ctx.closePath();\r\n ctx.fill();\r\n }\r\n\r\n return canvas.toDataURL();\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private getWebGLFingerprint(): any {\r\n try {\r\n const canvas = document.createElement('canvas');\r\n const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') as WebGLRenderingContext;\r\n if (!gl) return null;\r\n\r\n const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');\r\n\r\n return {\r\n vendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),\r\n renderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),\r\n version: gl.getParameter(gl.VERSION),\r\n shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),\r\n extensions: gl.getSupportedExtensions()?.length || 0\r\n };\r\n } catch {\r\n return null;\r\n }\r\n }\r\n}\r\n","/**\r\n * Behavior Collector\r\n * Collects behavioral data (mouse, scroll, clicks, typing)\r\n */\r\n\r\nimport type { BehaviorData } from '../types';\r\n\r\nexport class BehaviorCollector {\r\n private mouseMovements: Array<{ x: number; y: number; timestamp: number }> = [];\r\n private scrollEvents: Array<{ y: number; timestamp: number }> = [];\r\n private clickEvents: Array<{ x: number; y: number; timestamp: number }> = [];\r\n private keypresses: number = 0;\r\n private startTime: number = Date.now();\r\n private isCollecting: boolean = false;\r\n\r\n /**\r\n * Start collecting behavioral data\r\n */\r\n start(): void {\r\n if (this.isCollecting) return;\r\n this.isCollecting = true;\r\n\r\n // Mouse movements\r\n document.addEventListener('mousemove', this.handleMouseMove, { passive: true });\r\n\r\n // Touch events (for mobile)\r\n document.addEventListener('touchmove', this.handleTouchMove, { passive: true });\r\n document.addEventListener('touchstart', this.handleTouchStart, { passive: true });\r\n\r\n // Scroll events\r\n window.addEventListener('scroll', this.handleScroll, { passive: true });\r\n\r\n // Click events\r\n document.addEventListener('click', this.handleClick, { passive: true });\r\n\r\n // Keypress events\r\n document.addEventListener('keypress', this.handleKeypress, { passive: true });\r\n }\r\n\r\n /**\r\n * Stop collecting behavioral data\r\n */\r\n stop(): void {\r\n this.isCollecting = false;\r\n document.removeEventListener('mousemove', this.handleMouseMove);\r\n document.removeEventListener('touchmove', this.handleTouchMove);\r\n document.removeEventListener('touchstart', this.handleTouchStart);\r\n window.removeEventListener('scroll', this.handleScroll);\r\n document.removeEventListener('click', this.handleClick);\r\n document.removeEventListener('keypress', this.handleKeypress);\r\n }\r\n\r\n private handleMouseMove = (e: MouseEvent) => {\r\n this.mouseMovements.push({\r\n x: e.clientX,\r\n y: e.clientY,\r\n timestamp: Date.now()\r\n });\r\n // Limit array size to prevent memory issues\r\n if (this.mouseMovements.length > 100) {\r\n this.mouseMovements.shift();\r\n }\r\n };\r\n\r\n private handleTouchMove = (e: TouchEvent) => {\r\n if (e.touches.length > 0) {\r\n const touch = e.touches[0];\r\n this.mouseMovements.push({\r\n x: touch.clientX,\r\n y: touch.clientY,\r\n timestamp: Date.now()\r\n });\r\n if (this.mouseMovements.length > 100) {\r\n this.mouseMovements.shift();\r\n }\r\n }\r\n };\r\n\r\n private handleTouchStart = (e: TouchEvent) => {\r\n if (e.touches.length > 0) {\r\n const touch = e.touches[0];\r\n this.clickEvents.push({\r\n x: touch.clientX,\r\n y: touch.clientY,\r\n timestamp: Date.now()\r\n });\r\n if (this.clickEvents.length > 50) {\r\n this.clickEvents.shift();\r\n }\r\n }\r\n };\r\n\r\n private handleScroll = () => {\r\n this.scrollEvents.push({\r\n y: window.scrollY,\r\n timestamp: Date.now()\r\n });\r\n if (this.scrollEvents.length > 50) {\r\n this.scrollEvents.shift();\r\n }\r\n };\r\n\r\n private handleClick = (e: MouseEvent) => {\r\n this.clickEvents.push({\r\n x: e.clientX,\r\n y: e.clientY,\r\n timestamp: Date.now()\r\n });\r\n if (this.clickEvents.length > 50) {\r\n this.clickEvents.shift();\r\n }\r\n };\r\n\r\n private handleKeypress = () => {\r\n this.keypresses++;\r\n };\r\n\r\n /**\r\n * Get collected behavioral data\r\n */\r\n getData(): BehaviorData {\r\n const sessionDuration = Date.now() - this.startTime;\r\n const typingSpeed = this.keypresses / (sessionDuration / 60000); // per minute\r\n\r\n return {\r\n mouseMovements: this.mouseMovements,\r\n scrollEvents: this.scrollEvents,\r\n clickEvents: this.clickEvents,\r\n sessionDuration,\r\n typingSpeed,\r\n keypresses: this.keypresses\r\n };\r\n }\r\n}\r\n","/**\r\n * Device Collector\r\n * Collects device and browser information\r\n */\r\n\r\nimport type { DeviceData } from '../types';\r\n\r\nexport class DeviceCollector {\r\n /**\r\n * Collect device and browser data\r\n */\r\n async collect(): Promise<DeviceData> {\r\n return {\r\n userAgent: navigator.userAgent,\r\n platform: navigator.platform,\r\n language: navigator.language,\r\n screenWidth: window.screen.width,\r\n screenHeight: window.screen.height,\r\n colorDepth: window.screen.colorDepth,\r\n pixelRatio: window.devicePixelRatio,\r\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\r\n hardwareConcurrency: navigator.hardwareConcurrency || 0,\r\n maxTouchPoints: navigator.maxTouchPoints || 0,\r\n cookieEnabled: navigator.cookieEnabled\r\n };\r\n }\r\n}\r\n","/**\r\n * API Client for IPRanker\r\n * Handles communication with IPRanker backend API\r\n */\r\n\r\nimport type { AnalysisResult, FingerprintData, BehaviorData, DeviceData } from '../types';\r\n\r\nexport interface AnalyzePayload {\r\n ip?: string;\r\n fingerprint: FingerprintData;\r\n behavior: BehaviorData | null;\r\n device: DeviceData;\r\n includeRawData?: boolean;\r\n}\r\n\r\nexport class APIClient {\r\n constructor(\r\n private baseURL: string,\r\n private apiKey: string\r\n ) {}\r\n\r\n /**\r\n * Send analysis request to IPRanker API\r\n */\r\n async analyze(payload: AnalyzePayload): Promise<AnalysisResult> {\r\n const startTime = Date.now();\r\n\r\n try {\r\n const response = await fetch(`${this.baseURL}/analyze`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'X-API-Key': this.apiKey\r\n },\r\n body: JSON.stringify(payload)\r\n });\r\n\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`API Error: ${response.status} ${response.statusText} - ${errorText}`);\r\n }\r\n\r\n const result = await response.json();\r\n const responseTime = Date.now() - startTime;\r\n\r\n return {\r\n ...result,\r\n responseTime\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`IPRanker API request failed: ${error.message}`);\r\n }\r\n throw error;\r\n }\r\n }\r\n}\r\n","/**\r\n * IPRanker SDK - Main Class\r\n * Single API call for IP Intelligence analysis with automatic fingerprint + behavior collection\r\n */\r\n\r\nimport { FingerprintCollector, BehaviorCollector, DeviceCollector } from './collectors';\r\nimport { APIClient } from './api/client';\r\nimport type { IPRankerOptions, AnalysisResult, AnalyzeOptions, EventCallbacks } from './types';\r\n\r\n/**\r\n * API version this SDK targets.\r\n * Each major SDK release is built for a specific API version.\r\n * - SDK v1.x → API v1\r\n * - SDK v2.x → API v2 (future)\r\n */\r\nconst API_VERSION = 'v1';\r\n\r\nexport class IPRanker {\r\n private apiKey: string;\r\n private apiClient: APIClient;\r\n private fingerprintCollector: FingerprintCollector;\r\n private behaviorCollector: BehaviorCollector;\r\n private deviceCollector: DeviceCollector;\r\n private options: Required<Omit<IPRankerOptions, 'onReady' | 'onAnalyzing' | 'onComplete' | 'onError'>>;\r\n private callbacks: EventCallbacks;\r\n\r\n constructor(apiKey: string, options: Partial<IPRankerOptions> = {}) {\r\n // Validate API key\r\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 10) {\r\n throw new Error('Invalid API key. Please provide a valid IPRanker API key.');\r\n }\r\n\r\n this.apiKey = apiKey;\r\n\r\n // Set default options\r\n this.options = {\r\n baseURL: options.baseURL || 'https://api.ipranker.com',\r\n collectBehavior: options.collectBehavior !== false, // default true\r\n behaviorTimeout: options.behaviorTimeout || 5000, // 5 seconds\r\n autoRetry: options.autoRetry !== false, // default true\r\n maxRetries: options.maxRetries || 3,\r\n cache: options.cache !== false, // default true\r\n cacheTimeout: options.cacheTimeout || 300000, // 5 minutes\r\n debug: options.debug || false\r\n };\r\n\r\n // Set callbacks\r\n this.callbacks = {\r\n onReady: options.onReady,\r\n onAnalyzing: options.onAnalyzing,\r\n onComplete: options.onComplete,\r\n onError: options.onError\r\n };\r\n\r\n // Initialize components with versioned API paths\r\n // baseURL should NOT include version (e.g., 'https://api.ipranker.com' or 'http://localhost:4001/api')\r\n const versionedBaseURL = `${this.options.baseURL}/${API_VERSION}`;\r\n this.apiClient = new APIClient(versionedBaseURL, this.apiKey);\r\n const fingerprintEndpoint = `${versionedBaseURL}/fingerprint-visitor-id`;\r\n this.fingerprintCollector = new FingerprintCollector(fingerprintEndpoint, this.apiKey);\r\n this.behaviorCollector = new BehaviorCollector();\r\n this.deviceCollector = new DeviceCollector();\r\n\r\n // Initialize collectors\r\n this._initialize();\r\n }\r\n\r\n /**\r\n * Initialize SDK\r\n */\r\n private _initialize(): void {\r\n try {\r\n // Start collecting behavioral data in background\r\n if (this.options.collectBehavior) {\r\n this.behaviorCollector.start();\r\n }\r\n\r\n this._log('IPRanker SDK initialized');\r\n this.callbacks.onReady?.();\r\n } catch (error) {\r\n this._handleError(error as Error);\r\n }\r\n }\r\n\r\n /**\r\n * Analyze current visitor - main method\r\n * Collects fingerprint + behavioral data and sends to IPRanker API\r\n */\r\n public async analyze(customOptions?: AnalyzeOptions): Promise<AnalysisResult> {\r\n try {\r\n this._log('Starting analysis...');\r\n this.callbacks.onAnalyzing?.();\r\n\r\n // Get IP for cache key (use provided IP or 'auto' for auto-detected)\r\n const cacheKey = customOptions?.ip || 'auto';\r\n\r\n // Check cache first (per IP address)\r\n if (this.options.cache) {\r\n const cached = this._getCachedResult(cacheKey);\r\n if (cached) {\r\n this._log('Returning cached result for IP:', cacheKey);\r\n this.callbacks.onComplete?.(cached);\r\n return cached;\r\n }\r\n }\r\n\r\n // Step 1: Collect fingerprint data\r\n this._log('Collecting fingerprint data...');\r\n const fingerprintData = await this.fingerprintCollector.collect();\r\n\r\n // Step 2: Collect behavioral data (with timeout)\r\n this._log('Collecting behavioral data...');\r\n const behaviorData = this.options.collectBehavior\r\n ? await this._collectBehaviorWithTimeout(customOptions?.timeout || this.options.behaviorTimeout)\r\n : null;\r\n\r\n // Step 3: Collect device data\r\n this._log('Collecting device data...');\r\n const deviceData = await this.deviceCollector.collect();\r\n\r\n // Step 4: Prepare request payload\r\n const payload: any = {\r\n fingerprint: fingerprintData,\r\n behavior: behaviorData,\r\n device: deviceData,\r\n includeRawData: customOptions?.includeRawData || false\r\n };\r\n\r\n // Only include IP if explicitly provided (otherwise backend will auto-detect from request)\r\n if (customOptions?.ip) {\r\n payload.ip = customOptions.ip;\r\n }\r\n\r\n this._log('Sending analysis request to IPRanker API...', { ip: payload.ip, hasIp: !!payload.ip });\r\n\r\n // Step 5: Send to IPRanker API (with auto-retry)\r\n const result = await this._sendWithRetry(payload);\r\n\r\n // Step 6: Cache result (per IP address)\r\n if (this.options.cache) {\r\n this._cacheResult(cacheKey, result);\r\n }\r\n\r\n this._log('Analysis complete', result);\r\n this.callbacks.onComplete?.(result);\r\n return result;\r\n\r\n } catch (error) {\r\n this._handleError(error as Error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Get fingerprint data only (without full analysis)\r\n */\r\n public async getFingerprint() {\r\n return await this.fingerprintCollector.collect();\r\n }\r\n\r\n /**\r\n * Get behavioral data only\r\n */\r\n public getBehaviorData() {\r\n return this.behaviorCollector.getData();\r\n }\r\n\r\n /**\r\n * Get device data only\r\n */\r\n public async getDeviceData() {\r\n return await this.deviceCollector.collect();\r\n }\r\n\r\n /**\r\n * Clear cache\r\n */\r\n public clearCache(): void {\r\n this._clearCache();\r\n this._log('Cache cleared');\r\n }\r\n\r\n /**\r\n * Stop behavioral data collection and cleanup\r\n */\r\n public destroy(): void {\r\n this.behaviorCollector.stop();\r\n this._log('SDK destroyed');\r\n }\r\n\r\n // ========== Private Methods ==========\r\n\r\n /**\r\n * Collect behavioral data with timeout\r\n */\r\n private async _collectBehaviorWithTimeout(timeout: number): Promise<any> {\r\n return Promise.race([\r\n Promise.resolve(this.behaviorCollector.getData()),\r\n new Promise(resolve => setTimeout(() => resolve(null), timeout))\r\n ]);\r\n }\r\n\r\n /**\r\n * Send request with auto-retry\r\n */\r\n private async _sendWithRetry(payload: any, retries = 0): Promise<AnalysisResult> {\r\n try {\r\n return await this.apiClient.analyze(payload);\r\n } catch (error) {\r\n if (this.options.autoRetry && retries < this.options.maxRetries) {\r\n this._log(`Retry ${retries + 1}/${this.options.maxRetries} after error:`, error);\r\n await this._delay(1000 * (retries + 1)); // Exponential backoff\r\n return this._sendWithRetry(payload, retries + 1);\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Get cached result if available and not expired (per IP address)\r\n */\r\n private _getCachedResult(ip: string): AnalysisResult | null {\r\n try {\r\n const cacheData = localStorage.getItem('ipranker_cache');\r\n if (!cacheData) return null;\r\n\r\n const cache = JSON.parse(cacheData);\r\n\r\n // Migration: Clear old cache format (has 'result' and 'timestamp' at root level)\r\n if (cache.result && cache.timestamp) {\r\n this._log('Clearing old cache format');\r\n localStorage.removeItem('ipranker_cache');\r\n return null;\r\n }\r\n\r\n const entry = cache[ip];\r\n if (!entry || !entry.result || !entry.timestamp) return null;\r\n\r\n const age = Date.now() - entry.timestamp;\r\n\r\n if (age < this.options.cacheTimeout) {\r\n return entry.result;\r\n }\r\n\r\n // Remove expired entry\r\n delete cache[ip];\r\n localStorage.setItem('ipranker_cache', JSON.stringify(cache));\r\n return null;\r\n } catch (e) {\r\n // If parsing fails, clear the cache\r\n localStorage.removeItem('ipranker_cache');\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Cache result to localStorage (per IP address)\r\n */\r\n private _cacheResult(ip: string, result: AnalysisResult): void {\r\n try {\r\n let cache: Record<string, { result: AnalysisResult; timestamp: number }> = {};\r\n\r\n const existing = localStorage.getItem('ipranker_cache');\r\n if (existing) {\r\n cache = JSON.parse(existing);\r\n }\r\n\r\n cache[ip] = {\r\n result,\r\n timestamp: Date.now()\r\n };\r\n\r\n localStorage.setItem('ipranker_cache', JSON.stringify(cache));\r\n } catch (e) {\r\n // Cache failed, ignore\r\n this._log('Cache failed:', e);\r\n }\r\n }\r\n\r\n /**\r\n * Clear cache from localStorage\r\n */\r\n private _clearCache(): void {\r\n try {\r\n localStorage.removeItem('ipranker_cache');\r\n } catch (e) {}\r\n }\r\n\r\n /**\r\n * Handle errors\r\n */\r\n private _handleError(error: Error): void {\r\n this._log('Error:', error);\r\n this.callbacks.onError?.(error);\r\n }\r\n\r\n /**\r\n * Debug logging\r\n */\r\n private _log(...args: any[]): void {\r\n if (this.options.debug) {\r\n console.log('[IPRanker SDK]', ...args);\r\n }\r\n }\r\n\r\n /**\r\n * Delay utility\r\n */\r\n private _delay(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n }\r\n}\r\n\r\nexport default IPRanker;\r\n"],"names":["simpleHash","str","h","i","length","Math","imul","charCodeAt","abs","toString","TEST_FONTS","BASE_FONTS","TEST_STRING","TEST_SIZE","FingerprintCollector","constructor","fingerprintEndpoint","apiKey","this","collect","completeDeviceData","collectCompleteDeviceData","headers","Accept","response","fetch","method","credentials","body","JSON","stringify","deviceData","ok","collectBasicFingerprint","fingerprintData","json","visitorId","fingerprints","installationId","transformFingerprintResponse","error","data","baseFingerprint","canvas","text","canvasText","geometry","canvasGeometry","blending","canvasBlending","webgl","vendor","renderer","extensions","audio","screen","width","window","height","colorDepth","pixelRatio","devicePixelRatio","orientation","type","timezone","localization","timeZone","Intl","DateTimeFormat","resolvedOptions","language","languages","navigator","platform","fonts","webrtc","sensors","advancedBrowser","hardware","permissions","fontData","webrtcData","Promise","all","collectFontFingerprint","collectWebRTCFingerprint","sensorData","collectSensorFingerprint","advancedBrowserData","collectAdvancedBrowserFeatures","hardwareData","collectHardwareData","permissionsData","collectPermissionsState","webglData","getWebGLFingerprint","getCanvasFingerprint","getCanvasGeometryFingerprint","getCanvasBlendingFingerprint","userAgent","brands","userAgentData","mobile","test","architecture","bitness","browserInfo","pixelDepth","capabilities","cookiesEnabled","cookieEnabled","doNotTrack","pdfViewerEnabled","timingResolution","performance","now","fontCount","fontFingerprint","async","startTime","installedFonts","ctx","document","createElement","getContext","detectionTimeMs","baseWidths","baseFont","font","measureText","detected","push","sort","join","detectFonts","result","webrtcSupported","RTCPeerConnection","mediaDevicesCount","hasCamera","hasMicrophone","hasSpeaker","webrtcFingerprint","mediaDevices","enumerateDevices","devices","some","d","kind","supported","camera","mic","speaker","accelerometerSupported","gyroscopeSupported","magnetometerSupported","ambientLightSupported","sensorFingerprint","accel","gyro","mag","light","localStorageEnabled","isStorageAvailable","sessionStorageEnabled","indexedDBEnabled","indexedDB","cacheAPIEnabled","webGL2Supported","webAssemblySupported","WebAssembly","serviceWorkerSupported","browserFeaturesFingerprint","ls","ss","idb","cache","webgl2","wasm","sw","dnt","pdf","storage","testKey","setItem","removeItem","cores","hardwareConcurrency","memory","deviceMemory","maxTouchPoints","gpuVendor","gpuRenderer","geolocation","notifications","microphone","encodeState","state","names","name","status","query","textBaseline","fillStyle","fillRect","fillText","toDataURL","beginPath","arc","PI","closePath","fill","globalCompositeOperation","floor","gl","debugInfo","getExtension","getParameter","UNMASKED_VENDOR_WEBGL","VENDOR","UNMASKED_RENDERER_WEBGL","RENDERER","version","VERSION","shadingLanguageVersion","SHADING_LANGUAGE_VERSION","getSupportedExtensions","BehaviorCollector","mouseMovements","scrollEvents","clickEvents","keypresses","Date","isCollecting","handleMouseMove","e","x","clientX","y","clientY","timestamp","shift","handleTouchMove","touches","touch","handleTouchStart","handleScroll","scrollY","handleClick","handleKeypress","start","addEventListener","passive","stop","removeEventListener","getData","sessionDuration","typingSpeed","DeviceCollector","screenWidth","screenHeight","APIClient","baseURL","analyze","payload","errorText","Error","statusText","responseTime","message","IPRanker","options","collectBehavior","behaviorTimeout","autoRetry","maxRetries","cacheTimeout","debug","callbacks","onReady","onAnalyzing","onComplete","onError","versionedBaseURL","apiClient","fingerprintCollector","behaviorCollector","deviceCollector","_initialize","_log","_handleError","customOptions","cacheKey","ip","cached","_getCachedResult","behaviorData","_collectBehaviorWithTimeout","timeout","fingerprint","behavior","device","includeRawData","hasIp","_sendWithRetry","_cacheResult","getFingerprint","getBehaviorData","getDeviceData","clearCache","_clearCache","destroy","race","resolve","setTimeout","retries","_delay","cacheData","localStorage","getItem","parse","entry","existing","args","console","log","ms"],"mappings":"AAoCM,SAAUA,EAAWC,GACzB,IAAIC,EAAI,EACR,IAAK,IAAIC,EAAI,EAAGA,EAAIF,EAAIG,OAAQD,IAC9BD,EAAIG,KAAKC,KAAK,GAAIJ,GAAKD,EAAIM,WAAWJ,GAAK,EAE7C,OAAOE,KAAKG,IAAIN,GAAGO,SAAS,GAC9B,CCpCA,MAAMC,EAAa,CACjB,QAAS,cAAe,eAAgB,UAAW,gBAAiB,UACpE,UAAW,iBAAkB,gBAAiB,WAAY,aAAc,SACxE,cAAe,SAAU,yBAA0B,WAAY,SAAU,UACzE,SAAU,WAAY,gBAAiB,gBAAiB,iBACxD,sBAAuB,gBAAiB,UAAW,qBACnD,qBAAsB,wBAAyB,oBAC/C,uBAAwB,mBAAoB,kBAAmB,qBAC/D,eAAgB,kBAAmB,YAAa,aAAc,eAC9D,UAAW,eAAgB,aAAc,oBAAqB,oBAC9D,cAAe,eAAgB,WAAY,iBAAkB,oBAC7D,iBAAkB,oBAAqB,kBAAmB,SAAU,eACpE,gBAAiB,gBAAiB,cAAe,mBAAoB,aACrE,UAAW,SAAU,SAAU,kBAAmB,eAAgB,UAClE,WAAY,YAAa,YAAa,eACtC,sBAAuB,cAAe,iBAAkB,oBACxD,sBAAuB,cAAe,eAAgB,SAAU,cAChE,wBAAyB,cAAe,aAAc,YAAa,qBACnE,sBAAuB,eAAgB,kBAAmB,aAAc,gBACxE,cAAe,UAAW,SAAU,cAAe,iBAAkB,UACrE,gBAAiB,gBAAiB,QAAS,gBAAiB,SAAU,SACtE,YAAa,YAAa,iBAAkB,aAAc,2BAC1D,sBAAuB,eAAgB,UAAW,oBAAqB,kBACvE,kBAAmB,sBAAuB,kBAAmB,gBAC7D,gBAAiB,WAAY,sBAAuB,cAAe,QACnE,SAAU,aAAc,SAAU,kBAAmB,QAAS,WAAY,UAC1E,YAAa,cAAe,cAAe,cAAe,uBAC1D,UAAW,UAAW,kBAAmB,iBAAkB,WAAY,mBACvE,WAAY,aAAc,cAAe,oBAAqB,OAAQ,kBACtE,cAAe,iBAAkB,kBAAmB,mBAAoB,QACxE,cAAe,UACf,SAAU,mBAAoB,eAAgB,cAAe,cAC7D,mBAAoB,eAAgB,kBAAmB,kBACvD,mBAAoB,gBAAiB,qBAAsB,gBAC3D,WAAY,WAAY,YAAa,YAAa,aAAc,YAChE,aAAc,kBAAmB,cACjC,YAAa,SAAU,mBAAoB,cAAe,cAAe,OACzE,aAAc,kBAAmB,kBAAmB,mBAAoB,SACxE,UAAW,eAAgB,SAAU,UAAW,mBAAoB,YACpE,QAAS,YAAa,YAAa,YAAa,YAAa,SAC7D,mBAAoB,QAAS,UAAW,UAAW,mBACnD,gBAAiB,gBAAiB,iBAAkB,cAAe,aACnE,iBAGIC,EAAa,CAAC,YAAa,aAAc,SACzCC,EAAc,gBACdC,EAAY,aCrCLC,EAIX,WAAAC,CAAYC,EAA8B,0DAA2DC,GACnGC,KAAKF,oBAAsBA,EAC3BE,KAAKD,OAASA,CAChB,CAEA,aAAME,GACJ,IACE,MAAMC,QAA2BF,KAAKG,4BAEhCC,EAAkC,CACtC,eAAgB,mBAChBC,OAAU,oBAGRL,KAAKD,SACPK,EAAQ,aAAeJ,KAAKD,QAG9B,MAAMO,QAAiBC,MAAMP,KAAKF,oBAAqB,CACrDU,OAAQ,OACRJ,UACAK,YAAa,OACbC,KAAMC,KAAKC,UAAU,CAAEC,WAAYX,MAGrC,IAAKI,EAASQ,GACZ,aAAad,KAAKe,0BAGpB,MAAMC,QAAwBV,EAASW,OAEvC,OAAKD,EAAgBE,WAAcF,EAAgBG,cAAcC,eAI1DpB,KAAKqB,6BAA6BL,SAH1BhB,KAAKe,yBAKtB,CAAE,MAAOO,GACP,aAAatB,KAAKe,yBACpB,CACF,CAEQ,4BAAAM,CAA6BE,GACnC,MAAMC,EAAmC,CACvCJ,eAAgBG,EAAKL,WAAaK,EAAKJ,cAAcC,gBAAkBG,EAAKH,gBAAkB,KAC9FK,OAAQ,CACNC,KAAMH,EAAKJ,cAAcQ,YAAcJ,EAAKE,QAAQC,MAAQ,KAC5DE,SAAUL,EAAKJ,cAAcU,gBAAkBN,EAAKE,QAAQG,UAAY,KACxEE,SAAUP,EAAKJ,cAAcY,gBAAkBR,EAAKE,QAAQK,UAAY,MAE1EE,MAAOT,EAAKJ,cAAca,OAAST,EAAKS,MAAQ,CAC9CC,OAASV,EAAKJ,cAAca,OAAOC,QAAUV,EAAKS,OAAOC,QAAW,KACpEC,SAAWX,EAAKJ,cAAca,OAAOE,UAAYX,EAAKS,OAAOE,UAAa,KAC1EC,WAAaZ,EAAKJ,cAAca,OAAOG,YAAYjD,QAAUqC,EAAKS,OAAOG,YAAYjD,QAAU,GAC7F,CAAE+C,OAAQ,KAAMC,SAAU,KAAMC,WAAY,GAChDC,MAAOb,EAAKJ,cAAciB,OAASb,EAAKa,OAAS,KACjDC,OAAQ,CACNC,MAAOf,EAAKc,QAAQC,OAASC,OAAOF,OAAOC,MAC3CE,OAAQjB,EAAKc,QAAQG,QAAUD,OAAOF,OAAOG,OAC7CC,WAAYlB,EAAKc,QAAQI,YAAcF,OAAOF,OAAOI,WACrDC,WAAYnB,EAAKc,QAAQK,YAAcH,OAAOI,iBAC9CC,YAAarB,EAAKc,QAAQO,aAAeP,OAAOO,aAAaC,MAASN,OAAeK,aAAe,WAEtGE,SAAUvB,EAAKwB,cAAcC,UAAYzB,EAAKuB,UAAYG,KAAKC,iBAAiBC,kBAAkBH,SAClGI,SAAU7B,EAAKwB,cAAcM,YAAY,IAAM9B,EAAK6B,UAAYE,UAAUF,SAC1EG,SAAUhC,EAAKgC,UAAUA,UAAYhC,EAAKgC,UAAYD,UAAUC,UAUlE,OAPIhC,EAAKiC,QAAOhC,EAAgBgC,MAAQjC,EAAKiC,OACzCjC,EAAKkC,SAAQjC,EAAgBiC,OAASlC,EAAKkC,QAC3ClC,EAAKmC,UAASlC,EAAgBkC,QAAUnC,EAAKmC,SAC7CnC,EAAKoC,kBAAiBnC,EAAgBmC,gBAAkBpC,EAAKoC,iBAC7DpC,EAAKqC,WAAUpC,EAAgBoC,SAAWrC,EAAKqC,UAC/CrC,EAAKsC,cAAarC,EAAgBqC,YAActC,EAAKsC,aAElDrC,CACT,CAEQ,6BAAMT,GACZ,MAAO+C,EAAUC,SAAoBC,QAAQC,IAAI,CAC/CjE,KAAKkE,yBACLlE,KAAKmE,6BAGDC,EAAapE,KAAKqE,2BAClBC,EAAsBtE,KAAKuE,iCAC3BC,EAAexE,KAAKyE,sBACpBC,QAAwB1E,KAAK2E,0BAC7BC,EAAY5E,KAAK6E,sBAEvB,MAAO,CACLzD,eAAgB,KAChBK,OAAQ,CACNC,KAAM1B,KAAK8E,uBACXlD,SAAU5B,KAAK+E,+BACfjD,SAAU9B,KAAKgF,gCAEjBhD,MAAO4C,EAAY,CACjB3C,OAAQ2C,EAAU3C,QAAU,KAC5BC,SAAU0C,EAAU1C,UAAY,KAChCC,WAAYyC,EAAUzC,YAAc,GAClC,CAAEF,OAAQ,KAAMC,SAAU,KAAMC,WAAY,GAChDC,MAAO,KACPC,OAAQ,CACNC,MAAOC,OAAOF,OAAOC,MACrBE,OAAQD,OAAOF,OAAOG,OACtBC,WAAYF,OAAOF,OAAOI,WAC1BC,WAAYH,OAAOI,iBACnBC,YAAaP,OAAOO,aAAaC,MAASN,OAAeK,aAAe,WAE1EE,SAAUG,KAAKC,iBAAiBC,kBAAkBH,SAClDI,SAAUE,UAAUF,SACpBG,SAAUD,UAAUC,SACpBC,MAAOM,EACPL,OAAQM,EACRL,QAASU,EACTT,gBAAiBW,EACjBV,SAAUY,EACVX,YAAaa,EAEjB,CAEQ,+BAAMvE,GACZ,MAAM8E,EAAY3B,UAAU2B,UACtB1B,EAAWD,UAAUC,UAEpBO,EAAUC,SAAoBC,QAAQC,IAAI,CAC/CjE,KAAKkE,yBACLlE,KAAKmE,6BAGDC,EAAapE,KAAKqE,2BAClBC,EAAsBtE,KAAKuE,iCAC3BC,EAAexE,KAAKyE,sBACpBC,QAAwB1E,KAAK2E,0BAC7BC,EAAY5E,KAAK6E,sBAEvB,MAAO,CACLtB,SAAU,CACR2B,OAAS5B,UAAkB6B,eAAeD,QAAU,GACpD3B,SAAUA,EACV6B,OAAQ,mCAAmCC,KAAKJ,GAChDK,aAAehC,UAAkB6B,eAAeG,cAAgB,UAChEC,QAAUjC,UAAkB6B,eAAeI,SAAW,UACtDC,YAAa,CACXP,UAAWA,EACX7B,SAAUE,UAAUF,SACpBC,UAAWC,UAAUD,WAAa,CAACC,UAAUF,YAGjDf,OAAQ,CACNC,MAAOC,OAAOF,OAAOC,MACrBE,OAAQD,OAAOF,OAAOG,OACtBC,WAAYF,OAAOF,OAAOI,WAC1BgD,WAAYlD,OAAOF,OAAOoD,YAAclD,OAAOF,OAAOI,WACtDG,YAAaP,OAAOO,aAAaC,MAASN,OAAeK,aAAe,mBACxEF,WAAYH,OAAOI,kBAAoB,GAEzCiB,SAAUY,EACVkB,aAAc,CACZC,eAAgBrC,UAAUsC,cAC1BC,WAAqC,MAAzBvC,UAAUuC,YAAqD,MAA9BtD,OAAesD,WAC5DC,iBAAmBxC,UAAkBwC,mBAAoB,GAE3D/C,aAAc,CACZM,UAAWC,UAAUD,WAAa,CAACC,UAAUF,UAC7CJ,SAAUC,KAAKC,iBAAiBC,kBAAkBH,UAEpD7B,aAAc,CACZQ,WAAY3B,KAAK8E,uBACjB/C,eAAgB/B,KAAKgF,+BACrBnD,eAAgB7B,KAAK+E,+BACrB/C,MAAO4C,EACPmB,iBAAyC,oBAAhBC,aAA0D,mBAApBA,YAAYC,IAAqB,OAAS,OAE3GzC,MAAO,CAAE0C,UAAWpC,EAASoC,UAAWC,gBAAiBrC,EAASqC,iBAClE1C,OAAQM,EACRL,QAASU,EACTT,gBAAiBW,EACjBT,YAAaa,EAEjB,CAEQ,4BAAMR,GACZ,IACE,aD/ICkC,iBACL,MAAMC,EAAYL,YAAYC,MACxBK,EAA2B,GAEjC,IACE,MACMC,EADSC,SAASC,cAAc,UACnBC,WAAW,MAE9B,IAAKH,EACH,MAAO,CACLD,eAAgB,GAChBJ,UAAW,EACXC,gBAAiB,cACjBQ,gBAAiBX,YAAYC,MAAQI,GAIzC,MAAMO,EAAqC,CAAA,EAC3C,IAAK,MAAMC,KAAYpH,EACrB8G,EAAIO,KAAO,GAAGnH,KAAakH,IAC3BD,EAAWC,GAAYN,EAAIQ,YAAYrH,GAAa4C,MAGtD,IAAK,MAAMwE,KAAQtH,EAAY,CAC7B,IAAIwH,GAAW,EACf,IAAK,MAAMH,KAAYpH,EAErB,GADA8G,EAAIO,KAAO,G