@ianmenethil/zp-devicefp 0.1.1 → 0.2.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.
@@ -22,6 +22,35 @@ function hasCanvas(env) {
22
22
  return Boolean(env.document?.createElement?.("canvas").getContext);
23
23
  }
24
24
 
25
+ // src/signals/adblock.ts
26
+ var adblockCollector = {
27
+ name: "adblock",
28
+ tier: "extended",
29
+ supportsSync: false,
30
+ async collect(context) {
31
+ const started = now(context.env);
32
+ const doc = context.env.document;
33
+ if (!doc || !hasDom(context.env)) {
34
+ return { status: "unsupported", durationMs: now(context.env) - started };
35
+ }
36
+ return new Promise((resolve) => {
37
+ const bait = doc.createElement("div");
38
+ bait.innerHTML = "\xA0";
39
+ bait.className = "adsbox";
40
+ doc.body.appendChild(bait);
41
+ setTimeout(() => {
42
+ const adblockEnabled = bait.offsetHeight === 0;
43
+ doc.body.removeChild(bait);
44
+ resolve({
45
+ status: "ok",
46
+ durationMs: now(context.env) - started,
47
+ value: { adblockEnabled }
48
+ });
49
+ }, 100);
50
+ });
51
+ }
52
+ };
53
+
25
54
  // src/signals/audio.ts
