@seekora-ai/search-sdk 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,868 +0,0 @@
1
- "use strict";
2
- /**
3
- * Context Collector for Seekora SDK
4
- *
5
- * Collects browser context, device information, and fingerprinting
6
- * for enriching analytics events with comprehensive user environment data.
7
- *
8
- * Uses FingerprintJS Open Source for industry-standard device fingerprinting
9
- * with 50+ browser signals for high accuracy cross-session identification.
10
- */
11
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
- if (k2 === undefined) k2 = k;
13
- var desc = Object.getOwnPropertyDescriptor(m, k);
14
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
- desc = { enumerable: true, get: function() { return m[k]; } };
16
- }
17
- Object.defineProperty(o, k2, desc);
18
- }) : (function(o, m, k, k2) {
19
- if (k2 === undefined) k2 = k;
20
- o[k2] = m[k];
21
- }));
22
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
- Object.defineProperty(o, "default", { enumerable: true, value: v });
24
- }) : function(o, v) {
25
- o["default"] = v;
26
- });
27
- var __importStar = (this && this.__importStar) || (function () {
28
- var ownKeys = function(o) {
29
- ownKeys = Object.getOwnPropertyNames || function (o) {
30
- var ar = [];
31
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
- return ar;
33
- };
34
- return ownKeys(o);
35
- };
36
- return function (mod) {
37
- if (mod && mod.__esModule) return mod;
38
- var result = {};
39
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
- __setModuleDefault(result, mod);
41
- return result;
42
- };
43
- })();
44
- Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.ContextCollector = void 0;
46
- exports.getDefaultContextCollector = getDefaultContextCollector;
47
- exports.collectBrowserContext = collectBrowserContext;
48
- let FingerprintJS = null;
49
- // Try to load FingerprintJS
50
- async function loadFingerprintJS() {
51
- if (FingerprintJS)
52
- return FingerprintJS;
53
- try {
54
- FingerprintJS = await Promise.resolve().then(() => __importStar(require('@fingerprintjs/fingerprintjs')));
55
- return FingerprintJS;
56
- }
57
- catch {
58
- // FingerprintJS not available, will fall back to manual implementation
59
- return null;
60
- }
61
- }
62
- /**
63
- * Default configuration
64
- * Fingerprinting is enabled by default with FingerprintJS for best accuracy
65
- */
66
- const DEFAULT_CONFIG = {
67
- enableFingerprinting: true,
68
- useFingerprintJS: true,
69
- enableCanvasFingerprint: true,
70
- enableWebGLFingerprint: true,
71
- enableAudioFingerprint: false,
72
- autoCollect: true,
73
- cacheDuration: 30000,
74
- fingerprintStorageKey: 'seekora_device_fp',
75
- enableExtendedComponents: true,
76
- };
77
- /**
78
- * ContextCollector class for gathering browser and device context
79
- *
80
- * Uses FingerprintJS Open Source for industry-standard fingerprinting
81
- * with automatic fallback to manual implementation if unavailable.
82
- */
83
- class ContextCollector {
84
- constructor(config = {}) {
85
- this.cachedContext = null;
86
- this.cacheTimestamp = 0;
87
- this.cachedFingerprint = null;
88
- this.fingerprintJSAgent = null;
89
- this.fingerprintJSLoading = null;
90
- this.usingFingerprintJS = false;
91
- this.config = { ...DEFAULT_CONFIG, ...config };
92
- // Load cached fingerprint from storage
93
- if (this.config.enableFingerprinting) {
94
- this.loadCachedFingerprint();
95
- // Pre-load FingerprintJS agent for faster first fingerprint
96
- if (this.config.useFingerprintJS && this.isBrowser()) {
97
- this.initFingerprintJS();
98
- }
99
- }
100
- }
101
- /**
102
- * Initialize FingerprintJS agent (async, non-blocking)
103
- */
104
- async initFingerprintJS() {
105
- if (this.fingerprintJSAgent || this.fingerprintJSLoading)
106
- return;
107
- this.fingerprintJSLoading = (async () => {
108
- try {
109
- const fpjs = await loadFingerprintJS();
110
- if (fpjs) {
111
- this.fingerprintJSAgent = await fpjs.load();
112
- this.usingFingerprintJS = true;
113
- console.debug('[Seekora] FingerprintJS initialized successfully');
114
- }
115
- }
116
- catch (error) {
117
- console.debug('[Seekora] FingerprintJS not available, using manual fingerprinting');
118
- }
119
- return this.fingerprintJSAgent;
120
- })();
121
- await this.fingerprintJSLoading;
122
- }
123
- /**
124
- * Get FingerprintJS agent, waiting for initialization if needed
125
- */
126
- async getFingerprintJSAgent() {
127
- if (this.fingerprintJSAgent)
128
- return this.fingerprintJSAgent;
129
- if (this.fingerprintJSLoading) {
130
- await this.fingerprintJSLoading;
131
- return this.fingerprintJSAgent;
132
- }
133
- await this.initFingerprintJS();
134
- return this.fingerprintJSAgent;
135
- }
136
- /**
137
- * Check if running in browser environment
138
- */
139
- isBrowser() {
140
- return typeof window !== 'undefined' && typeof document !== 'undefined';
141
- }
142
- /**
143
- * Collect complete browser context
144
- */
145
- async collect() {
146
- // Return cached context if still valid
147
- if (this.cachedContext && this.config.cacheDuration > 0) {
148
- const elapsed = Date.now() - this.cacheTimestamp;
149
- if (elapsed < this.config.cacheDuration) {
150
- // Update dynamic fields that may have changed
151
- this.cachedContext.document_visibility = this.getDocumentVisibility();
152
- this.cachedContext.viewport_width = this.getViewportWidth();
153
- this.cachedContext.viewport_height = this.getViewportHeight();
154
- return this.cachedContext;
155
- }
156
- }
157
- const context = await this.collectFresh();
158
- // Cache the context
159
- this.cachedContext = context;
160
- this.cacheTimestamp = Date.now();
161
- return context;
162
- }
163
- /**
164
- * Collect fresh context without caching
165
- */
166
- async collectFresh() {
167
- if (!this.isBrowser()) {
168
- return this.getServerContext();
169
- }
170
- const utm = this.parseUTMParameters();
171
- const connection = this.getConnectionInfo();
172
- const browserInfo = this.parseBrowserInfo();
173
- const context = {
174
- // Screen dimensions
175
- screen_width: screen.width || 0,
176
- screen_height: screen.height || 0,
177
- viewport_width: this.getViewportWidth(),
178
- viewport_height: this.getViewportHeight(),
179
- color_depth: screen.colorDepth || screen.pixelDepth || 24,
180
- pixel_ratio: window.devicePixelRatio || 1,
181
- // Browser information
182
- browser_name: browserInfo.name,
183
- browser_version: browserInfo.version,
184
- browser_language: navigator.language || 'en',
185
- browser_languages: Array.from(navigator.languages || [navigator.language || 'en']),
186
- timezone: this.getTimezone(),
187
- timezone_offset: new Date().getTimezoneOffset(),
188
- // Page context
189
- page_url: this.sanitizeUrl(window.location.href),
190
- page_path: window.location.pathname,
191
- page_title: document.title || '',
192
- page_referrer: this.sanitizeUrl(document.referrer || ''),
193
- document_visibility: this.getDocumentVisibility(),
194
- // UTM parameters
195
- ...utm,
196
- // Connection information
197
- ...connection,
198
- // Platform detection
199
- platform: this.detectPlatform(),
200
- is_mobile: this.isMobile(),
201
- is_tablet: this.isTablet(),
202
- is_touch_device: this.isTouchDevice(),
203
- hardware_concurrency: navigator.hardwareConcurrency,
204
- max_touch_points: navigator.maxTouchPoints,
205
- device_memory: navigator.deviceMemory,
206
- };
207
- // Generate fingerprint if enabled
208
- if (this.config.enableFingerprinting) {
209
- context.device_fingerprint = await this.getOrGenerateFingerprint();
210
- }
211
- return context;
212
- }
213
- /**
214
- * Get server-side context (for Node.js environments)
215
- */
216
- getServerContext() {
217
- return {
218
- screen_width: 0,
219
- screen_height: 0,
220
- viewport_width: 0,
221
- viewport_height: 0,
222
- color_depth: 0,
223
- pixel_ratio: 1,
224
- browser_name: 'node',
225
- browser_version: process.version || '',
226
- browser_language: 'en',
227
- browser_languages: ['en'],
228
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
229
- timezone_offset: new Date().getTimezoneOffset(),
230
- page_url: '',
231
- page_path: '',
232
- page_title: '',
233
- page_referrer: '',
234
- document_visibility: 'visible',
235
- platform: process.platform || 'unknown',
236
- is_mobile: false,
237
- is_tablet: false,
238
- is_touch_device: false,
239
- };
240
- }
241
- /**
242
- * Get viewport width
243
- */
244
- getViewportWidth() {
245
- if (!this.isBrowser())
246
- return 0;
247
- return window.innerWidth || document.documentElement?.clientWidth || document.body?.clientWidth || 0;
248
- }
249
- /**
250
- * Get viewport height
251
- */
252
- getViewportHeight() {
253
- if (!this.isBrowser())
254
- return 0;
255
- return window.innerHeight || document.documentElement?.clientHeight || document.body?.clientHeight || 0;
256
- }
257
- /**
258
- * Get document visibility state
259
- */
260
- getDocumentVisibility() {
261
- if (!this.isBrowser())
262
- return 'visible';
263
- return document.visibilityState || 'visible';
264
- }
265
- /**
266
- * Get timezone name
267
- */
268
- getTimezone() {
269
- try {
270
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
271
- }
272
- catch {
273
- return 'UTC';
274
- }
275
- }
276
- /**
277
- * Parse UTM parameters from URL
278
- */
279
- parseUTMParameters() {
280
- if (!this.isBrowser())
281
- return {};
282
- try {
283
- const params = new URLSearchParams(window.location.search);
284
- const result = {};
285
- const utmSource = params.get('utm_source');
286
- const utmMedium = params.get('utm_medium');
287
- const utmCampaign = params.get('utm_campaign');
288
- const utmTerm = params.get('utm_term');
289
- const utmContent = params.get('utm_content');
290
- if (utmSource)
291
- result.utm_source = utmSource;
292
- if (utmMedium)
293
- result.utm_medium = utmMedium;
294
- if (utmCampaign)
295
- result.utm_campaign = utmCampaign;
296
- if (utmTerm)
297
- result.utm_term = utmTerm;
298
- if (utmContent)
299
- result.utm_content = utmContent;
300
- // Also check referrer for UTM params (for cross-domain tracking)
301
- if (Object.keys(result).length === 0 && document.referrer) {
302
- try {
303
- const referrerUrl = new URL(document.referrer);
304
- const referrerParams = new URLSearchParams(referrerUrl.search);
305
- const refUtmSource = referrerParams.get('utm_source');
306
- const refUtmMedium = referrerParams.get('utm_medium');
307
- const refUtmCampaign = referrerParams.get('utm_campaign');
308
- const refUtmTerm = referrerParams.get('utm_term');
309
- const refUtmContent = referrerParams.get('utm_content');
310
- if (refUtmSource)
311
- result.utm_source = refUtmSource;
312
- if (refUtmMedium)
313
- result.utm_medium = refUtmMedium;
314
- if (refUtmCampaign)
315
- result.utm_campaign = refUtmCampaign;
316
- if (refUtmTerm)
317
- result.utm_term = refUtmTerm;
318
- if (refUtmContent)
319
- result.utm_content = refUtmContent;
320
- }
321
- catch {
322
- // Invalid referrer URL, ignore
323
- }
324
- }
325
- return result;
326
- }
327
- catch {
328
- return {};
329
- }
330
- }
331
- /**
332
- * Get connection information from Network Information API
333
- */
334
- getConnectionInfo() {
335
- if (!this.isBrowser())
336
- return {};
337
- const connection = navigator.connection ||
338
- navigator.mozConnection ||
339
- navigator.webkitConnection;
340
- if (!connection)
341
- return {};
342
- return {
343
- connection_type: connection.type,
344
- connection_effective_type: connection.effectiveType,
345
- connection_downlink: connection.downlink,
346
- connection_rtt: connection.rtt,
347
- };
348
- }
349
- /**
350
- * Parse browser name and version from user agent
351
- */
352
- parseBrowserInfo() {
353
- if (!this.isBrowser()) {
354
- return { name: 'node', version: process.version || '' };
355
- }
356
- const ua = navigator.userAgent;
357
- let name = 'unknown';
358
- let version = '';
359
- // Order matters - check more specific browsers first
360
- if (ua.includes('Firefox/')) {
361
- name = 'firefox';
362
- const match = ua.match(/Firefox\/(\d+\.?\d*)/);
363
- version = match ? match[1] : '';
364
- }
365
- else if (ua.includes('Edg/')) {
366
- name = 'edge';
367
- const match = ua.match(/Edg\/(\d+\.?\d*)/);
368
- version = match ? match[1] : '';
369
- }
370
- else if (ua.includes('OPR/') || ua.includes('Opera/')) {
371
- name = 'opera';
372
- const match = ua.match(/(?:OPR|Opera)\/(\d+\.?\d*)/);
373
- version = match ? match[1] : '';
374
- }
375
- else if (ua.includes('Chrome/')) {
376
- name = 'chrome';
377
- const match = ua.match(/Chrome\/(\d+\.?\d*)/);
378
- version = match ? match[1] : '';
379
- }
380
- else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
381
- name = 'safari';
382
- const match = ua.match(/Version\/(\d+\.?\d*)/);
383
- version = match ? match[1] : '';
384
- }
385
- else if (ua.includes('MSIE') || ua.includes('Trident/')) {
386
- name = 'ie';
387
- const match = ua.match(/(?:MSIE |rv:)(\d+\.?\d*)/);
388
- version = match ? match[1] : '';
389
- }
390
- return { name, version };
391
- }
392
- /**
393
- * Detect platform from user agent
394
- */
395
- detectPlatform() {
396
- if (!this.isBrowser()) {
397
- return process.platform || 'unknown';
398
- }
399
- const ua = navigator.userAgent.toLowerCase();
400
- const platform = (navigator.platform || '').toLowerCase();
401
- if (ua.includes('android'))
402
- return 'android';
403
- if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod'))
404
- return 'ios';
405
- if (platform.includes('mac') || ua.includes('mac os'))
406
- return 'macos';
407
- if (platform.includes('win') || ua.includes('windows'))
408
- return 'windows';
409
- if (platform.includes('linux') || ua.includes('linux'))
410
- return 'linux';
411
- if (ua.includes('cros'))
412
- return 'chromeos';
413
- return 'unknown';
414
- }
415
- /**
416
- * Check if device is mobile
417
- */
418
- isMobile() {
419
- if (!this.isBrowser())
420
- return false;
421
- const ua = navigator.userAgent.toLowerCase();
422
- return /android|webos|iphone|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);
423
- }
424
- /**
425
- * Check if device is tablet
426
- */
427
- isTablet() {
428
- if (!this.isBrowser())
429
- return false;
430
- const ua = navigator.userAgent.toLowerCase();
431
- return /ipad|tablet|playbook|silk/i.test(ua) ||
432
- (ua.includes('android') && !ua.includes('mobile'));
433
- }
434
- /**
435
- * Check if device supports touch
436
- */
437
- isTouchDevice() {
438
- if (!this.isBrowser())
439
- return false;
440
- return 'ontouchstart' in window ||
441
- navigator.maxTouchPoints > 0 ||
442
- navigator.msMaxTouchPoints > 0;
443
- }
444
- /**
445
- * Sanitize URL to remove sensitive query parameters
446
- */
447
- sanitizeUrl(url) {
448
- if (!url)
449
- return '';
450
- try {
451
- const urlObj = new URL(url);
452
- const sensitiveParams = ['password', 'token', 'api_key', 'apikey', 'secret', 'auth', 'key'];
453
- sensitiveParams.forEach(param => {
454
- if (urlObj.searchParams.has(param)) {
455
- urlObj.searchParams.set(param, '[REDACTED]');
456
- }
457
- });
458
- return urlObj.toString();
459
- }
460
- catch {
461
- return url;
462
- }
463
- }
464
- /**
465
- * Load cached fingerprint from storage
466
- */
467
- loadCachedFingerprint() {
468
- if (!this.isBrowser())
469
- return;
470
- try {
471
- const stored = localStorage.getItem(this.config.fingerprintStorageKey);
472
- if (stored) {
473
- this.cachedFingerprint = stored;
474
- }
475
- }
476
- catch {
477
- // localStorage not available
478
- }
479
- }
480
- /**
481
- * Get or generate device fingerprint
482
- */
483
- async getOrGenerateFingerprint() {
484
- // Return cached fingerprint if available
485
- if (this.cachedFingerprint) {
486
- return this.cachedFingerprint;
487
- }
488
- // Generate new fingerprint
489
- const fingerprint = await this.generateFingerprint();
490
- // Cache it
491
- this.cachedFingerprint = fingerprint;
492
- // Persist to localStorage
493
- if (this.isBrowser()) {
494
- try {
495
- localStorage.setItem(this.config.fingerprintStorageKey, fingerprint);
496
- }
497
- catch {
498
- // localStorage not available
499
- }
500
- }
501
- return fingerprint;
502
- }
503
- /**
504
- * Generate device fingerprint using FingerprintJS or manual fallback
505
- *
506
- * FingerprintJS provides ~99.5% accuracy with 50+ browser signals
507
- * Manual fallback provides ~70% accuracy with 15+ signals
508
- */
509
- async generateFingerprint() {
510
- // Try FingerprintJS first (preferred)
511
- if (this.config.useFingerprintJS && this.isBrowser()) {
512
- try {
513
- const agent = await this.getFingerprintJSAgent();
514
- if (agent) {
515
- const result = await agent.get();
516
- // FingerprintJS provides a visitorId (stable fingerprint)
517
- // and components (individual signals with confidence scores)
518
- console.debug('[Seekora] FingerprintJS fingerprint generated:', {
519
- visitorId: result.visitorId,
520
- confidence: result.confidence?.score,
521
- components: Object.keys(result.components || {}).length
522
- });
523
- return result.visitorId;
524
- }
525
- }
526
- catch (error) {
527
- console.debug('[Seekora] FingerprintJS failed, falling back to manual:', error);
528
- }
529
- }
530
- // Fallback to manual fingerprinting
531
- return this.generateManualFingerprint();
532
- }
533
- /**
534
- * Generate fingerprint manually using browser characteristics
535
- * Used as fallback when FingerprintJS is not available
536
- */
537
- async generateManualFingerprint() {
538
- const components = [];
539
- if (!this.isBrowser()) {
540
- // Server-side fingerprint (limited)
541
- components.push(process.platform || 'unknown', process.version || 'unknown', process.arch || 'unknown');
542
- return this.hashComponents(components);
543
- }
544
- // Screen characteristics
545
- components.push(screen.width, screen.height, screen.colorDepth, window.devicePixelRatio || 1, screen.availWidth || 0, screen.availHeight || 0);
546
- // Browser characteristics
547
- components.push(navigator.language, navigator.languages?.join(',') || '', navigator.hardwareConcurrency || 0, navigator.maxTouchPoints || 0, navigator.deviceMemory || 0, navigator.cookieEnabled ? 1 : 0, navigator.doNotTrack || '');
548
- // Timezone
549
- components.push(this.getTimezone());
550
- // Platform
551
- components.push(navigator.platform || '', navigator.vendor || '', navigator.userAgent || '');
552
- // Plugins (limited info)
553
- if (navigator.plugins) {
554
- components.push(navigator.plugins.length);
555
- // Add plugin names for extended fingerprinting
556
- if (this.config.enableExtendedComponents) {
557
- const pluginNames = Array.from(navigator.plugins)
558
- .map(p => p.name)
559
- .sort()
560
- .join(',');
561
- components.push(pluginNames);
562
- }
563
- }
564
- // Canvas fingerprint (if enabled)
565
- if (this.config.enableCanvasFingerprint) {
566
- const canvasHash = this.getCanvasFingerprint();
567
- if (canvasHash) {
568
- components.push(canvasHash);
569
- }
570
- }
571
- // WebGL fingerprint (if enabled)
572
- if (this.config.enableWebGLFingerprint) {
573
- const webglHash = this.getWebGLFingerprint();
574
- if (webglHash) {
575
- components.push(webglHash);
576
- }
577
- }
578
- // Audio fingerprint (if enabled)
579
- if (this.config.enableAudioFingerprint) {
580
- const audioHash = await this.getAudioFingerprint();
581
- if (audioHash) {
582
- components.push(audioHash);
583
- }
584
- }
585
- // Extended components for higher uniqueness
586
- if (this.config.enableExtendedComponents) {
587
- // Add available fonts detection (simplified)
588
- const fontHash = this.getFontFingerprint();
589
- if (fontHash) {
590
- components.push(fontHash);
591
- }
592
- // Add WebGL parameters
593
- const webglParams = this.getWebGLParameters();
594
- if (webglParams) {
595
- components.push(webglParams);
596
- }
597
- }
598
- return this.hashComponents(components);
599
- }
600
- /**
601
- * Get audio fingerprint using AudioContext
602
- */
603
- async getAudioFingerprint() {
604
- if (!this.isBrowser())
605
- return null;
606
- try {
607
- const AudioContext = window.AudioContext || window.webkitAudioContext;
608
- if (!AudioContext)
609
- return null;
610
- const context = new AudioContext();
611
- const oscillator = context.createOscillator();
612
- const analyser = context.createAnalyser();
613
- const gain = context.createGain();
614
- const processor = context.createScriptProcessor(4096, 1, 1);
615
- oscillator.type = 'triangle';
616
- oscillator.frequency.setValueAtTime(10000, context.currentTime);
617
- gain.gain.setValueAtTime(0, context.currentTime);
618
- oscillator.connect(analyser);
619
- analyser.connect(processor);
620
- processor.connect(gain);
621
- gain.connect(context.destination);
622
- oscillator.start(0);
623
- const dataArray = new Float32Array(analyser.frequencyBinCount);
624
- analyser.getFloatFrequencyData(dataArray);
625
- oscillator.stop();
626
- await context.close();
627
- // Hash the audio data
628
- const audioSum = dataArray.slice(0, 30).reduce((a, b) => a + Math.abs(b), 0);
629
- return this.simpleHash(audioSum.toString());
630
- }
631
- catch {
632
- return null;
633
- }
634
- }
635
- /**
636
- * Get font fingerprint by testing font availability
637
- */
638
- getFontFingerprint() {
639
- if (!this.isBrowser())
640
- return null;
641
- try {
642
- const testFonts = [
643
- 'Arial', 'Courier New', 'Georgia', 'Helvetica', 'Times New Roman',
644
- 'Verdana', 'Comic Sans MS', 'Impact', 'Lucida Console', 'Tahoma',
645
- 'Trebuchet MS', 'Palatino Linotype', 'Century Gothic', 'Garamond'
646
- ];
647
- const canvas = document.createElement('canvas');
648
- const ctx = canvas.getContext('2d');
649
- if (!ctx)
650
- return null;
651
- const testString = 'mmmmmmmmmmlli';
652
- const baseFont = 'monospace';
653
- ctx.font = `72px ${baseFont}`;
654
- const baseWidth = ctx.measureText(testString).width;
655
- const availableFonts = [];
656
- for (const font of testFonts) {
657
- ctx.font = `72px "${font}", ${baseFont}`;
658
- const width = ctx.measureText(testString).width;
659
- if (width !== baseWidth) {
660
- availableFonts.push(font);
661
- }
662
- }
663
- return this.simpleHash(availableFonts.join(','));
664
- }
665
- catch {
666
- return null;
667
- }
668
- }
669
- /**
670
- * Get additional WebGL parameters for fingerprinting
671
- */
672
- getWebGLParameters() {
673
- if (!this.isBrowser())
674
- return null;
675
- try {
676
- const canvas = document.createElement('canvas');
677
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
678
- if (!gl)
679
- return null;
680
- const params = [
681
- gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
682
- gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
683
- gl.getParameter(gl.MAX_VARYING_VECTORS),
684
- gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS),
685
- gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS),
686
- gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
687
- gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
688
- gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE),
689
- gl.getParameter(gl.MAX_RENDERBUFFER_SIZE),
690
- gl.getParameter(gl.MAX_VIEWPORT_DIMS)?.toString() || ''
691
- ];
692
- return this.simpleHash(params.join(','));
693
- }
694
- catch {
695
- return null;
696
- }
697
- }
698
- /**
699
- * Get canvas fingerprint
700
- */
701
- getCanvasFingerprint() {
702
- if (!this.isBrowser())
703
- return null;
704
- try {
705
- const canvas = document.createElement('canvas');
706
- const ctx = canvas.getContext('2d');
707
- if (!ctx)
708
- return null;
709
- canvas.width = 200;
710
- canvas.height = 50;
711
- // Draw text with various styles
712
- ctx.textBaseline = 'top';
713
- ctx.font = '14px Arial';
714
- ctx.fillStyle = '#f60';
715
- ctx.fillRect(125, 1, 62, 20);
716
- ctx.fillStyle = '#069';
717
- ctx.fillText('Seekora SDK', 2, 15);
718
- ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
719
- ctx.fillText('Canvas FP', 4, 17);
720
- // Get data URL and hash it
721
- const dataUrl = canvas.toDataURL();
722
- return this.simpleHash(dataUrl);
723
- }
724
- catch {
725
- return null;
726
- }
727
- }
728
- /**
729
- * Get WebGL fingerprint
730
- */
731
- getWebGLFingerprint() {
732
- if (!this.isBrowser())
733
- return null;
734
- try {
735
- const canvas = document.createElement('canvas');
736
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
737
- if (!gl)
738
- return null;
739
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
740
- if (!debugInfo)
741
- return null;
742
- const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
743
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
744
- return this.simpleHash(`${vendor}~${renderer}`);
745
- }
746
- catch {
747
- return null;
748
- }
749
- }
750
- /**
751
- * Hash array of components into a fingerprint string
752
- */
753
- hashComponents(components) {
754
- const str = components.map(c => String(c)).join('|');
755
- return this.simpleHash(str);
756
- }
757
- /**
758
- * Simple hash function (djb2 variant)
759
- */
760
- simpleHash(str) {
761
- let hash = 5381;
762
- for (let i = 0; i < str.length; i++) {
763
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
764
- hash = hash & hash; // Convert to 32bit integer
765
- }
766
- // Convert to hex string and ensure positive
767
- return (hash >>> 0).toString(16).padStart(8, '0');
768
- }
769
- /**
770
- * Clear cached context
771
- */
772
- clearCache() {
773
- this.cachedContext = null;
774
- this.cacheTimestamp = 0;
775
- }
776
- /**
777
- * Clear cached fingerprint (both in memory and storage)
778
- */
779
- clearFingerprint() {
780
- this.cachedFingerprint = null;
781
- if (this.isBrowser()) {
782
- try {
783
- localStorage.removeItem(this.config.fingerprintStorageKey);
784
- }
785
- catch {
786
- // localStorage not available
787
- }
788
- }
789
- }
790
- /**
791
- * Get current configuration
792
- */
793
- getConfig() {
794
- return { ...this.config };
795
- }
796
- /**
797
- * Update configuration
798
- */
799
- updateConfig(config) {
800
- this.config = { ...this.config, ...config };
801
- this.clearCache();
802
- }
803
- /**
804
- * Check if using FingerprintJS (vs manual implementation)
805
- */
806
- isUsingFingerprintJS() {
807
- return this.usingFingerprintJS;
808
- }
809
- /**
810
- * Get fingerprint info including method used and confidence
811
- */
812
- async getFingerprintInfo() {
813
- if (!this.config.enableFingerprinting) {
814
- return {
815
- fingerprint: '',
816
- method: 'manual',
817
- confidence: 0
818
- };
819
- }
820
- // Try FingerprintJS
821
- if (this.config.useFingerprintJS && this.isBrowser()) {
822
- try {
823
- const agent = await this.getFingerprintJSAgent();
824
- if (agent) {
825
- const result = await agent.get();
826
- return {
827
- fingerprint: result.visitorId,
828
- method: 'fingerprintjs',
829
- confidence: result.confidence?.score,
830
- componentsCount: Object.keys(result.components || {}).length
831
- };
832
- }
833
- }
834
- catch {
835
- // Fall through to manual
836
- }
837
- }
838
- // Manual fallback
839
- const fingerprint = await this.generateManualFingerprint();
840
- return {
841
- fingerprint,
842
- method: 'manual',
843
- confidence: 0.7, // Estimated accuracy for manual method
844
- componentsCount: 15
845
- };
846
- }
847
- }
848
- exports.ContextCollector = ContextCollector;
849
- /**
850
- * Default context collector instance
851
- */
852
- let defaultCollector = null;
853
- /**
854
- * Get or create the default context collector
855
- */
856
- function getDefaultContextCollector(config) {
857
- if (!defaultCollector) {
858
- defaultCollector = new ContextCollector(config);
859
- }
860
- return defaultCollector;
861
- }
862
- /**
863
- * Collect browser context using the default collector
864
- */
865
- async function collectBrowserContext(config) {
866
- const collector = getDefaultContextCollector(config);
867
- return collector.collect();
868
- }