@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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/cdn/zp.dfp.esm.js +1449 -0
- package/dist/cdn/zp.dfp.js +1453 -0
- package/dist/cdn/zp.dfp.manifest.json +34 -0
- package/dist/cdn/zp.dfp.min.js +1 -0
- package/dist/cdn/zp.dfp.obf.js +1 -0
- package/dist/npm/index.cjs +1478 -0
- package/dist/npm/index.d.ts +101 -0
- package/dist/npm/index.mjs +1449 -0
- package/docs/architecture.md +40 -0
- package/docs/privacy.md +20 -0
- package/docs/testing.md +32 -0
- package/package.json +75 -0
|
@@ -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
|
+
};
|