@rester159/blacktip 0.1.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.
Files changed (63) hide show
  1. package/AGENTS.md +249 -0
  2. package/LICENSE +38 -0
  3. package/README.md +234 -0
  4. package/dist/behavioral/calibration.d.ts +145 -0
  5. package/dist/behavioral/calibration.d.ts.map +1 -0
  6. package/dist/behavioral/calibration.js +242 -0
  7. package/dist/behavioral/calibration.js.map +1 -0
  8. package/dist/behavioral-engine.d.ts +156 -0
  9. package/dist/behavioral-engine.d.ts.map +1 -0
  10. package/dist/behavioral-engine.js +521 -0
  11. package/dist/behavioral-engine.js.map +1 -0
  12. package/dist/blacktip.d.ts +289 -0
  13. package/dist/blacktip.d.ts.map +1 -0
  14. package/dist/blacktip.js +1574 -0
  15. package/dist/blacktip.js.map +1 -0
  16. package/dist/browser-core.d.ts +47 -0
  17. package/dist/browser-core.d.ts.map +1 -0
  18. package/dist/browser-core.js +375 -0
  19. package/dist/browser-core.js.map +1 -0
  20. package/dist/cli.d.ts +20 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +226 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/element-finder.d.ts +42 -0
  25. package/dist/element-finder.d.ts.map +1 -0
  26. package/dist/element-finder.js +240 -0
  27. package/dist/element-finder.js.map +1 -0
  28. package/dist/evasion.d.ts +39 -0
  29. package/dist/evasion.d.ts.map +1 -0
  30. package/dist/evasion.js +488 -0
  31. package/dist/evasion.js.map +1 -0
  32. package/dist/fingerprint.d.ts +19 -0
  33. package/dist/fingerprint.d.ts.map +1 -0
  34. package/dist/fingerprint.js +171 -0
  35. package/dist/fingerprint.js.map +1 -0
  36. package/dist/index.d.ts +19 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +14 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/logging.d.ts +13 -0
  41. package/dist/logging.d.ts.map +1 -0
  42. package/dist/logging.js +42 -0
  43. package/dist/logging.js.map +1 -0
  44. package/dist/observability.d.ts +69 -0
  45. package/dist/observability.d.ts.map +1 -0
  46. package/dist/observability.js +189 -0
  47. package/dist/observability.js.map +1 -0
  48. package/dist/proxy-pool.d.ts +101 -0
  49. package/dist/proxy-pool.d.ts.map +1 -0
  50. package/dist/proxy-pool.js +156 -0
  51. package/dist/proxy-pool.js.map +1 -0
  52. package/dist/snapshot.d.ts +59 -0
  53. package/dist/snapshot.d.ts.map +1 -0
  54. package/dist/snapshot.js +91 -0
  55. package/dist/snapshot.js.map +1 -0
  56. package/dist/types.d.ts +243 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +15 -0
  59. package/dist/types.js.map +1 -0
  60. package/examples/01-basic-navigate.ts +40 -0
  61. package/examples/02-login-with-mfa.ts +68 -0
  62. package/examples/03-agent-serve-mode.md +98 -0
  63. package/package.json +62 -0
