@ianmenethil/zp-devicefp 0.1.0-alpha.0

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.
@@ -0,0 +1,1449 @@
1
+ // src/runtime/browser.ts
2
+ function getBrowserEnv() {
3
+ return {
4
+ global: globalThis,
5
+ navigator: typeof navigator === "undefined" ? void 0 : navigator,
6
+ document: typeof document === "undefined" ? void 0 : document,
7
+ screen: typeof screen === "undefined" ? void 0 : screen,
8
+ location: typeof location === "undefined" ? void 0 : location,
9
+ fetch: typeof fetch === "undefined" ? void 0 : fetch,
10
+ crypto: globalThis.crypto,
11
+ performance: typeof performance === "undefined" ? void 0 : performance,
12
+ intl: typeof Intl === "undefined" ? void 0 : Intl
13
+ };
14
+ }
15
+ function now(env) {
16
+ return env.performance?.now?.() ?? Date.now();
17
+ }
18
+ function hasDom(env) {
19
+ return Boolean(env.document?.createElement);
20
+ }
21
+ function hasCanvas(env) {
22
+ return Boolean(env.document?.createElement?.("canvas").getContext);
23
+ }
24
+
25
+ // src/signals/audio.ts
26
+ function getOfflineAudioContext(env) {
27
+ const g = env;
28
+ return g.OfflineAudioContext ?? g.webkitOfflineAudioContext;
29
+ }
30
+ var audioCollector = {
31
+ name: "audio",
32
+ tier: "core",
33
+ supportsSync: false,
34
+ async collect(context) {
35
+ const started = now(context.env);
36
+ const OfflineContext = getOfflineAudioContext(context.env.global);
37
+ if (!OfflineContext) {
38
+ return { status: "unsupported", durationMs: now(context.env) - started };
39
+ }
40
+ try {
41
+ const audioContext = new OfflineContext(1, 5e3, 44100);
42
+ const oscillator = audioContext.createOscillator();
43
+ const compressor = audioContext.createDynamicsCompressor();
44
+ oscillator.type = "triangle";
45
+ oscillator.frequency.value = 1e4;
46
+ compressor.threshold.value = -50;
47
+ compressor.knee.value = 40;
48
+ compressor.ratio.value = 12;
49
+ compressor.attack.value = 0;
50
+ compressor.release.value = 0.25;
51
+ oscillator.connect(compressor);
52
+ compressor.connect(audioContext.destination);
53
+ oscillator.start(0);
54
+ const rendered = await audioContext.startRendering();
55
+ const channelData = rendered.getChannelData(0);
56
+ let sum = 0;
57
+ for (let i = 4500; i < channelData.length; i += 1) {
58
+ sum += Math.abs(channelData[i]);
59
+ }
60
+ return {
61
+ status: "ok",
62
+ durationMs: now(context.env) - started,
63
+ value: {
64
+ sampleRate: rendered.sampleRate,
65
+ length: rendered.length,
66
+ signalValue: Number(sum.toFixed(6))
67
+ }
68
+ };
69
+ } catch (error) {
70
+ return {
71
+ status: "blocked",
72
+ durationMs: now(context.env) - started,
73
+ error: error instanceof Error ? error.message : String(error)
74
+ };
75
+ }
76
+ }
77
+ };
78
+
79
+ // src/signals/canvas.ts
80
+ function renderCanvasFingerprint(doc) {
81
+ const canvas = doc.createElement("canvas");
82
+ canvas.width = 280;
83
+ canvas.height = 80;
84
+ const ctx = canvas.getContext("2d");
85
+ if (!ctx) {
86
+ throw new Error("2D canvas context unavailable");
87
+ }
88
+ ctx.fillStyle = "#f60";
89
+ ctx.fillRect(8, 10, 120, 60);
90
+ ctx.fillStyle = "#069";
91
+ ctx.font = "20px Arial";
92
+ ctx.fillText("Browser FP", 12, 40);
93
+ ctx.strokeStyle = "rgba(102, 204, 0, 0.7)";
94
+ ctx.beginPath();
95
+ ctx.arc(180, 40, 24, 0, Math.PI * 2);
96
+ ctx.stroke();
97
+ return {
98
+ dataUrl: canvas.toDataURL(),
99
+ winding: ctx.isPointInPath(1, 1, "evenodd")
100
+ };
101
+ }
102
+ var canvasCollector = {
103
+ name: "canvas",
104
+ tier: "core",
105
+ supportsSync: true,
106
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
107
+ async collect(context) {
108
+ return this.collectSync(context);
109
+ },
110
+ collectSync(context) {
111
+ const started = now(context.env);
112
+ const doc = context.env.document;
113
+ if (!doc || !hasCanvas(context.env)) {
114
+ return { status: "unsupported", durationMs: now(context.env) - started };
115
+ }
116
+ try {
117
+ return {
118
+ status: "ok",
119
+ durationMs: now(context.env) - started,
120
+ value: renderCanvasFingerprint(doc)
121
+ };
122
+ } catch (error) {
123
+ return {
124
+ status: "error",
125
+ durationMs: now(context.env) - started,
126
+ error: error instanceof Error ? error.message : String(error)
127
+ };
128
+ }
129
+ }
130
+ };
131
+
132
+ // src/constants.ts
133
+ var CORE_SIGNALS = ["ua", "uaHints", "locale", "screen", "hardware", "storage", "fonts", "canvas", "webgl", "audio"];
134
+ var EXTENDED_SIGNALS = [
135
+ "mediaDevices",
136
+ "permissions",
137
+ "webrtc",
138
+ "frameInfo",
139
+ "networkInfo",
140
+ "paymentSupport",
141
+ "referrerInfo",
142
+ "navigationInfo",
143
+ "riskSignals"
144
+ ];
145
+ var SYNC_SIGNALS = [
146
+ "ua",
147
+ "locale",
148
+ "screen",
149
+ "hardware",
150
+ "storage",
151
+ "fonts",
152
+ "canvas",
153
+ "webgl",
154
+ "frameInfo",
155
+ "networkInfo",
156
+ "paymentSupport",
157
+ "referrerInfo",
158
+ "navigationInfo"
159
+ ];
160
+ var DEFAULT_TIMEOUT_MS = 1500;
161
+ var DEFAULT_FONT_LIST = [
162
+ "Arial",
163
+ "Helvetica Neue",
164
+ "Times New Roman",
165
+ "Georgia",
166
+ "Courier New",
167
+ "Trebuchet MS",
168
+ "Verdana",
169
+ "Tahoma",
170
+ "Impact",
171
+ "Comic Sans MS"
172
+ ];
173
+ var DEFAULT_PERMISSION_NAMES = ["geolocation", "notifications", "camera", "microphone"];
174
+ var DEFAULT_UA_HINTS = [
175
+ "architecture",
176
+ "bitness",
177
+ "formFactors",
178
+ "fullVersionList",
179
+ "model",
180
+ "platform",
181
+ "platformVersion",
182
+ "wow64"
183
+ ];
184
+
185
+ // src/signals/fonts.ts
186
+ var GENERIC_FAMILIES = ["monospace", "sans-serif", "serif"];
187
+ var TEST_TEXT = "mmmmmmmmmmlli";
188
+ function measureText(doc, fontFamily, genericFamily) {
189
+ const span = doc.createElement("span");
190
+ span.textContent = TEST_TEXT;
191
+ span.style.position = "absolute";
192
+ span.style.left = "-9999px";
193
+ span.style.top = "-9999px";
194
+ span.style.fontSize = "72px";
195
+ span.style.fontFamily = `${fontFamily}, ${genericFamily}`;
196
+ doc.body.appendChild(span);
197
+ const width = span.offsetWidth;
198
+ doc.body.removeChild(span);
199
+ return width;
200
+ }
201
+ function collectFontMatches(doc) {
202
+ const matches = [];
203
+ for (const candidate of DEFAULT_FONT_LIST) {
204
+ let detected = false;
205
+ for (const generic of GENERIC_FAMILIES) {
206
+ const baseWidth = measureText(doc, "", generic);
207
+ const candidateWidth = measureText(doc, candidate, generic);
208
+ if (candidateWidth !== baseWidth) {
209
+ detected = true;
210
+ break;
211
+ }
212
+ }
213
+ if (detected) {
214
+ matches.push(candidate);
215
+ }
216
+ }
217
+ return matches;
218
+ }
219
+ var fontsCollector = {
220
+ name: "fonts",
221
+ tier: "core",
222
+ supportsSync: true,
223
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
224
+ async collect(context) {
225
+ return this.collectSync(context);
226
+ },
227
+ collectSync(context) {
228
+ const started = now(context.env);
229
+ const doc = context.env.document;
230
+ if (!doc || !hasDom(context.env)) {
231
+ return { status: "unsupported", durationMs: now(context.env) - started };
232
+ }
233
+ try {
234
+ const fonts = collectFontMatches(doc);
235
+ return {
236
+ status: "ok",
237
+ durationMs: now(context.env) - started,
238
+ value: {
239
+ detectedFonts: fonts,
240
+ fontCount: fonts.length
241
+ }
242
+ };
243
+ } catch (error) {
244
+ return {
245
+ status: "error",
246
+ durationMs: now(context.env) - started,
247
+ error: error instanceof Error ? error.message : String(error)
248
+ };
249
+ }
250
+ }
251
+ };
252
+
253
+ // src/signals/frameInfo.ts
254
+ var frameInfoCollector = {
255
+ name: "frameInfo",
256
+ tier: "extended",
257
+ supportsSync: true,
258
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
259
+ async collect(context) {
260
+ return this.collectSync(context);
261
+ },
262
+ collectSync(context) {
263
+ const started = now(context.env);
264
+ const doc = context.env.document;
265
+ if (!doc) {
266
+ return { status: "unsupported", durationMs: now(context.env) - started };
267
+ }
268
+ try {
269
+ const iframes = doc.getElementsByTagName("iframe");
270
+ const domains = [];
271
+ for (const element of iframes) {
272
+ const src = element.src || "";
273
+ try {
274
+ domains.push(new URL(src).hostname);
275
+ } catch {
276
+ domains.push("unknown");
277
+ }
278
+ }
279
+ const win = context.env.global;
280
+ return {
281
+ status: "ok",
282
+ durationMs: now(context.env) - started,
283
+ value: {
284
+ iframesCount: iframes.length,
285
+ isTopLevel: win === win.top,
286
+ iframeDomains: domains
287
+ }
288
+ };
289
+ } catch (error) {
290
+ return {
291
+ status: "error",
292
+ durationMs: now(context.env) - started,
293
+ error: error instanceof Error ? error.message : String(error)
294
+ };
295
+ }
296
+ }
297
+ };
298
+
299
+ // src/signals/hardware.ts
300
+ var hardwareCollector = {
301
+ name: "hardware",
302
+ tier: "core",
303
+ supportsSync: true,
304
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
305
+ async collect(context) {
306
+ return this.collectSync(context);
307
+ },
308
+ collectSync(context) {
309
+ const started = now(context.env);
310
+ const nav = context.env.navigator;
311
+ if (!nav) {
312
+ return { status: "unsupported", durationMs: now(context.env) - started };
313
+ }
314
+ return {
315
+ status: "ok",
316
+ durationMs: now(context.env) - started,
317
+ value: {
318
+ hardwareConcurrency: nav.hardwareConcurrency,
319
+ deviceMemory: nav.deviceMemory,
320
+ platform: nav.platform,
321
+ maxTouchPoints: nav.maxTouchPoints
322
+ }
323
+ };
324
+ }
325
+ };
326
+
327
+ // src/signals/locale.ts
328
+ var localeCollector = {
329
+ name: "locale",
330
+ tier: "core",
331
+ supportsSync: true,
332
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
333
+ async collect(context) {
334
+ return this.collectSync(context);
335
+ },
336
+ collectSync(context) {
337
+ const started = now(context.env);
338
+ const nav = context.env.navigator;
339
+ const intl = context.env.intl;
340
+ if (!nav || !intl) {
341
+ return { status: "unsupported", durationMs: now(context.env) - started };
342
+ }
343
+ const formatter = new intl.DateTimeFormat();
344
+ const options = formatter.resolvedOptions();
345
+ const numberFormatter = new intl.NumberFormat(nav.language);
346
+ const relativeTimeFormatter = typeof intl.RelativeTimeFormat === "function" ? new intl.RelativeTimeFormat(nav.language, { numeric: "auto" }) : void 0;
347
+ const sampleDate = new Date(Date.UTC(2024, 0, 2, 3, 4, 5));
348
+ return {
349
+ status: "ok",
350
+ durationMs: now(context.env) - started,
351
+ value: {
352
+ language: nav.language,
353
+ languages: nav.languages,
354
+ locale: options.locale,
355
+ calendar: options.calendar,
356
+ numberingSystem: options.numberingSystem,
357
+ timeZone: options.timeZone,
358
+ hourCycle: options.hourCycle,
359
+ timeZoneOffsetMinutes: sampleDate.getTimezoneOffset(),
360
+ formattedNumber: numberFormatter.format(123456.789),
361
+ formattedDate: formatter.format(sampleDate),
362
+ formattedRelativeDay: relativeTimeFormatter?.format(-1, "day")
363
+ }
364
+ };
365
+ }
366
+ };
367
+
368
+ // src/signals/mediaDevices.ts
369
+ var mediaDevicesCollector = {
370
+ name: "mediaDevices",
371
+ tier: "extended",
372
+ supportsSync: false,
373
+ async collect(context) {
374
+ const started = now(context.env);
375
+ const mediaDevices = context.env.navigator?.mediaDevices;
376
+ const runtime = context.env.global;
377
+ if (!runtime.isSecureContext) {
378
+ return {
379
+ status: "blocked",
380
+ durationMs: now(context.env) - started,
381
+ error: "Media device enumeration requires a secure context."
382
+ };
383
+ }
384
+ if (runtime.document?.visibilityState !== "visible") {
385
+ return {
386
+ status: "blocked",
387
+ durationMs: now(context.env) - started,
388
+ error: "Media device enumeration requires a visible document."
389
+ };
390
+ }
391
+ if (!mediaDevices?.enumerateDevices) {
392
+ return { status: "unsupported", durationMs: now(context.env) - started };
393
+ }
394
+ try {
395
+ const devices = await mediaDevices.enumerateDevices();
396
+ const summary = devices.reduce((acc, device) => {
397
+ acc[device.kind] = (acc[device.kind] ?? 0) + 1;
398
+ return acc;
399
+ }, {});
400
+ return {
401
+ status: "ok",
402
+ durationMs: now(context.env) - started,
403
+ value: summary
404
+ };
405
+ } catch (error) {
406
+ return {
407
+ status: "blocked",
408
+ durationMs: now(context.env) - started,
409
+ error: error instanceof Error ? error.message : String(error)
410
+ };
411
+ }
412
+ }
413
+ };
414
+
415
+ // src/signals/navigationInfo.ts
416
+ var navigationInfoCollector = {
417
+ name: "navigationInfo",
418
+ tier: "extended",
419
+ supportsSync: true,
420
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
421
+ async collect(context) {
422
+ return this.collectSync(context);
423
+ },
424
+ collectSync(context) {
425
+ const started = now(context.env);
426
+ const perf = context.env.performance;
427
+ if (!perf) {
428
+ return { status: "unsupported", durationMs: now(context.env) - started };
429
+ }
430
+ try {
431
+ let navType = null;
432
+ const entries = perf.getEntriesByType("navigation");
433
+ const navEntry = entries?.[0];
434
+ if (navEntry !== void 0) {
435
+ navType = navEntry.type;
436
+ }
437
+ if (navType === null) {
438
+ navType = perf.navigation.type;
439
+ }
440
+ return {
441
+ status: "ok",
442
+ durationMs: now(context.env) - started,
443
+ value: { navigationType: navType }
444
+ };
445
+ } catch (error) {
446
+ return {
447
+ status: "error",
448
+ durationMs: now(context.env) - started,
449
+ error: error instanceof Error ? error.message : String(error)
450
+ };
451
+ }
452
+ }
453
+ };
454
+
455
+ // src/signals/networkInfo.ts
456
+ var networkInfoCollector = {
457
+ name: "networkInfo",
458
+ tier: "extended",
459
+ supportsSync: true,
460
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
461
+ async collect(context) {
462
+ return this.collectSync(context);
463
+ },
464
+ collectSync(context) {
465
+ const started = now(context.env);
466
+ const nav = context.env.navigator;
467
+ if (!nav) {
468
+ return { status: "unsupported", durationMs: now(context.env) - started };
469
+ }
470
+ const conn = nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
471
+ if (!conn) {
472
+ return { status: "unsupported", durationMs: now(context.env) - started };
473
+ }
474
+ const c = conn;
475
+ return {
476
+ status: "ok",
477
+ durationMs: now(context.env) - started,
478
+ value: {
479
+ effectiveType: c.effectiveType ?? null,
480
+ downlink: c.downlink ?? null,
481
+ rtt: c.rtt ?? null,
482
+ saveData: typeof c.saveData === "boolean" ? c.saveData : null
483
+ }
484
+ };
485
+ }
486
+ };
487
+
488
+ // src/signals/paymentSupport.ts
489
+ var paymentSupportCollector = {
490
+ name: "paymentSupport",
491
+ tier: "extended",
492
+ supportsSync: true,
493
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
494
+ async collect(context) {
495
+ return this.collectSync(context);
496
+ },
497
+ collectSync(context) {
498
+ const started = now(context.env);
499
+ const win = context.env.global;
500
+ return {
501
+ status: "ok",
502
+ durationMs: now(context.env) - started,
503
+ value: {
504
+ paymentRequest: win.PaymentRequest !== void 0
505
+ }
506
+ };
507
+ }
508
+ };
509
+
510
+ // src/signals/permissions.ts
511
+ var permissionsCollector = {
512
+ name: "permissions",
513
+ tier: "extended",
514
+ supportsSync: false,
515
+ async collect(context) {
516
+ const started = now(context.env);
517
+ const permissions = context.env.navigator?.permissions;
518
+ if (!permissions?.query) {
519
+ return { status: "unsupported", durationMs: now(context.env) - started };
520
+ }
521
+ try {
522
+ const results = await Promise.all(
523
+ DEFAULT_PERMISSION_NAMES.map(async (name) => {
524
+ try {
525
+ const state = await permissions.query({ name });
526
+ return [name, state.state];
527
+ } catch {
528
+ return [name, "unsupported"];
529
+ }
530
+ })
531
+ );
532
+ return {
533
+ status: "ok",
534
+ durationMs: now(context.env) - started,
535
+ value: Object.fromEntries(results)
536
+ };
537
+ } catch (error) {
538
+ return {
539
+ status: "blocked",
540
+ durationMs: now(context.env) - started,
541
+ error: error instanceof Error ? error.message : String(error)
542
+ };
543
+ }
544
+ }
545
+ };
546
+
547
+ // src/signals/referrerInfo.ts
548
+ var referrerInfoCollector = {
549
+ name: "referrerInfo",
550
+ tier: "extended",
551
+ supportsSync: true,
552
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
553
+ async collect(context) {
554
+ return this.collectSync(context);
555
+ },
556
+ collectSync(context) {
557
+ const started = now(context.env);
558
+ const doc = context.env.document;
559
+ if (!doc) {
560
+ return { status: "unsupported", durationMs: now(context.env) - started };
561
+ }
562
+ return {
563
+ status: "ok",
564
+ durationMs: now(context.env) - started,
565
+ value: { referrer: doc.referrer }
566
+ };
567
+ }
568
+ };
569
+
570
+ // src/signals/riskSignals.ts
571
+ var riskSignalsCollector = {
572
+ name: "riskSignals",
573
+ tier: "extended",
574
+ supportsSync: false,
575
+ async collect(context) {
576
+ const started = now(context.env);
577
+ const nav = context.env.navigator;
578
+ const win = context.env.global;
579
+ if (!nav) {
580
+ return { status: "unsupported", durationMs: now(context.env) - started };
581
+ }
582
+ try {
583
+ const ua = nav.userAgent;
584
+ const webdriver = nav.webdriver ? true : null;
585
+ const hints = [];
586
+ if (ua.includes("HeadlessChrome")) {
587
+ hints.push("headless-ua");
588
+ }
589
+ if (ua.includes("Chrome") && typeof win.chrome !== "object") {
590
+ hints.push("missing-chrome-object");
591
+ }
592
+ if (win.outerHeight === 0 || win.outerWidth === 0) {
593
+ hints.push("zero-outer-dimensions");
594
+ }
595
+ if (ua.includes("Chrome") && !ua.includes("Mobile") && nav.plugins.length === 0) {
596
+ hints.push("no-plugins-on-desktop-chrome");
597
+ }
598
+ if (Array.isArray(nav.languages) && nav.languages.length === 0) {
599
+ hints.push("empty-languages");
600
+ }
601
+ let uaConsistency = null;
602
+ const userAgentData = nav.userAgentData;
603
+ if (userAgentData && "getHighEntropyValues" in userAgentData) {
604
+ try {
605
+ const uad = userAgentData;
606
+ const uaPlatform = ua.includes("Win") ? "Windows" : ua.includes("Mac") ? "macOS" : ua.includes("Linux") ? "Linux" : null;
607
+ const uadPlatform = uad.platform || null;
608
+ uaConsistency = uaPlatform === null || uadPlatform === null ? null : uaPlatform === uadPlatform;
609
+ } catch {
610
+ uaConsistency = null;
611
+ }
612
+ }
613
+ const notificationPermission = typeof Notification !== "undefined" ? Notification.permission : null;
614
+ let permissionsHeadlessHint = null;
615
+ if (typeof nav.permissions?.query === "function") {
616
+ try {
617
+ const status = await nav.permissions.query({ name: "notifications" });
618
+ if (status.state === "denied" && Notification.permission === "default") {
619
+ permissionsHeadlessHint = true;
620
+ }
621
+ } catch {
622
+ permissionsHeadlessHint = null;
623
+ }
624
+ }
625
+ const timeToCaptureMs = context.env.performance?.timeOrigin ? Math.round(Date.now() - context.env.performance.timeOrigin) : null;
626
+ return {
627
+ status: "ok",
628
+ durationMs: now(context.env) - started,
629
+ value: {
630
+ webdriver,
631
+ headlessHints: hints,
632
+ uaConsistency,
633
+ notificationPermission,
634
+ permissionsHeadlessHint,
635
+ timeToCaptureMs
636
+ }
637
+ };
638
+ } catch (error) {
639
+ return {
640
+ status: "blocked",
641
+ durationMs: now(context.env) - started,
642
+ error: error instanceof Error ? error.message : String(error)
643
+ };
644
+ }
645
+ }
646
+ };
647
+
648
+ // src/signals/screen.ts
649
+ function getMediaPreference(runtime, query) {
650
+ if (typeof runtime.matchMedia !== "function") {
651
+ return "unsupported";
652
+ }
653
+ try {
654
+ return runtime.matchMedia(query).matches;
655
+ } catch {
656
+ return "unsupported";
657
+ }
658
+ }
659
+ var screenCollector = {
660
+ name: "screen",
661
+ tier: "core",
662
+ supportsSync: true,
663
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
664
+ async collect(context) {
665
+ return this.collectSync(context);
666
+ },
667
+ collectSync(context) {
668
+ const started = now(context.env);
669
+ const screen2 = context.env.screen;
670
+ const nav = context.env.navigator;
671
+ const runtime = context.env.global;
672
+ if (!screen2) {
673
+ return { status: "unsupported", durationMs: now(context.env) - started };
674
+ }
675
+ return {
676
+ status: "ok",
677
+ durationMs: now(context.env) - started,
678
+ value: {
679
+ width: screen2.width,
680
+ height: screen2.height,
681
+ availWidth: screen2.availWidth,
682
+ availHeight: screen2.availHeight,
683
+ colorDepth: screen2.colorDepth,
684
+ pixelDepth: screen2.pixelDepth,
685
+ orientationType: screen2.orientation.type,
686
+ orientationAngle: screen2.orientation.angle,
687
+ maxTouchPoints: nav?.maxTouchPoints ?? 0,
688
+ devicePixelRatio: typeof runtime.devicePixelRatio === "number" ? runtime.devicePixelRatio : void 0,
689
+ colorGamutP3: getMediaPreference(runtime, "(color-gamut: p3)"),
690
+ prefersReducedMotion: getMediaPreference(runtime, "(prefers-reduced-motion: reduce)"),
691
+ prefersContrastMore: getMediaPreference(runtime, "(prefers-contrast: more)"),
692
+ forcedColorsActive: getMediaPreference(runtime, "(forced-colors: active)")
693
+ }
694
+ };
695
+ }
696
+ };
697
+
698
+ // src/signals/storage.ts
699
+ function checkStorageFromGlobal(runtime, name) {
700
+ try {
701
+ const target = runtime[name];
702
+ if (!target) {
703
+ return "unsupported";
704
+ }
705
+ const key = "__bf_probe__";
706
+ target.setItem(key, "1");
707
+ target.removeItem(key);
708
+ return "available";
709
+ } catch {
710
+ return "blocked";
711
+ }
712
+ }
713
+ var storageCollector = {
714
+ name: "storage",
715
+ tier: "core",
716
+ supportsSync: true,
717
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
718
+ async collect(context) {
719
+ return this.collectSync(context);
720
+ },
721
+ collectSync(context) {
722
+ const started = now(context.env);
723
+ const nav = context.env.navigator;
724
+ const runtime = context.env.global;
725
+ return {
726
+ status: "ok",
727
+ durationMs: now(context.env) - started,
728
+ value: {
729
+ cookiesEnabled: nav?.cookieEnabled ?? false,
730
+ localStorage: checkStorageFromGlobal(runtime, "localStorage"),
731
+ sessionStorage: checkStorageFromGlobal(runtime, "sessionStorage"),
732
+ indexedDb: typeof runtime.indexedDB === "undefined" ? "unsupported" : "available",
733
+ openDatabase: typeof runtime.openDatabase === "undefined" ? "unsupported" : "available",
734
+ pdfViewerEnabled: nav?.pdfViewerEnabled ?? false
735
+ }
736
+ };
737
+ }
738
+ };
739
+
740
+ // src/signals/ua.ts
741
+ var uaCollector = {
742
+ name: "ua",
743
+ tier: "core",
744
+ supportsSync: true,
745
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
746
+ async collect(context) {
747
+ return this.collectSync(context);
748
+ },
749
+ collectSync(context) {
750
+ const started = now(context.env);
751
+ const nav = context.env.navigator;
752
+ if (!nav) {
753
+ return { status: "unsupported", durationMs: now(context.env) - started };
754
+ }
755
+ return {
756
+ status: "ok",
757
+ durationMs: now(context.env) - started,
758
+ value: {
759
+ userAgent: nav.userAgent,
760
+ appVersion: nav.appVersion,
761
+ vendor: nav.vendor,
762
+ platform: nav.platform,
763
+ webdriver: nav.webdriver,
764
+ maxTouchPoints: nav.maxTouchPoints,
765
+ cookieEnabled: nav.cookieEnabled,
766
+ vendorSub: nav.vendorSub
767
+ }
768
+ };
769
+ }
770
+ };
771
+
772
+ // src/signals/uaHints.ts
773
+ var uaHintsCollector = {
774
+ name: "uaHints",
775
+ tier: "core",
776
+ supportsSync: false,
777
+ async collect(context) {
778
+ const started = now(context.env);
779
+ const userAgentData = context.env.navigator?.userAgentData;
780
+ if (!userAgentData) {
781
+ return { status: "unsupported", durationMs: now(context.env) - started };
782
+ }
783
+ try {
784
+ const highEntropy = userAgentData.getHighEntropyValues ? await userAgentData.getHighEntropyValues([...DEFAULT_UA_HINTS]) : void 0;
785
+ return {
786
+ status: "ok",
787
+ durationMs: now(context.env) - started,
788
+ value: {
789
+ brands: userAgentData.brands ?? [],
790
+ mobile: Boolean(userAgentData.mobile),
791
+ platform: userAgentData.platform,
792
+ ...highEntropy
793
+ }
794
+ };
795
+ } catch (error) {
796
+ return {
797
+ status: "blocked",
798
+ durationMs: now(context.env) - started,
799
+ error: error instanceof Error ? error.message : String(error)
800
+ };
801
+ }
802
+ }
803
+ };
804
+
805
+ // src/signals/webgl.ts
806
+ function getWebGlContext(doc) {
807
+ const canvas = doc.createElement("canvas");
808
+ return canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
809
+ }
810
+ var webglCollector = {
811
+ name: "webgl",
812
+ tier: "core",
813
+ supportsSync: true,
814
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires Promise return; delegates synchronously.
815
+ async collect(context) {
816
+ return this.collectSync(context);
817
+ },
818
+ collectSync(context) {
819
+ const started = now(context.env);
820
+ const doc = context.env.document;
821
+ if (!doc || !hasCanvas(context.env)) {
822
+ return { status: "unsupported", durationMs: now(context.env) - started };
823
+ }
824
+ const gl = getWebGlContext(doc);
825
+ if (!gl) {
826
+ return { status: "unsupported", durationMs: now(context.env) - started };
827
+ }
828
+ const debugExtension = gl.getExtension("WEBGL_debug_renderer_info");
829
+ const renderer = debugExtension ? gl.getParameter(debugExtension.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
830
+ const vendor = debugExtension ? gl.getParameter(debugExtension.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
831
+ return {
832
+ status: "ok",
833
+ durationMs: now(context.env) - started,
834
+ value: {
835
+ vendor,
836
+ renderer,
837
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
838
+ version: gl.getParameter(gl.VERSION),
839
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
840
+ shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
841
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
842
+ maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
843
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
844
+ maxViewportDims: gl.getParameter(gl.MAX_VIEWPORT_DIMS),
845
+ extensions: gl.getSupportedExtensions() ?? []
846
+ }
847
+ };
848
+ }
849
+ };
850
+
851
+ // src/signals/webrtc.ts
852
+ function extractSdpSummary(sdp) {
853
+ const codecs = /* @__PURE__ */ new Set();
854
+ const extmaps = /* @__PURE__ */ new Set();
855
+ for (const line of sdp.split(/\r?\n/)) {
856
+ if (line.startsWith("a=rtpmap:")) {
857
+ const payload = line.slice("a=rtpmap:".length);
858
+ codecs.add(payload.split(" ", 2)[1] ?? line);
859
+ }
860
+ if (line.startsWith("a=extmap:")) {
861
+ extmaps.add(line.slice("a=extmap:".length));
862
+ }
863
+ }
864
+ return {
865
+ codecs: [...codecs].sort(),
866
+ extmaps: [...extmaps].sort()
867
+ };
868
+ }
869
+ var webrtcCollector = {
870
+ name: "webrtc",
871
+ tier: "extended",
872
+ supportsSync: false,
873
+ async collect(context) {
874
+ const started = now(context.env);
875
+ const g = context.env.global;
876
+ const PeerConnection = g.RTCPeerConnection ?? g.webkitRTCPeerConnection;
877
+ if (!PeerConnection) {
878
+ return { status: "unsupported", durationMs: now(context.env) - started };
879
+ }
880
+ let peer;
881
+ try {
882
+ peer = new PeerConnection();
883
+ peer.createDataChannel("bf");
884
+ const offer = await peer.createOffer();
885
+ const summary = extractSdpSummary(offer.sdp ?? "");
886
+ return {
887
+ status: "ok",
888
+ durationMs: now(context.env) - started,
889
+ value: summary
890
+ };
891
+ } catch (error) {
892
+ return {
893
+ status: "blocked",
894
+ durationMs: now(context.env) - started,
895
+ error: error instanceof Error ? error.message : String(error)
896
+ };
897
+ } finally {
898
+ peer?.close();
899
+ }
900
+ }
901
+ };
902
+
903
+ // src/signals/index.ts
904
+ var signalCollectors = [
905
+ uaCollector,
906
+ uaHintsCollector,
907
+ localeCollector,
908
+ screenCollector,
909
+ hardwareCollector,
910
+ storageCollector,
911
+ fontsCollector,
912
+ canvasCollector,
913
+ webglCollector,
914
+ audioCollector,
915
+ mediaDevicesCollector,
916
+ permissionsCollector,
917
+ webrtcCollector,
918
+ frameInfoCollector,
919
+ networkInfoCollector,
920
+ paymentSupportCollector,
921
+ referrerInfoCollector,
922
+ navigationInfoCollector,
923
+ riskSignalsCollector
924
+ ];
925
+ var collectorMap = new Map(signalCollectors.map((collector) => [collector.name, collector]));
926
+
927
+ // src/core/antiSpoof.ts
928
+ function asRecord(value) {
929
+ return value && typeof value === "object" ? value : void 0;
930
+ }
931
+ function asString(value) {
932
+ return typeof value === "string" && value.length > 0 ? value : void 0;
933
+ }
934
+ function asNumber(value) {
935
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
936
+ }
937
+ function analyseSignals(signals) {
938
+ const anomalies = [];
939
+ const automationHints = [];
940
+ const ua = asRecord(signals.ua?.value);
941
+ const uaHints = asRecord(signals.uaHints?.value);
942
+ const hardware = asRecord(signals.hardware?.value);
943
+ const screen2 = asRecord(signals.screen?.value);
944
+ const userAgent = asString(ua?.userAgent)?.toLowerCase() ?? "";
945
+ const platform = asString(ua?.platform)?.toLowerCase() ?? "";
946
+ const hintPlatform = asString(uaHints?.platform)?.toLowerCase() ?? "";
947
+ const webdriver = Boolean(ua?.webdriver);
948
+ const maxTouchPoints = asNumber(screen2?.maxTouchPoints) ?? asNumber(ua?.maxTouchPoints) ?? 0;
949
+ const width = asNumber(screen2?.width) ?? 0;
950
+ const height = asNumber(screen2?.height) ?? 0;
951
+ const deviceMemory = asNumber(hardware?.deviceMemory);
952
+ if (webdriver) {
953
+ automationHints.push("navigator_webdriver");
954
+ }
955
+ if (userAgent.includes("headless")) {
956
+ automationHints.push("headless_user_agent");
957
+ }
958
+ if (platform && hintPlatform && !platform.includes(hintPlatform) && !hintPlatform.includes(platform)) {
959
+ anomalies.push("ua_platform_mismatch");
960
+ }
961
+ if (userAgent.includes("iphone") && maxTouchPoints === 0) {
962
+ anomalies.push("touch_claim_without_touch_points");
963
+ }
964
+ if (userAgent.includes("android") && width > 0 && width >= 1600) {
965
+ anomalies.push("mobile_ua_desktop_screen");
966
+ }
967
+ if (userAgent.includes("windows") && platform.includes("mac")) {
968
+ anomalies.push("windows_ua_mac_platform");
969
+ }
970
+ if (deviceMemory !== void 0 && (deviceMemory < 0.25 || deviceMemory > 64)) {
971
+ anomalies.push("implausible_device_memory");
972
+ }
973
+ if (width > 0 && height > 0 && width < 200 && height < 200) {
974
+ anomalies.push("tiny_screen_geometry");
975
+ }
976
+ const anomalyPenalty = anomalies.length * 0.12;
977
+ const automationPenalty = automationHints.length * 0.18;
978
+ const score = Math.max(0, Math.min(1, Number((1 - anomalyPenalty - automationPenalty).toFixed(3))));
979
+ return {
980
+ score,
981
+ anomalies,
982
+ automationHints
983
+ };
984
+ }
985
+
986
+ // src/core/canonicalize.ts
987
+ function normalizeNumber(value) {
988
+ if (!Number.isFinite(value)) {
989
+ return value;
990
+ }
991
+ return Number(value.toFixed(6));
992
+ }
993
+ function canonicalizeInner(value) {
994
+ if (value === void 0) {
995
+ return "undefined";
996
+ }
997
+ if (value === null) {
998
+ return null;
999
+ }
1000
+ if (typeof value === "number") {
1001
+ return normalizeNumber(value);
1002
+ }
1003
+ if (typeof value === "bigint") {
1004
+ return value.toString();
1005
+ }
1006
+ if (typeof value === "boolean" || typeof value === "string") {
1007
+ return value;
1008
+ }
1009
+ if (Array.isArray(value)) {
1010
+ return value.map((entry) => canonicalizeInner(entry));
1011
+ }
1012
+ if (value instanceof Date) {
1013
+ return value.toISOString();
1014
+ }
1015
+ if (typeof value === "object") {
1016
+ const input = value;
1017
+ const output = {};
1018
+ for (const key of Object.keys(input).sort()) {
1019
+ output[key] = canonicalizeInner(input[key]);
1020
+ }
1021
+ return output;
1022
+ }
1023
+ return String(value);
1024
+ }
1025
+ function canonicalizeValue(value) {
1026
+ return canonicalizeInner(value);
1027
+ }
1028
+ function canonicalizeToString(value) {
1029
+ return JSON.stringify(canonicalizeValue(value));
1030
+ }
1031
+
1032
+ // src/core/confidence.ts
1033
+ var SIGNAL_WEIGHTS = {
1034
+ ua: 0.5,
1035
+ uaHints: 0.7,
1036
+ locale: 0.5,
1037
+ screen: 0.8,
1038
+ hardware: 0.7,
1039
+ storage: 0.4,
1040
+ fonts: 0.9,
1041
+ canvas: 1,
1042
+ webgl: 1,
1043
+ audio: 0.9,
1044
+ mediaDevices: 0.6,
1045
+ permissions: 0.4,
1046
+ webrtc: 0.8,
1047
+ frameInfo: 0.3,
1048
+ networkInfo: 0.6,
1049
+ paymentSupport: 0.2,
1050
+ referrerInfo: 0.3,
1051
+ navigationInfo: 0.4,
1052
+ riskSignals: 0.7
1053
+ };
1054
+ function computeConfidence(signals, antiSpoof) {
1055
+ let earned = 0;
1056
+ let available = 0;
1057
+ for (const [name, signal] of Object.entries(signals)) {
1058
+ const weight = SIGNAL_WEIGHTS[name];
1059
+ available += weight;
1060
+ if (signal.status === "ok") {
1061
+ earned += weight;
1062
+ continue;
1063
+ }
1064
+ if (signal.status === "unsupported" || signal.status === "blocked") {
1065
+ earned += weight * 0.35;
1066
+ continue;
1067
+ }
1068
+ if (signal.status === "timeout") {
1069
+ earned += weight * 0.15;
1070
+ }
1071
+ }
1072
+ if (available === 0) {
1073
+ return 0;
1074
+ }
1075
+ const base = earned / available;
1076
+ const adjusted = base * (0.65 + antiSpoof.score * 0.35);
1077
+ return Math.max(0, Math.min(1, Number(adjusted.toFixed(3))));
1078
+ }
1079
+
1080
+ // src/core/events.ts
1081
+ var FingerprintEmitter = class {
1082
+ listeners = /* @__PURE__ */ new Map();
1083
+ /**
1084
+ * Register an event listener.
1085
+ *
1086
+ * @typeParam K - Event name.
1087
+ * @param event - The event to listen for.
1088
+ * @param cb - Callback invoked on emit.
1089
+ * @returns An unsubscribe function.
1090
+ */
1091
+ on(event, cb) {
1092
+ const bucket = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
1093
+ bucket.add(cb);
1094
+ this.listeners.set(event, bucket);
1095
+ return () => {
1096
+ bucket.delete(cb);
1097
+ if (bucket.size === 0) {
1098
+ this.listeners.delete(event);
1099
+ }
1100
+ };
1101
+ }
1102
+ /**
1103
+ * Emit an event to all registered listeners.
1104
+ *
1105
+ * @typeParam K - Event name.
1106
+ * @param event - The event to emit.
1107
+ * @param payload - Payload matching the event's type.
1108
+ */
1109
+ emit(event, payload) {
1110
+ const bucket = this.listeners.get(event);
1111
+ if (!bucket) {
1112
+ return;
1113
+ }
1114
+ for (const listener of bucket) {
1115
+ listener(payload);
1116
+ }
1117
+ }
1118
+ };
1119
+
1120
+ // src/core/hash.ts
1121
+ var K = [
1122
+ 1116352408,
1123
+ 1899447441,
1124
+ -1245643825,
1125
+ -373957723,
1126
+ 961987163,
1127
+ 1508970993,
1128
+ -1841331548,
1129
+ -1424204075,
1130
+ -670586216,
1131
+ 310598401,
1132
+ 607225278,
1133
+ 1426881987,
1134
+ 1925078388,
1135
+ -2132889090,
1136
+ -1680079193,
1137
+ -1046744716,
1138
+ -459576895,
1139
+ -272742522,
1140
+ 264347078,
1141
+ 604807628,
1142
+ 770255983,
1143
+ 1249150122,
1144
+ 1555081692,
1145
+ 1996064986,
1146
+ -1740746414,
1147
+ -1473132947,
1148
+ -1341970488,
1149
+ -1084653625,
1150
+ -958395405,
1151
+ -710438585,
1152
+ 113926993,
1153
+ 338241895,
1154
+ 666307205,
1155
+ 773529912,
1156
+ 1294757372,
1157
+ 1396182291,
1158
+ 1695183700,
1159
+ 1986661051,
1160
+ -2117940946,
1161
+ -1838011259,
1162
+ -1564481375,
1163
+ -1474664885,
1164
+ -1035236496,
1165
+ -949202525,
1166
+ -778901479,
1167
+ -694614492,
1168
+ -200395387,
1169
+ 275423344,
1170
+ 430227734,
1171
+ 506948616,
1172
+ 659060556,
1173
+ 883997877,
1174
+ 958139571,
1175
+ 1322822218,
1176
+ 1537002063,
1177
+ 1747873779,
1178
+ 1955562222,
1179
+ 2024104815,
1180
+ -2067236844,
1181
+ -1933114872,
1182
+ -1866530822,
1183
+ -1538233109,
1184
+ -1090935817,
1185
+ -965641998
1186
+ ];
1187
+ function rightRotate(value, amount) {
1188
+ return value >>> amount | value << 32 - amount;
1189
+ }
1190
+ function utf8Encode(input) {
1191
+ return new TextEncoder().encode(input);
1192
+ }
1193
+ function sha256Hex(input) {
1194
+ const bytes = utf8Encode(input);
1195
+ const bitLength = bytes.length * 8;
1196
+ const withPaddingLength = bytes.length + 9 + 63 >> 6 << 6;
1197
+ const padded = new Uint8Array(withPaddingLength);
1198
+ padded.set(bytes);
1199
+ padded[bytes.length] = 128;
1200
+ const view = new DataView(padded.buffer);
1201
+ view.setUint32(withPaddingLength - 4, bitLength, false);
1202
+ let h0 = 1779033703;
1203
+ let h1 = 3144134277;
1204
+ let h2 = 1013904242;
1205
+ let h3 = 2773480762;
1206
+ let h4 = 1359893119;
1207
+ let h5 = 2600822924;
1208
+ let h6 = 528734635;
1209
+ let h7 = 1541459225;
1210
+ const w = new Uint32Array(64);
1211
+ for (let offset = 0; offset < padded.length; offset += 64) {
1212
+ for (let i = 0; i < 16; i += 1) {
1213
+ w[i] = view.getUint32(offset + i * 4, false);
1214
+ }
1215
+ for (let i = 16; i < 64; i += 1) {
1216
+ const s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ w[i - 15] >>> 3;
1217
+ const s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ w[i - 2] >>> 10;
1218
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
1219
+ }
1220
+ let a = h0;
1221
+ let b = h1;
1222
+ let c = h2;
1223
+ let d = h3;
1224
+ let e = h4;
1225
+ let f = h5;
1226
+ let g = h6;
1227
+ let h = h7;
1228
+ for (let i = 0; i < 64; i += 1) {
1229
+ const s1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
1230
+ const ch = e & f ^ ~e & g;
1231
+ const temp1 = h + s1 + ch + K[i] + w[i] >>> 0;
1232
+ const s0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
1233
+ const maj = a & b ^ a & c ^ b & c;
1234
+ const temp2 = s0 + maj >>> 0;
1235
+ h = g;
1236
+ g = f;
1237
+ f = e;
1238
+ e = d + temp1 >>> 0;
1239
+ d = c;
1240
+ c = b;
1241
+ b = a;
1242
+ a = temp1 + temp2 >>> 0;
1243
+ }
1244
+ h0 = h0 + a >>> 0;
1245
+ h1 = h1 + b >>> 0;
1246
+ h2 = h2 + c >>> 0;
1247
+ h3 = h3 + d >>> 0;
1248
+ h4 = h4 + e >>> 0;
1249
+ h5 = h5 + f >>> 0;
1250
+ h6 = h6 + g >>> 0;
1251
+ h7 = h7 + h >>> 0;
1252
+ }
1253
+ return [h0, h1, h2, h3, h4, h5, h6, h7].map((part) => part.toString(16).padStart(8, "0")).join("");
1254
+ }
1255
+
1256
+ // src/core/options.ts
1257
+ function withDefaultOptions(options) {
1258
+ return {
1259
+ debug: false,
1260
+ extended: false,
1261
+ timeoutMs: DEFAULT_TIMEOUT_MS,
1262
+ ...options
1263
+ };
1264
+ }
1265
+ function resolveSignalSet(options, mode = "async") {
1266
+ const resolved = withDefaultOptions(options);
1267
+ const seed = resolved.extended ? [...CORE_SIGNALS, ...EXTENDED_SIGNALS] : [...CORE_SIGNALS];
1268
+ const allowed = mode === "sync" ? seed.filter((signal) => SYNC_SIGNALS.includes(signal)) : seed;
1269
+ const included = resolved.include ? allowed.filter((signal) => resolved.include?.includes(signal)) : allowed;
1270
+ const excluded = new Set(resolved.exclude ?? []);
1271
+ return included.filter((signal) => !excluded.has(signal));
1272
+ }
1273
+
1274
+ // src/core/upload.ts
1275
+ async function uploadFingerprint(env, result, options) {
1276
+ const fetchImpl = options.endpoint && env.fetch;
1277
+ if (!fetchImpl) {
1278
+ throw new Error("Fetch is not available in this runtime.");
1279
+ }
1280
+ return fetchImpl(options.endpoint, {
1281
+ method: "POST",
1282
+ headers: {
1283
+ "content-type": "application/json",
1284
+ ...options.headers
1285
+ },
1286
+ body: JSON.stringify({
1287
+ fingerprint: result,
1288
+ ...options.bodyExtras
1289
+ })
1290
+ });
1291
+ }
1292
+
1293
+ // src/version.ts
1294
+ var LIBRARY_VERSION = "0.1.0";
1295
+ var SCHEMA_VERSION = 1;
1296
+
1297
+ // src/client.ts
1298
+ function toTimedOutResult(durationMs) {
1299
+ return {
1300
+ status: "timeout",
1301
+ durationMs,
1302
+ error: "Signal collection timed out."
1303
+ };
1304
+ }
1305
+ async function runCollectorWithTimeout(collector, env, options, warn) {
1306
+ const started = env.performance?.now?.() ?? Date.now();
1307
+ if (options.abortSignal?.aborted) {
1308
+ return {
1309
+ status: "error",
1310
+ durationMs: 0,
1311
+ error: "Collection aborted before signal execution started."
1312
+ };
1313
+ }
1314
+ let clearTimeoutHandle = () => {
1315
+ };
1316
+ const timeoutPromise = new Promise((resolve) => {
1317
+ const timeout = setTimeout(() => {
1318
+ resolve(toTimedOutResult((env.performance?.now?.() ?? Date.now()) - started));
1319
+ }, options.timeoutMs);
1320
+ clearTimeoutHandle = () => clearTimeout(timeout);
1321
+ options.abortSignal?.addEventListener(
1322
+ "abort",
1323
+ () => {
1324
+ clearTimeoutHandle();
1325
+ resolve({
1326
+ status: "error",
1327
+ durationMs: (env.performance?.now?.() ?? Date.now()) - started,
1328
+ error: "Collection aborted."
1329
+ });
1330
+ },
1331
+ { once: true }
1332
+ );
1333
+ });
1334
+ const resultPromise = collector.collect({
1335
+ env,
1336
+ options,
1337
+ warn
1338
+ });
1339
+ return Promise.race([resultPromise, timeoutPromise]).finally(() => {
1340
+ clearTimeoutHandle();
1341
+ });
1342
+ }
1343
+ function buildResult(signals, warnings, requestedSignals, elapsedMs) {
1344
+ const componentHashes = {};
1345
+ for (const [name, signal] of Object.entries(signals)) {
1346
+ if (signal.value === void 0) {
1347
+ continue;
1348
+ }
1349
+ componentHashes[name] = sha256Hex(canonicalizeToString(signal.value));
1350
+ }
1351
+ const thumbprint = sha256Hex(
1352
+ canonicalizeToString(
1353
+ Object.keys(componentHashes).sort().map((name) => [name, componentHashes[name]])
1354
+ )
1355
+ );
1356
+ const antiSpoof = analyseSignals(signals);
1357
+ const confidence = computeConfidence(signals, antiSpoof);
1358
+ return {
1359
+ schemaVersion: SCHEMA_VERSION,
1360
+ libraryVersion: LIBRARY_VERSION,
1361
+ thumbprint,
1362
+ confidence,
1363
+ componentHashes,
1364
+ signals,
1365
+ antiSpoof,
1366
+ warnings,
1367
+ diagnostics: {
1368
+ requestedSignals,
1369
+ completedSignals: Object.keys(signals).sort(),
1370
+ elapsedMs: Number(elapsedMs.toFixed(3))
1371
+ }
1372
+ };
1373
+ }
1374
+ async function createFingerprintClient(options) {
1375
+ const baseOptions = withDefaultOptions(options);
1376
+ const emitter = new FingerprintEmitter();
1377
+ return {
1378
+ on(event, cb) {
1379
+ return emitter.on(event, cb);
1380
+ },
1381
+ async collect(overrides) {
1382
+ const resolved = withDefaultOptions({ ...baseOptions, ...overrides });
1383
+ const env = getBrowserEnv();
1384
+ const signalNames = resolveSignalSet(resolved, "async");
1385
+ const collected = {};
1386
+ const warnings = [];
1387
+ const started = env.performance?.now?.() ?? Date.now();
1388
+ const warn = (message, signal) => {
1389
+ warnings.push(message);
1390
+ emitter.emit("warning", { signal, message });
1391
+ };
1392
+ let completed = 0;
1393
+ await Promise.all(
1394
+ signalNames.map(async (name) => {
1395
+ const collector = collectorMap.get(name);
1396
+ if (!collector) {
1397
+ warn(`No collector registered for signal "${name}".`, name);
1398
+ return;
1399
+ }
1400
+ const result = await runCollectorWithTimeout(collector, env, resolved, warn);
1401
+ collected[name] = result;
1402
+ completed += 1;
1403
+ emitter.emit("progress", {
1404
+ completed,
1405
+ total: signalNames.length,
1406
+ signal: name,
1407
+ result
1408
+ });
1409
+ })
1410
+ );
1411
+ const output = buildResult(collected, warnings, signalNames, (env.performance?.now?.() ?? Date.now()) - started);
1412
+ emitter.emit("complete", { result: output });
1413
+ return output;
1414
+ },
1415
+ collectSync(overrides) {
1416
+ const resolved = withDefaultOptions({ ...baseOptions, ...overrides });
1417
+ const env = getBrowserEnv();
1418
+ const signalNames = resolveSignalSet(resolved, "sync");
1419
+ const collected = {};
1420
+ const warnings = [];
1421
+ const started = env.performance?.now?.() ?? Date.now();
1422
+ const warn = (message, signal) => {
1423
+ warnings.push(message);
1424
+ emitter.emit("warning", { signal, message });
1425
+ };
1426
+ for (const name of signalNames) {
1427
+ const collector = collectorMap.get(name);
1428
+ if (!collector?.collectSync) {
1429
+ warn(`Signal "${name}" is not available in synchronous mode.`, name);
1430
+ continue;
1431
+ }
1432
+ collected[name] = collector.collectSync({
1433
+ env,
1434
+ options: resolved,
1435
+ warn
1436
+ });
1437
+ }
1438
+ return buildResult(collected, warnings, signalNames, (env.performance?.now?.() ?? Date.now()) - started);
1439
+ },
1440
+ upload(result, uploadOptions) {
1441
+ return uploadFingerprint(getBrowserEnv(), result, uploadOptions);
1442
+ }
1443
+ };
1444
+ }
1445
+ export {
1446
+ LIBRARY_VERSION,
1447
+ SCHEMA_VERSION,
1448
+ createFingerprintClient
1449
+ };