26
55
  function getOfflineAudioContext(env) {
27
56
  const g = env;
@@ -140,7 +169,9 @@ var EXTENDED_SIGNALS = [
140
169
  "paymentSupport",
141
170
  "referrerInfo",
142
171
  "navigationInfo",
143
- "riskSignals"
172
+ "riskSignals",
173
+ "adblock",
174
+ "geolocation"
144
175
  ];
145
176
  var SYNC_SIGNALS = [
146
177
  "ua",
@@ -159,16 +190,76 @@ var SYNC_SIGNALS = [
159
190
  ];
160
191
  var DEFAULT_TIMEOUT_MS = 1500;
161
192
  var DEFAULT_FONT_LIST = [
193
+ "Andale Mono",
194
+ "American Typewriter",
195
+ "Apple Chancery",
162
196
  "Arial",
163
- "Helvetica Neue",
164
- "Times New Roman",
165
- "Georgia",
197
+ "Arial Black",
198
+ "Arial Narrow",
199
+ "Arial Rounded MT Bold",
200
+ "Baskerville",
201
+ "Book Antiqua",
202
+ "Bookman Old Style",
203
+ "Bradley Hand ITC",
204
+ "Calibri",
205
+ "Cambria",
206
+ "Candara",
207
+ "Century",
208
+ "Century Gothic",
209
+ "Century Schoolbook",
210
+ "Chalkboard",
211
+ "Charcoal",
212
+ "Cochin",
213
+ "Comic Sans MS",
214
+ "Consolas",
215
+ "Constantia",
216
+ "Corbel",
217
+ "Courier",
166
218
  "Courier New",
219
+ "Franklin Gothic Medium",
220
+ "Futura",
221
+ "Garamond",
222
+ "Geneva",
223
+ "Georgia",
224
+ "Gill Sans",
225
+ "Gill Sans MT",
226
+ "Helvetica",
227
+ "Hoefler Text",
228
+ "Impact",
229
+ "Lucida Bright",
230
+ "Lucida Console",
231
+ "Lucida Grande",
232
+ "Lucida Sans",
233
+ "MS Gothic",
234
+ "MS PGothic",
235
+ "MS Serif",
236
+ "MS UI Gothic",
237
+ "Meiryo",
238
+ "Meiryo UI",
239
+ "Monaco",
240
+ "Monotype Corsiva",
241
+ "New York",
242
+ "Optima",
243
+ "Palatino",
244
+ "Palatino Linotype",
245
+ "Papyrus",
246
+ "Plantagenet Cherokee",
247
+ "Segoe Print",
248
+ "Segoe Script",
249
+ "Segoe UI",
250
+ "Segoe UI Light",
251
+ "Segoe UI Semibold",
252
+ "Segoe UI Symbol",
253
+ "Symbol",
254
+ "Tahoma",
255
+ "Times",
256
+ "Times New Roman",
167
257
  "Trebuchet MS",
168
258
  "Verdana",
169
- "Tahoma",
170
- "Impact",
171
- "Comic Sans MS"
259
+ "Wingdings",
260
+ "Wingdings 2",
261
+ "Wingdings 3",
262
+ "Zapfino"
172
263
  ];
173
264
  var DEFAULT_PERMISSION_NAMES = ["geolocation", "notifications", "camera", "microphone"];
174
265
  var DEFAULT_UA_HINTS = [
@@ -296,6 +387,44 @@ var frameInfoCollector = {
296
387
  }
297
388
  };
298
389
 
390
+ // src/signals/geolocation.ts
391
+ var geolocationCollector = {
392
+ name: "geolocation",
393
+ tier: "extended",
394
+ supportsSync: false,
395
+ async collect(context) {
396
+ const started = now(context.env);
397
+ const nav = context.env.navigator;
398
+ if (!nav?.geolocation) {
399
+ return { status: "unsupported", durationMs: now(context.env) - started };
400
+ }
401
+ return new Promise((resolve) => {
402
+ nav.geolocation.getCurrentPosition(
403
+ (pos) => {
404
+ resolve({
405
+ status: "ok",
406
+ durationMs: now(context.env) - started,
407
+ value: {
408
+ latitude: pos.coords.latitude,
409
+ longitude: pos.coords.longitude,
410
+ accuracy: pos.coords.accuracy
411
+ }
412
+ });
413
+ },
414
+ (err) => {
415
+ resolve({
416
+ // PERMISSION_DENIED = 1
417
+ status: err.code === 1 ? "blocked" : "error",
418
+ durationMs: now(context.env) - started,
419
+ error: err.message
420
+ });
421
+ },
422
+ { timeout: context.options.timeoutMs, maximumAge: 6e4 }
423
+ );
424
+ });
425
+ }
426
+ };
427
+
299
428
  // src/signals/hardware.ts
300
429
  var hardwareCollector = {
301
430
  name: "hardware",
@@ -311,6 +440,17 @@ var hardwareCollector = {
311
440
  if (!nav) {
312
441
  return { status: "unsupported", durationMs: now(context.env) - started };
313
442
  }
443
+ const doc = context.env.document;
444
+ const g = context.env.global;
445
+ let touchEventCreationSuccessful = false;
446
+ try {
447
+ if (doc) {
448
+ doc.createEvent("TouchEvent");
449
+ touchEventCreationSuccessful = true;
450
+ }
451
+ } catch {
452
+ }
453
+ const onTouchStartAvailable = g != null && "ontouchstart" in g;
314
454
  return {
315
455
  status: "ok",
316
456
  durationMs: now(context.env) - started,
@@ -318,7 +458,9 @@ var hardwareCollector = {
318
458
  hardwareConcurrency: nav.hardwareConcurrency,
319
459
  deviceMemory: nav.deviceMemory,
320
460
  platform: nav.platform,
321
- maxTouchPoints: nav.maxTouchPoints
461
+ maxTouchPoints: nav.maxTouchPoints,
462
+ touchEventCreationSuccessful,
463
+ onTouchStartAvailable
322
464
  }
323
465
  };
324
466
  }
@@ -340,28 +482,36 @@ var localeCollector = {
340
482
  if (!nav || !intl) {
341
483
  return { status: "unsupported", durationMs: now(context.env) - started };
342
484
  }
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
- };
485
+ try {
486
+ const formatter = new intl.DateTimeFormat();
487
+ const options = formatter.resolvedOptions();
488
+ const numberFormatter = new intl.NumberFormat(nav.language);
489
+ const relativeTimeFormatter = typeof intl.RelativeTimeFormat === "function" ? new intl.RelativeTimeFormat(nav.language, { numeric: "auto" }) : void 0;
490
+ const sampleDate = new Date(Date.UTC(2024, 0, 2, 3, 4, 5));
491
+ return {
492
+ status: "ok",
493
+ durationMs: now(context.env) - started,
494
+ value: {
495
+ language: nav.language,
496
+ languages: nav.languages,
497
+ locale: options.locale,
498
+ calendar: options.calendar,
499
+ numberingSystem: options.numberingSystem,
500
+ timeZone: options.timeZone,
501
+ hourCycle: options.hourCycle,
502
+ timeZoneOffsetMinutes: sampleDate.getTimezoneOffset(),
503
+ formattedNumber: numberFormatter.format(123456.789),
504
+ formattedDate: formatter.format(sampleDate),
505
+ formattedRelativeDay: relativeTimeFormatter?.format(-1, "day")
506
+ }
507
+ };
508
+ } catch (error) {
509
+ return {
510
+ status: "error",
511
+ durationMs: now(context.env) - started,
512
+ error: error instanceof Error ? error.message : String(error)
513
+ };
514
+ }
365
515
  }
366
516
  };
367
517
 
@@ -672,6 +822,7 @@ var screenCollector = {
672
822
  if (!screen2) {
673
823
  return { status: "unsupported", durationMs: now(context.env) - started };
674
824
  }
825
+ const orientation = screen2.orientation;
675
826
  return {
676
827
  status: "ok",
677
828
  durationMs: now(context.env) - started,
@@ -682,10 +833,24 @@ var screenCollector = {
682
833
  availHeight: screen2.availHeight,
683
834
  colorDepth: screen2.colorDepth,
684
835
  pixelDepth: screen2.pixelDepth,
685
- orientationType: screen2.orientation.type,
686
- orientationAngle: screen2.orientation.angle,
836
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- orientation is optional at runtime despite the Screen intersection
837
+ ...orientation ? { orientationType: orientation.type, orientationAngle: orientation.angle } : {},
687
838
  maxTouchPoints: nav?.maxTouchPoints ?? 0,
839
+ resolution: `${String(screen2.width)}x${String(screen2.height)}`,
840
+ ratio: screen2.height > 0 ? Math.round(screen2.width / screen2.height * 1e10) / 1e10 : 0,
688
841
  devicePixelRatio: typeof runtime.devicePixelRatio === "number" ? runtime.devicePixelRatio : void 0,
842
+ // Fingerprint2 / Cardinal Commerce has_lied_resolution: availWidth never exceeds width on a real device
843
+ fakedResolution: screen2.availWidth > screen2.width || screen2.availHeight > screen2.height,
844
+ // Cardinal Commerce CCAScreenSize — outerWidth/outerHeight bucket code
845
+ ccaScreenSize: (() => {
846
+ const w = typeof runtime.outerWidth === "number" ? runtime.outerWidth : 0;
847
+ const h = typeof runtime.outerHeight === "number" ? runtime.outerHeight : 0;
848
+ if (w < 390) return "01";
849
+ if (w < 500) return "02";
850
+ if (w < 600) return "03";
851
+ if (h < 600) return "04";
852
+ return "02";
853
+ })(),
689
854
  colorGamutP3: getMediaPreference(runtime, "(color-gamut: p3)"),
690
855
  prefersReducedMotion: getMediaPreference(runtime, "(prefers-reduced-motion: reduce)"),
691
856
  prefersContrastMore: getMediaPreference(runtime, "(prefers-contrast: more)"),
@@ -752,6 +917,16 @@ var uaCollector = {
752
917
  if (!nav) {
753
918
  return { status: "unsupported", durationMs: now(context.env) - started };
754
919
  }
920
+ let plugins = [];
921
+ try {
922
+ plugins = Array.from(nav.plugins).map((p) => {
923
+ const mimes = Array.from({ length: p.length }, (_, i) => p.item(i)).filter((m) => m !== null).map((m) => `${m.type}~${m.suffixes}`).join(",");
924
+ return `${p.name}::${p.description}::${mimes}`;
925
+ });
926
+ } catch {
927
+ }
928
+ const rawDnt = nav.doNotTrack ?? context.env.global.doNotTrack ?? null;
929
+ const doNotTrack = rawDnt === "1" ? "enabled" : rawDnt === "0" ? "disabled" : rawDnt ?? "not_set";
755
930
  return {
756
931
  status: "ok",
757
932
  durationMs: now(context.env) - started,
@@ -763,7 +938,9 @@ var uaCollector = {
763
938
  webdriver: nav.webdriver,
764
939
  maxTouchPoints: nav.maxTouchPoints,
765
940
  cookieEnabled: nav.cookieEnabled,
766
- vendorSub: nav.vendorSub
941
+ vendorSub: nav.vendorSub,
942
+ doNotTrack,
943
+ plugins
767
944
  }
768
945
  };
769
946
  }
@@ -920,7 +1097,9 @@ var signalCollectors = [
920
1097
  paymentSupportCollector,
921
1098
  referrerInfoCollector,
922
1099
  navigationInfoCollector,
923
- riskSignalsCollector
1100
+ riskSignalsCollector,
1101
+ adblockCollector,
1102
+ geolocationCollector
924
1103
  ];
925
1104
  var collectorMap = new Map(signalCollectors.map((collector) => [collector.name, collector]));
926
1105
 
@@ -934,6 +1113,13 @@ function asString(value) {
934
1113
  function asNumber(value) {
935
1114
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
936
1115
  }
1116
+ function canonicalizeLegacyPlatform(legacyPlatform) {
1117
+ if (legacyPlatform.startsWith("win")) return "windows";
1118
+ if (legacyPlatform.startsWith("mac")) return "macos";
1119
+ if (/cros|chrome/.test(legacyPlatform)) return "chrome os";
1120
+ if (/linux|x11/.test(legacyPlatform)) return "linux";
1121
+ return legacyPlatform;
1122
+ }
937
1123
  function analyseSignals(signals) {
938
1124
  const anomalies = [];
939
1125
  const automationHints = [];
@@ -955,8 +1141,12 @@ function analyseSignals(signals) {
955
1141
  if (userAgent.includes("headless")) {
956
1142
  automationHints.push("headless_user_agent");
957
1143
  }
958
- if (platform && hintPlatform && !platform.includes(hintPlatform) && !hintPlatform.includes(platform)) {
959
- anomalies.push("ua_platform_mismatch");
1144
+ if (platform && hintPlatform) {
1145
+ const canonicalLegacy = canonicalizeLegacyPlatform(platform);
1146
+ const linuxFamilyOk = canonicalLegacy === "linux" && (hintPlatform === "linux" || hintPlatform === "android" || hintPlatform === "chrome os");
1147
+ if (!linuxFamilyOk && canonicalLegacy !== hintPlatform) {
1148
+ anomalies.push("ua_platform_mismatch");
1149
+ }
960
1150
  }
961
1151
  if (userAgent.includes("iphone") && maxTouchPoints === 0) {
962
1152
  anomalies.push("touch_claim_without_touch_points");
@@ -976,10 +1166,16 @@ function analyseSignals(signals) {
976
1166
  const anomalyPenalty = anomalies.length * 0.12;
977
1167
  const automationPenalty = automationHints.length * 0.18;
978
1168
  const score = Math.max(0, Math.min(1, Number((1 - anomalyPenalty - automationPenalty).toFixed(3))));
1169
+ const fakedOS = anomalies.includes("ua_platform_mismatch");
1170
+ const riskVal = asRecord(signals.riskSignals?.value);
1171
+ const headlessHints = Array.isArray(riskVal?.headlessHints) ? riskVal.headlessHints : [];
1172
+ const fakedBrowser = headlessHints.includes("missing-chrome-object");
979
1173
  return {
980
1174
  score,
981
1175
  anomalies,
982
- automationHints
1176
+ automationHints,
1177
+ fakedOS,
1178
+ fakedBrowser
983
1179
  };
984
1180
  }
985
1181
 
@@ -1049,7 +1245,9 @@ var SIGNAL_WEIGHTS = {
1049
1245
  paymentSupport: 0.2,
1050
1246
  referrerInfo: 0.3,
1051
1247
  navigationInfo: 0.4,
1052
- riskSignals: 0.7
1248
+ riskSignals: 0.7,
1249
+ adblock: 0.3,
1250
+ geolocation: 0.5
1053
1251
  };
1054
1252
  function computeConfidence(signals, antiSpoof) {
1055
1253
  let earned = 0;
@@ -1271,28 +1469,9 @@ function resolveSignalSet(options, mode = "async") {
1271
1469
  return included.filter((signal) => !excluded.has(signal));
1272
1470
  }
1273
1471
 
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
1472
  // src/version.ts
1294
- var LIBRARY_VERSION = "0.1.0";
1295
- var SCHEMA_VERSION = 1;
1473
+ var LIBRARY_VERSION = true ? "0.2.0" : "0.0.0-dev";
1474
+ var SCHEMA_VERSION = 2;
1296
1475
 
1297
1476
  // src/client.ts
1298
1477
  function toTimedOutResult(durationMs) {
@@ -1302,6 +1481,18 @@ function toTimedOutResult(durationMs) {
1302
1481
  error: "Signal collection timed out."
1303
1482
  };
1304
1483
  }
1484
+ function toErrorResult(durationMs, error) {
1485
+ return {
1486
+ status: "error",
1487
+ durationMs,
1488
+ error: error instanceof Error ? error.message : String(error)
1489
+ };
1490
+ }
1491
+ function debugLog(resolved, message) {
1492
+ if (resolved.debug) {
1493
+ console.debug(`[zp-devicefp] ${message}`);
1494
+ }
1495
+ }
1305
1496
  async function runCollectorWithTimeout(collector, env, options, warn) {
1306
1497
  const started = env.performance?.now?.() ?? Date.now();
1307
1498
  if (options.abortSignal?.aborted) {
@@ -1313,31 +1504,36 @@ async function runCollectorWithTimeout(collector, env, options, warn) {
1313
1504
  }
1314
1505
  let clearTimeoutHandle = () => {
1315
1506
  };
1507
+ let removeAbortListener = () => {
1508
+ };
1316
1509
  const timeoutPromise = new Promise((resolve) => {
1317
1510
  const timeout = setTimeout(() => {
1318
1511
  resolve(toTimedOutResult((env.performance?.now?.() ?? Date.now()) - started));
1319
1512
  }, options.timeoutMs);
1320
1513
  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
1514
+ const onAbort = () => {
1515
+ clearTimeoutHandle();
1516
+ resolve({
1517
+ status: "error",
1518
+ durationMs: (env.performance?.now?.() ?? Date.now()) - started,
1519
+ error: "Collection aborted."
1520
+ });
1521
+ };
1522
+ options.abortSignal?.addEventListener("abort", onAbort, { once: true });
1523
+ removeAbortListener = () => options.abortSignal?.removeEventListener("abort", onAbort);
1338
1524
  });
1525
+ const resultPromise = (async () => {
1526
+ try {
1527
+ return await collector.collect({ env, options, warn });
1528
+ } catch (error) {
1529
+ const result = toErrorResult((env.performance?.now?.() ?? Date.now()) - started, error);
1530
+ warn(`Signal "${collector.name}" threw: ${result.error ?? "unknown error"}`, collector.name);
1531
+ return result;
1532
+ }
1533
+ })();
1339
1534
  return Promise.race([resultPromise, timeoutPromise]).finally(() => {
1340
1535
  clearTimeoutHandle();
1536
+ removeAbortListener();
1341
1537
  });
1342
1538
  }
1343
1539
  function buildResult(signals, warnings, requestedSignals, elapsedMs) {
@@ -1399,6 +1595,7 @@ async function createFingerprintClient(options) {
1399
1595
  }
1400
1596
  const result = await runCollectorWithTimeout(collector, env, resolved, warn);
1401
1597
  collected[name] = result;
1598
+ debugLog(resolved, `${name}: ${result.status} (${String(result.durationMs)}ms)`);
1402
1599
  completed += 1;
1403
1600
  emitter.emit("progress", {
1404
1601
  completed,
@@ -1429,21 +1626,34 @@ async function createFingerprintClient(options) {
1429
1626
  warn(`Signal "${name}" is not available in synchronous mode.`, name);
1430
1627
  continue;
1431
1628
  }
1432
- collected[name] = collector.collectSync({
1433
- env,
1434
- options: resolved,
1435
- warn
1436
- });
1629
+ const signalStarted = env.performance?.now?.() ?? Date.now();
1630
+ let result;
1631
+ try {
1632
+ result = collector.collectSync({
1633
+ env,
1634
+ options: resolved,
1635
+ warn
1636
+ });
1637
+ } catch (error) {
1638
+ result = toErrorResult((env.performance?.now?.() ?? Date.now()) - signalStarted, error);
1639
+ warn(`Signal "${name}" threw: ${result.error ?? "unknown error"}`, name);
1640
+ }
1641
+ collected[name] = result;
1642
+ debugLog(resolved, `${name}: ${result.status} (${String(result.durationMs)}ms)`);
1437
1643
  }
1438
1644
  return buildResult(collected, warnings, signalNames, (env.performance?.now?.() ?? Date.now()) - started);
1439
- },
1440
- upload(result, uploadOptions) {
1441
- return uploadFingerprint(getBrowserEnv(), result, uploadOptions);
1442
1645
  }
1443
1646
  };
1444
1647
  }
1445
1648
  export {
1649
+ CORE_SIGNALS,
1650
+ DEFAULT_FONT_LIST,
1651
+ DEFAULT_PERMISSION_NAMES,
1652
+ DEFAULT_TIMEOUT_MS,
1653
+ DEFAULT_UA_HINTS,
1654
+ EXTENDED_SIGNALS,
1446
1655
  LIBRARY_VERSION,
1447
1656
  SCHEMA_VERSION,
1657
+ SYNC_SIGNALS,
1448
1658
  createFingerprintClient
1449
1659
  };