@@ -0,0 +1,488 @@
1
+ // ── Helper: wrap code in a self-contained IIFE ──
2
+ function iife(body) {
3
+ return `(function(){${body}})();`;
4
+ }
5
+ // ── 1. Navigator overrides ──
6
+ function navigatorOverrides(profile) {
7
+ return iife(`
8
+ // Delete navigator.webdriver — make it undefined like a real browser
9
+ try {
10
+ const nd = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
11
+ if (nd) {
12
+ Object.defineProperty(Navigator.prototype, 'webdriver', {
13
+ get: function() { return undefined; },
14
+ configurable: false,
15
+ enumerable: true,
16
+ });
17
+ }
18
+ } catch(e) {}
19
+
20
+ // Override navigator properties to match profile
21
+ var overrides = {
22
+ hardwareConcurrency: ${profile.hardwareConcurrency},
23
+ deviceMemory: ${profile.deviceMemory},
24
+ maxTouchPoints: ${profile.maxTouchPoints},
25
+ platform: ${JSON.stringify(profile.platform)},
26
+ vendor: ${JSON.stringify(profile.vendor)},
27
+ languages: Object.freeze(${JSON.stringify(profile.languages)}),
28
+ language: ${JSON.stringify(profile.languages[0] || 'en-US')},
29
+ };
30
+
31
+ for (var key in overrides) {
32
+ try {
33
+ Object.defineProperty(Navigator.prototype, key, {
34
+ get: function(val) { return function() { return val; }; }(overrides[key]),
35
+ configurable: false,
36
+ enumerable: true,
37
+ });
38
+ } catch(e) {}
39
+ }
40
+
41
+ // Also override userAgent, appVersion on Navigator.prototype
42
+ try {
43
+ Object.defineProperty(Navigator.prototype, 'userAgent', {
44
+ get: function() { return ${JSON.stringify(profile.userAgent)}; },
45
+ configurable: false,
46
+ enumerable: true,
47
+ });
48
+ } catch(e) {}
49
+
50
+ try {
51
+ // appVersion is userAgent minus "Mozilla/"
52
+ var appVer = ${JSON.stringify(profile.userAgent.replace('Mozilla/', ''))};
53
+ Object.defineProperty(Navigator.prototype, 'appVersion', {
54
+ get: function() { return appVer; },
55
+ configurable: false,
56
+ enumerable: true,
57
+ });
58
+ } catch(e) {}
59
+ `);
60
+ }
61
+ // ── 2. Chrome runtime ──
62
+ function chromeRuntime() {
63
+ return iife(`
64
+ if (!window.chrome) {
65
+ window.chrome = {};
66
+ }
67
+
68
+ if (!window.chrome.runtime) {
69
+ window.chrome.runtime = {
70
+ connect: function(extensionId, connectInfo) {
71
+ // Match Chrome's signature: returns a Port-like object
72
+ return {
73
+ name: (connectInfo && connectInfo.name) || '',
74
+ postMessage: function() {},
75
+ disconnect: function() {},
76
+ onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
77
+ onDisconnect: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
78
+ };
79
+ },
80
+ sendMessage: function(extensionId, message, options, responseCallback) {
81
+ // Chrome throws if no callback and no runtime ID
82
+ if (typeof responseCallback === 'function') {
83
+ setTimeout(function() { responseCallback(undefined); }, 0);
84
+ } else if (typeof options === 'function') {
85
+ setTimeout(function() { options(undefined); }, 0);
86
+ }
87
+ },
88
+ id: undefined,
89
+ getManifest: function() { return {}; },
90
+ getURL: function(path) { return ''; },
91
+ onConnect: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
92
+ onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
93
+ };
94
+ }
95
+
96
+ // chrome.loadTimes
97
+ if (!window.chrome.loadTimes) {
98
+ var startTime = Date.now() / 1000;
99
+ window.chrome.loadTimes = function() {
100
+ return {
101
+ commitLoadTime: startTime,
102
+ connectionInfo: 'h2',
103
+ finishDocumentLoadTime: startTime + 0.3,
104
+ finishLoadTime: startTime + 0.6,
105
+ firstPaintAfterLoadTime: startTime + 0.65,
106
+ firstPaintTime: startTime + 0.35,
107
+ navigationType: 'Other',
108
+ npnNegotiatedProtocol: 'h2',
109
+ requestTime: startTime - 0.1,
110
+ startLoadTime: startTime,
111
+ wasAlternateProtocolAvailable: false,
112
+ wasFetchedViaSpdy: true,
113
+ wasNpnNegotiated: true,
114
+ };
115
+ };
116
+ }
117
+
118
+ // chrome.csi
119
+ if (!window.chrome.csi) {
120
+ window.chrome.csi = function() {
121
+ return {
122
+ onloadT: Date.now(),
123
+ pageT: performance.now(),
124
+ startE: Date.now() - performance.now(),
125
+ tran: 15, // Navigation type: normal
126
+ };
127
+ };
128
+ }
129
+
130
+ // Make chrome non-writable to prevent overwrite detection
131
+ try {
132
+ Object.defineProperty(window, 'chrome', {
133
+ value: window.chrome,
134
+ writable: false,
135
+ configurable: false,
136
+ enumerable: true,
137
+ });
138
+ } catch(e) {}
139
+ `);
140
+ }
141
+ // ── 3. Plugins and MimeTypes ──
142
+ function pluginsAndMimeTypes(profile) {
143
+ const pluginsJson = JSON.stringify(profile.plugins);
144
+ return iife(`
145
+ var pluginData = ${pluginsJson};
146
+
147
+ // Build MimeType and Plugin objects that pass instanceof checks
148
+ var mimeTypes = [];
149
+ var plugins = [];
150
+
151
+ // We need to create objects whose prototype chains match the browser's native ones.
152
+ // PluginArray, Plugin, MimeTypeArray, MimeType prototypes already exist in the DOM.
153
+
154
+ for (var i = 0; i < pluginData.length; i++) {
155
+ var pd = pluginData[i];
156
+ var plugin = Object.create(Plugin.prototype);
157
+
158
+ var pluginMimes = [];
159
+ for (var j = 0; j < pd.mimeTypes.length; j++) {
160
+ var md = pd.mimeTypes[j];
161
+ var mime = Object.create(MimeType.prototype);
162
+ Object.defineProperties(mime, {
163
+ type: { value: md.type, enumerable: true },
164
+ suffixes: { value: md.suffixes, enumerable: true },
165
+ description: { value: md.description, enumerable: true },
166
+ enabledPlugin: { value: plugin, enumerable: true },
167
+ });
168
+ pluginMimes.push(mime);
169
+ mimeTypes.push(mime);
170
+ }
171
+
172
+ Object.defineProperties(plugin, {
173
+ name: { value: pd.name, enumerable: true },
174
+ description: { value: pd.description, enumerable: true },
175
+ filename: { value: pd.filename, enumerable: true },
176
+ length: { value: pluginMimes.length, enumerable: true },
177
+ });
178
+
179
+ // Index access and namedItem for plugin's mimeTypes
180
+ for (var k = 0; k < pluginMimes.length; k++) {
181
+ Object.defineProperty(plugin, k, { value: pluginMimes[k], enumerable: false });
182
+ }
183
+ plugin.item = function(idx) { return pluginMimes[idx] || null; };
184
+ plugin.namedItem = function(name) {
185
+ for (var m = 0; m < pluginMimes.length; m++) {
186
+ if (pluginMimes[m].type === name) return pluginMimes[m];
187
+ }
188
+ return null;
189
+ };
190
+ // Symbol.iterator
191
+ plugin[Symbol.iterator] = function() {
192
+ var _i = 0; var _a = pluginMimes;
193
+ return { next: function() { return _i < _a.length ? { value: _a[_i++], done: false } : { done: true }; } };
194
+ };
195
+
196
+ plugins.push(plugin);
197
+ }
198
+
199
+ // Build PluginArray
200
+ var pluginArray = Object.create(PluginArray.prototype);
201
+ Object.defineProperty(pluginArray, 'length', { value: plugins.length, enumerable: true });
202
+ for (var p = 0; p < plugins.length; p++) {
203
+ Object.defineProperty(pluginArray, p, { value: plugins[p], enumerable: false });
204
+ // Name-based access
205
+ Object.defineProperty(pluginArray, plugins[p].name, { value: plugins[p], enumerable: false });
206
+ }
207
+ pluginArray.item = function(idx) { return plugins[idx] || null; };
208
+ pluginArray.namedItem = function(name) {
209
+ for (var n = 0; n < plugins.length; n++) {
210
+ if (plugins[n].name === name) return plugins[n];
211
+ }
212
+ return null;
213
+ };
214
+ pluginArray.refresh = function() {};
215
+ pluginArray[Symbol.iterator] = function() {
216
+ var _i = 0;
217
+ return { next: function() { return _i < plugins.length ? { value: plugins[_i++], done: false } : { done: true }; } };
218
+ };
219
+
220
+ // Build MimeTypeArray
221
+ var mimeTypeArray = Object.create(MimeTypeArray.prototype);
222
+ Object.defineProperty(mimeTypeArray, 'length', { value: mimeTypes.length, enumerable: true });
223
+ for (var q = 0; q < mimeTypes.length; q++) {
224
+ Object.defineProperty(mimeTypeArray, q, { value: mimeTypes[q], enumerable: false });
225
+ Object.defineProperty(mimeTypeArray, mimeTypes[q].type, { value: mimeTypes[q], enumerable: false });
226
+ }
227
+ mimeTypeArray.item = function(idx) { return mimeTypes[idx] || null; };
228
+ mimeTypeArray.namedItem = function(name) {
229
+ for (var r = 0; r < mimeTypes.length; r++) {
230
+ if (mimeTypes[r].type === name) return mimeTypes[r];
231
+ }
232
+ return null;
233
+ };
234
+ mimeTypeArray[Symbol.iterator] = function() {
235
+ var _i = 0;
236
+ return { next: function() { return _i < mimeTypes.length ? { value: mimeTypes[_i++], done: false } : { done: true }; } };
237
+ };
238
+
239
+ // Override navigator.plugins and navigator.mimeTypes
240
+ Object.defineProperty(Navigator.prototype, 'plugins', {
241
+ get: function() { return pluginArray; },
242
+ configurable: false,
243
+ enumerable: true,
244
+ });
245
+ Object.defineProperty(Navigator.prototype, 'mimeTypes', {
246
+ get: function() { return mimeTypeArray; },
247
+ configurable: false,
248
+ enumerable: true,
249
+ });
250
+ `);
251
+ }
252
+ // ── 4. Permissions override ──
253
+ function permissionsOverride() {
254
+ return iife(`
255
+ var originalQuery = Permissions.prototype.query;
256
+ Permissions.prototype.query = function(permissionDesc) {
257
+ // Notification permission: headless returns 'denied', real Chrome returns 'prompt'
258
+ if (permissionDesc && permissionDesc.name === 'notifications') {
259
+ return Promise.resolve({
260
+ state: 'prompt',
261
+ name: 'notifications',
262
+ onchange: null,
263
+ addEventListener: function() {},
264
+ removeEventListener: function() {},
265
+ dispatchEvent: function() { return true; },
266
+ });
267
+ }
268
+ // For all other permissions, delegate to the original implementation
269
+ return originalQuery.call(this, permissionDesc);
270
+ };
271
+ `);
272
+ }
273
+ // ── 5. WebGL override ──
274
+ function webglOverride(profile) {
275
+ return iife(`
276
+ var VENDOR = ${JSON.stringify(profile.webglVendor)};
277
+ var RENDERER = ${JSON.stringify(profile.webglRenderer)};
278
+
279
+ // Constants for WEBGL_debug_renderer_info
280
+ var UNMASKED_VENDOR_WEBGL = 0x9245;
281
+ var UNMASKED_RENDERER_WEBGL = 0x9246;
282
+
283
+ // Hook getParameter on both WebGL contexts
284
+ var contexts = ['WebGLRenderingContext', 'WebGL2RenderingContext'];
285
+ for (var c = 0; c < contexts.length; c++) {
286
+ var ctx = window[contexts[c]];
287
+ if (!ctx || !ctx.prototype) continue;
288
+
289
+ var origGetParameter = ctx.prototype.getParameter;
290
+ ctx.prototype.getParameter = (function(orig) {
291
+ return function(param) {
292
+ if (param === UNMASKED_VENDOR_WEBGL) return VENDOR;
293
+ if (param === UNMASKED_RENDERER_WEBGL) return RENDERER;
294
+ // Also override VENDOR and RENDERER base params
295
+ if (param === 0x1F00) return VENDOR; // gl.VENDOR
296
+ if (param === 0x1F01) return RENDERER; // gl.RENDERER
297
+ return orig.call(this, param);
298
+ };
299
+ })(origGetParameter);
300
+
301
+ // Hook getExtension to ensure WEBGL_debug_renderer_info is available
302
+ // and returns consistent values
303
+ var origGetExtension = ctx.prototype.getExtension;
304
+ ctx.prototype.getExtension = (function(orig) {
305
+ return function(name) {
306
+ var ext = orig.call(this, name);
307
+ if (name === 'WEBGL_debug_renderer_info') {
308
+ // If extension is null (as in some headless modes), create a shim
309
+ if (!ext) {
310
+ ext = {
311
+ UNMASKED_VENDOR_WEBGL: UNMASKED_VENDOR_WEBGL,
312
+ UNMASKED_RENDERER_WEBGL: UNMASKED_RENDERER_WEBGL,
313
+ };
314
+ }
315
+ return ext;
316
+ }
317
+ return ext;
318
+ };
319
+ })(origGetExtension);
320
+ }
321
+ `);
322
+ }
323
+ // ── 6. Canvas noise ──
324
+ function canvasNoise(profile) {
325
+ // Generate a deterministic seed from the profile name
326
+ let seed = 0;
327
+ for (let i = 0; i < profile.name.length; i++) {
328
+ seed = ((seed << 5) - seed + profile.name.charCodeAt(i)) | 0;
329
+ }
330
+ return iife(`
331
+ // Simple seeded PRNG (mulberry32)
332
+ var seed = ${seed >>> 0};
333
+ function prng() {
334
+ seed |= 0; seed = seed + 0x6D2B79F5 | 0;
335
+ var t = Math.imul(seed ^ seed >>> 15, 1 | seed);
336
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
337
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
338
+ }
339
+
340
+ // Inject subtle noise into ImageData
341
+ function addNoise(imageData) {
342
+ var data = imageData.data;
343
+ // Only perturb a small fraction of pixels for subtlety
344
+ for (var i = 0; i < data.length; i += 4) {
345
+ // Perturb ~2% of pixels
346
+ if (prng() < 0.02) {
347
+ // Pick a random channel (R, G, or B — skip alpha)
348
+ var channel = (prng() * 3) | 0;
349
+ var delta = prng() < 0.5 ? -1 : 1;
350
+ var val = data[i + channel] + delta;
351
+ if (val < 0) val = 0;
352
+ if (val > 255) val = 255;
353
+ data[i + channel] = val;
354
+ }
355
+ }
356
+ return imageData;
357
+ }
358
+
359
+ // Hook toDataURL
360
+ var origToDataURL = HTMLCanvasElement.prototype.toDataURL;
361
+ HTMLCanvasElement.prototype.toDataURL = function() {
362
+ var ctx = this.getContext('2d');
363
+ if (ctx) {
364
+ try {
365
+ var imageData = ctx.getImageData(0, 0, this.width, this.height);
366
+ addNoise(imageData);
367
+ ctx.putImageData(imageData, 0, 0);
368
+ } catch(e) {
369
+ // Canvas may be tainted (cross-origin) — silently skip
370
+ }
371
+ }
372
+ return origToDataURL.apply(this, arguments);
373
+ };
374
+
375
+ // Hook toBlob
376
+ var origToBlob = HTMLCanvasElement.prototype.toBlob;
377
+ HTMLCanvasElement.prototype.toBlob = function() {
378
+ var ctx = this.getContext('2d');
379
+ if (ctx) {
380
+ try {
381
+ var imageData = ctx.getImageData(0, 0, this.width, this.height);
382
+ addNoise(imageData);
383
+ ctx.putImageData(imageData, 0, 0);
384
+ } catch(e) {}
385
+ }
386
+ return origToBlob.apply(this, arguments);
387
+ };
388
+ `);
389
+ }
390
+ // ── 7. AudioContext override ──
391
+ //
392
+ // Adds subtle noise to audio fingerprinting methods (getFloatFrequencyData,
393
+ // OfflineAudioContext.startRendering) so the audio fingerprint isn't a
394
+ // deterministic device-wide identifier. The noise is seeded by the device
395
+ // profile name so it's consistent across sessions for the same profile —
396
+ // a real human's audio fingerprint is stable, not random.
397
+ function audioContextOverride(profile) {
398
+ // Derive a seed from the profile name. Same seed → same PRNG stream →
399
+ // same audio fingerprint across sessions. Using Math.random() here (the
400
+ // pre-v2 behavior) would have made the fingerprint drift every session,
401
+ // which is itself a detectable "this user looks different every time" signal.
402
+ let seed = 0;
403
+ for (let i = 0; i < profile.name.length; i++) {
404
+ seed = ((seed << 5) - seed + profile.name.charCodeAt(i)) | 0;
405
+ }
406
+ return iife(`
407
+ // Mulberry32 PRNG seeded from profile.name — matches the canvas noise
408
+ // seeding approach so a given profile has a stable audio + canvas pair.
409
+ var audioSeed = ${seed >>> 0};
410
+ function audioPrng() {
411
+ audioSeed |= 0; audioSeed = audioSeed + 0x6D2B79F5 | 0;
412
+ var t = Math.imul(audioSeed ^ audioSeed >>> 15, 1 | audioSeed);
413
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
414
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
415
+ }
416
+
417
+ if (typeof AnalyserNode !== 'undefined') {
418
+ var origGetFloat = AnalyserNode.prototype.getFloatFrequencyData;
419
+ AnalyserNode.prototype.getFloatFrequencyData = function(array) {
420
+ origGetFloat.call(this, array);
421
+ for (var i = 0; i < array.length; i++) {
422
+ // Seeded noise, ~0.0001 magnitude — imperceptible to real audio
423
+ // use but breaks deterministic fingerprinting.
424
+ array[i] = array[i] + (audioPrng() * 0.0002 - 0.0001);
425
+ }
426
+ };
427
+ }
428
+
429
+ if (typeof OfflineAudioContext !== 'undefined') {
430
+ var origOfflineRender = OfflineAudioContext.prototype.startRendering;
431
+ OfflineAudioContext.prototype.startRendering = function() {
432
+ return origOfflineRender.call(this).then(function(buffer) {
433
+ for (var ch = 0; ch < buffer.numberOfChannels; ch++) {
434
+ var data = buffer.getChannelData(ch);
435
+ for (var i = 0; i < data.length; i++) {
436
+ data[i] = data[i] + (audioPrng() * 0.0001 - 0.00005);
437
+ }
438
+ }
439
+ return buffer;
440
+ });
441
+ };
442
+ }
443
+ `);
444
+ }
445
+ // ── Public API ──
446
+ /**
447
+ * Generate all evasion scripts for a given device profile.
448
+ * Each script is a self-contained IIFE ready to be injected via
449
+ * `Page.addScriptToEvaluateOnNewDocument()`.
450
+ */
451
+ /**
452
+ * Evasion script generator. With BlackTip v2 running on real Chrome
453
+ * (`channel: 'chrome'`) + patchright's CDP-level stealth patches, most of
454
+ * the original JS-shim evasion is unnecessary and some of it was actively
455
+ * creating signals (see L012 in planning/lessons.md for the chrome.runtime
456
+ * and launch-flag story).
457
+ *
458
+ * What we keep:
459
+ * - canvasNoise: privacy/anti-tracking, adds plausible noise to canvas
460
+ * rendering so we don't get a stable device-wide fingerprint.
461
+ * - audioContextOverride: same story for audio fingerprint.
462
+ *
463
+ * What we removed (and why):
464
+ * - chromeRuntime: real Chrome doesn't expose `chrome.runtime` on regular
465
+ * pages — only on extension-accessible contexts. Our shim was adding
466
+ * it everywhere, which CreepJS catches as `hasBadChromeRuntime: true`.
467
+ * Let patchright + real Chrome handle `window.chrome` naturally.
468
+ * - navigatorOverrides: real Chrome's navigator is already correct when
469
+ * we use `channel: 'chrome'`. Overriding with profile values creates
470
+ * mismatches (e.g., profile says hardwareConcurrency=8 but the actual
471
+ * CPU has 16, and other APIs like Performance.now timing can reveal
472
+ * the truth).
473
+ * - pluginsAndMimeTypes: real Chrome populates navigator.plugins with
474
+ * the real PDF viewer plugin. Our shim was adding fake plugins that
475
+ * didn't match.
476
+ * - permissionsOverride: real Chrome already returns 'prompt' for
477
+ * notifications on most states. Shim only needed on headless Chromium.
478
+ * - webglOverride: real Chrome reports the real GPU through ANGLE. Our
479
+ * shim was replacing AMD Radeon with a profile string, making the
480
+ * fingerprint inconsistent with the real DirectX pipeline signals.
481
+ */
482
+ export function generateEvasionScripts(profile) {
483
+ return [
484
+ canvasNoise(profile),
485
+ audioContextOverride(profile),
486
+ ];
487
+ }
488
+ //# sourceMappingURL=evasion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evasion.js","sourceRoot":"","sources":["../src/evasion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { DeviceProfile } from './types';
2
+ export declare class DeviceProfileManager {
3
+ private profiles;
4
+ constructor();
5
+ getProfile(name: string): DeviceProfile;
6
+ listProfiles(): string[];
7
+ addProfile(name: string, profile: DeviceProfile): void;
8
+ /**
9
+ * Generate a slightly randomized variant of a named profile.
10
+ *
11
+ * Randomization:
12
+ * - Picks a random UA string from a small pool for the same OS
13
+ * - Varies hardwareConcurrency by +/-2 (min 2)
14
+ * - Picks a random realistic deviceMemory (4, 8, or 16)
15
+ * - Everything else (GPU, fonts, plugins, platform) stays consistent
16
+ */
17
+ randomizeProfile(name: string): DeviceProfile;
18
+ }
19
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,SAAS,CAAC;AA2IzD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAA6B;;IAS7C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAQvC,YAAY,IAAI,MAAM,EAAE;IAIxB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAItD;;;;;;;;OAQG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;CAmB9C"}
@@ -0,0 +1,171 @@
1
+ // ── Shared plugin data (Chrome on all platforms) ──
2
+ const chromePlugins = [
3
+ {
4
+ name: 'Chrome PDF Plugin',
5
+ description: 'Portable Document Format',
6
+ filename: 'internal-pdf-viewer',
7
+ mimeTypes: [
8
+ { type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
9
+ ],
10
+ },
11
+ {
12
+ name: 'Chrome PDF Viewer',
13
+ description: '',
14
+ filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
15
+ mimeTypes: [
16
+ { type: 'application/pdf', suffixes: 'pdf', description: '' },
17
+ ],
18
+ },
19
+ {
20
+ name: 'Native Client',
21
+ description: '',
22
+ filename: 'internal-nacl-plugin',
23
+ mimeTypes: [
24
+ { type: 'application/x-nacl', suffixes: '', description: 'Native Client Executable' },
25
+ { type: 'application/x-pnacl', suffixes: '', description: 'Portable Native Client Executable' },
26
+ ],
27
+ },
28
+ ];
29
+ // ── UA pools per OS ──
30
+ const windowsUAs = [
31
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
32
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
33
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
34
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
35
+ ];
36
+ const macosUAs = [
37
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
38
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
39
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
40
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
41
+ ];
42
+ const linuxUAs = [
43
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
44
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
45
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
46
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
47
+ ];
48
+ // ── Pre-configured profiles ──
49
+ const desktopWindows = {
50
+ name: 'desktop-windows',
51
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
52
+ platform: 'Win32',
53
+ hardwareConcurrency: 8,
54
+ deviceMemory: 8,
55
+ maxTouchPoints: 0,
56
+ screenWidth: 1920,
57
+ screenHeight: 1080,
58
+ devicePixelRatio: 1,
59
+ colorDepth: 24,
60
+ vendor: 'Google Inc.',
61
+ renderer: 'Google Inc. (NVIDIA)',
62
+ webglVendor: 'Google Inc. (NVIDIA)',
63
+ webglRenderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)',
64
+ languages: ['en-US', 'en'],
65
+ plugins: chromePlugins,
66
+ fonts: [
67
+ 'Arial', 'Calibri', 'Cambria', 'Consolas', 'Courier New',
68
+ 'Georgia', 'Impact', 'Lucida Console', 'Segoe UI', 'Tahoma',
69
+ 'Times New Roman', 'Trebuchet MS', 'Verdana',
70
+ ],
71
+ };
72
+ const desktopMacos = {
73
+ name: 'desktop-macos',
74
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
75
+ platform: 'MacIntel',
76
+ hardwareConcurrency: 8,
77
+ deviceMemory: 16,
78
+ maxTouchPoints: 0,
79
+ screenWidth: 2560,
80
+ screenHeight: 1440,
81
+ devicePixelRatio: 2,
82
+ colorDepth: 30,
83
+ vendor: 'Google Inc.',
84
+ renderer: 'Google Inc. (Apple)',
85
+ webglVendor: 'Google Inc. (Apple)',
86
+ webglRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
87
+ languages: ['en-US', 'en'],
88
+ plugins: chromePlugins,
89
+ fonts: [
90
+ 'Arial', 'Courier New', 'Georgia', 'Helvetica', 'Helvetica Neue',
91
+ 'Lucida Grande', 'Menlo', 'Monaco', 'San Francisco', 'Times New Roman',
92
+ 'Verdana',
93
+ ],
94
+ };
95
+ const desktopLinux = {
96
+ name: 'desktop-linux',
97
+ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
98
+ platform: 'Linux x86_64',
99
+ hardwareConcurrency: 4,
100
+ deviceMemory: 8,
101
+ maxTouchPoints: 0,
102
+ screenWidth: 1920,
103
+ screenHeight: 1080,
104
+ devicePixelRatio: 1,
105
+ colorDepth: 24,
106
+ vendor: 'Google Inc.',
107
+ renderer: 'Google Inc. (Intel)',
108
+ webglVendor: 'Google Inc. (Intel)',
109
+ webglRenderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)',
110
+ languages: ['en-US', 'en'],
111
+ plugins: chromePlugins,
112
+ fonts: [
113
+ 'Arial', 'Courier New', 'DejaVu Sans', 'DejaVu Sans Mono', 'FreeMono',
114
+ 'FreeSans', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
115
+ 'Noto Sans', 'Times New Roman', 'Ubuntu',
116
+ ],
117
+ };
118
+ // ── UA pool lookup by profile name ──
119
+ const uaPoolByProfile = {
120
+ 'desktop-windows': windowsUAs,
121
+ 'desktop-macos': macosUAs,
122
+ 'desktop-linux': linuxUAs,
123
+ };
124
+ // ── DeviceProfileManager ──
125
+ export class DeviceProfileManager {
126
+ profiles;
127
+ constructor() {
128
+ this.profiles = new Map();
129
+ this.profiles.set('desktop-windows', desktopWindows);
130
+ this.profiles.set('desktop-macos', desktopMacos);
131
+ this.profiles.set('desktop-linux', desktopLinux);
132
+ }
133
+ getProfile(name) {
134
+ const profile = this.profiles.get(name);
135
+ if (!profile) {
136
+ throw new Error(`Unknown device profile: "${name}". Available: ${this.listProfiles().join(', ')}`);
137
+ }
138
+ return { ...profile, plugins: profile.plugins.map(p => ({ ...p, mimeTypes: [...p.mimeTypes] })), fonts: [...profile.fonts], languages: [...profile.languages] };
139
+ }
140
+ listProfiles() {
141
+ return Array.from(this.profiles.keys());
142
+ }
143
+ addProfile(name, profile) {
144
+ this.profiles.set(name, profile);
145
+ }
146
+ /**
147
+ * Generate a slightly randomized variant of a named profile.
148
+ *
149
+ * Randomization:
150
+ * - Picks a random UA string from a small pool for the same OS
151
+ * - Varies hardwareConcurrency by +/-2 (min 2)
152
+ * - Picks a random realistic deviceMemory (4, 8, or 16)
153
+ * - Everything else (GPU, fonts, plugins, platform) stays consistent
154
+ */
155
+ randomizeProfile(name) {
156
+ const base = this.getProfile(name); // already a shallow clone
157
+ // Pick a random UA from the pool (fall back to base UA if no pool)
158
+ const uaPool = uaPoolByProfile[name];
159
+ if (uaPool && uaPool.length > 0) {
160
+ base.userAgent = uaPool[Math.floor(Math.random() * uaPool.length)];
161
+ }
162
+ // Vary hardwareConcurrency by +/-2, min 2
163
+ const delta = Math.floor(Math.random() * 5) - 2; // -2..+2
164
+ base.hardwareConcurrency = Math.max(2, base.hardwareConcurrency + delta);
165
+ // Pick a random realistic deviceMemory
166
+ const memoryOptions = [4, 8, 16];
167
+ base.deviceMemory = memoryOptions[Math.floor(Math.random() * memoryOptions.length)];
168
+ return base;
169
+ }
170
+ }
171
+ //# sourceMappingURL=fingerprint.js.map