@apmantza/greedysearch-pi 2.1.4 → 2.1.5

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.
@@ -129,7 +129,18 @@ export async function getOrOpenTab(tabPrefix) {
129
129
  "Target.createTarget",
130
130
  '{"url":"about:blank"}',
131
131
  ]);
132
- const { targetId } = JSON.parse(raw);
132
+ let createdTarget;
133
+ try {
134
+ createdTarget = JSON.parse(raw);
135
+ } catch (error) {
136
+ throw new Error(
137
+ `Target.createTarget returned invalid JSON: ${error.message}`,
138
+ );
139
+ }
140
+ const { targetId } = createdTarget;
141
+ if (!targetId) {
142
+ throw new Error("Target.createTarget did not return a targetId");
143
+ }
133
144
  await cdp(["list"]); // refresh cache
134
145
  const tid = targetId.slice(0, 8);
135
146
  // Inject stealth patches for anti-detection coverage (both headless + visible).
@@ -221,40 +232,62 @@ export async function injectHeadlessStealth(tab) {
221
232
  try { delete window._phantom; } catch(_) {}
222
233
  try { delete window.Buffer; } catch(_) {}
223
234
 
224
- // Real Chrome without automation does not expose a useful webdriver value.
225
- // A literal false value is itself a common stealth tell; prefer undefined and
226
- // make the descriptor configurable like native browser properties.
227
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
235
+ // Real Chrome without automation should not expose navigator.webdriver at all.
236
+ // A literal false or an own-property getter returning undefined is itself a
237
+ // common stealth tell; remove both instance and prototype properties when the
238
+ // descriptor is configurable (as it is with --disable-blink-features).
239
+ try { delete navigator.webdriver; } catch(_) {}
240
+ try { delete Navigator.prototype.webdriver; } catch(_) {}
228
241
  Object.defineProperty(navigator, 'vendor', { get: () => 'Google Inc.', configurable: true });
229
242
  Object.defineProperty(navigator, 'platform', { get: () => 'Win32', configurable: true });
230
243
  Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 0, configurable: true });
231
244
  Object.defineProperty(navigator, 'pdfViewerEnabled', { get: () => true, configurable: true });
245
+ Object.defineProperty(navigator, 'productSub', { get: () => '20030107', configurable: true });
246
+ Object.defineProperty(navigator, 'product', { get: () => 'Gecko', configurable: true });
247
+ var __greedyMimeTypes = null;
248
+ function __makeMimeTypes() {
249
+ var pdf = { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null };
250
+ var textPdf = { type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null };
251
+ try { Object.setPrototypeOf(pdf, MimeType.prototype); } catch(_) {}
252
+ try { Object.setPrototypeOf(textPdf, MimeType.prototype); } catch(_) {}
253
+ var m = [pdf, textPdf];
254
+ try { Object.setPrototypeOf(m, MimeTypeArray.prototype); } catch(_) {}
255
+ m.item = function item(i) { return this[i] || null; };
256
+ m.namedItem = function namedItem(name) { return Array.prototype.find.call(this, function(x) { return x && x.type === name; }) || null; };
257
+ return m;
258
+ }
232
259
  Object.defineProperty(navigator, 'plugins', {
233
260
  get: () => {
234
- var p = [
235
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
236
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
237
- { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
238
- ];
239
- p.length = 3;
261
+ __greedyMimeTypes = __greedyMimeTypes || __makeMimeTypes();
262
+ var plugin0 = { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' };
263
+ var plugin1 = { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' };
264
+ var plugin2 = { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' };
265
+ try { Object.setPrototypeOf(plugin0, Plugin.prototype); } catch(_) {}
266
+ try { Object.setPrototypeOf(plugin1, Plugin.prototype); } catch(_) {}
267
+ try { Object.setPrototypeOf(plugin2, Plugin.prototype); } catch(_) {}
268
+ var p = [plugin0, plugin1, plugin2];
269
+ p.item = function item(i) { return this[i] || null; };
270
+ p.namedItem = function namedItem(name) { return Array.prototype.find.call(this, function(x) { return x && x.name === name; }) || null; };
271
+ p.refresh = function refresh() {};
272
+ try { Object.setPrototypeOf(p, PluginArray.prototype); } catch(_) {}
273
+ try {
274
+ __greedyMimeTypes[0].enabledPlugin = p[0];
275
+ __greedyMimeTypes[1].enabledPlugin = p[0];
276
+ } catch(_) {}
240
277
  return p;
241
278
  },
279
+ configurable: true,
242
280
  });
243
281
  Object.defineProperty(navigator, 'mimeTypes', {
244
282
  get: () => {
245
- var m = [
246
- { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null },
247
- { type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: null },
248
- ];
249
- m.item = function(i) { return m[i] || null; };
250
- m.namedItem = function(name) { return m.find(function(x) { return x.type === name; }) || null; };
251
- return m;
283
+ __greedyMimeTypes = __greedyMimeTypes || __makeMimeTypes();
284
+ return __greedyMimeTypes;
252
285
  },
253
286
  configurable: true,
254
287
  });
255
288
  Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'], configurable: true });
256
289
  try {
257
- Object.defineProperty(navigator, 'connection', { get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, saveData: false }), configurable: true });
290
+ Object.defineProperty(navigator, 'connection', { get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, downlinkMax: Infinity, saveData: false }), configurable: true });
258
291
  } catch(_) {}
259
292
  if (!navigator.mediaDevices) {
260
293
  Object.defineProperty(navigator, 'mediaDevices', {
@@ -270,6 +303,18 @@ export async function injectHeadlessStealth(tab) {
270
303
  configurable: true,
271
304
  });
272
305
  }
306
+ // ── Missing platform APIs (headless often lacks these) ─
307
+ try {
308
+ if (!navigator.share) {
309
+ navigator.share = function() { return Promise.reject(new Error('NotAllowedError')); };
310
+ }
311
+ } catch(_) {}
312
+ try {
313
+ if (!navigator.contentIndex) {
314
+ Object.defineProperty(navigator, 'contentIndex', { get: () => ({ add: function() {}, delete: function() {}, getAll: function() { return Promise.resolve([]); } }), configurable: true });
315
+ }
316
+ } catch(_) {}
317
+
273
318
  if (!window.chrome) {
274
319
  window.chrome = {
275
320
  app: { isInstalled: false, InstallState: {}, RunningState: {} },
@@ -277,8 +322,8 @@ export async function injectHeadlessStealth(tab) {
277
322
  OnInstalledReason: {}, OnRestartRequiredReason: {}, PlatformArch: {}, PlatformNaclArch: {}, PlatformOs: {}, RequestUpdateCheckStatus: {},
278
323
  connect: () => ({}), sendMessage: () => {}, onMessage: { addListener: () => {} }
279
324
  },
280
- loadTimes: () => ({}),
281
- csi: () => ({}),
325
+ loadTimes: function() { return { requestTime: 0, startLoadTime: Date.now() - 5000, commitLoadTime: Date.now() - 3000, finishDocumentLoadTime: Date.now() - 2000, finishLoadTime: Date.now() - 1000, firstPaintTime: Date.now() - 800, navigationType: 'Other', wasFetchedViaSpdy: true, wasNpnNegotiated: true, npnNegotiatedProtocol: 'h2', wasAlternateProtocolAvailable: false, connectionInfo: 'http/2' }; },
326
+ csi: function() { var t = Date.now(); return { onloadT: t - 2000, startE: t - 5000, pageT: 'back', tran: 2 }; },
282
327
  };
283
328
  }
284
329
  var __greedyNativeFns = [];
@@ -299,6 +344,19 @@ export async function injectHeadlessStealth(tab) {
299
344
  return getParam.call(this, p);
300
345
  });
301
346
  } catch(_) {}
347
+ // ── WebGL readPixels noise ──────────────────────────
348
+ // CreepJS and other fingerprinters draw content with WebGL and read back the
349
+ // rendered pixels. Adding subtle noise breaks rendering-based fingerprinting.
350
+ try {
351
+ var origReadPixels = WebGLRenderingContext.prototype.readPixels;
352
+ WebGLRenderingContext.prototype.readPixels = __markNative(function readPixels(x, y, width, height, format, type, pixels) {
353
+ var result = origReadPixels.call(this, x, y, width, height, format, type, pixels);
354
+ if (pixels && pixels.length > 0) {
355
+ pixels[0] ^= 1;
356
+ }
357
+ return result;
358
+ });
359
+ } catch(_) {}
302
360
  Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8, configurable: true });
303
361
  Object.defineProperty(navigator, 'deviceMemory', { get: () => 8, configurable: true });
304
362
 
@@ -306,7 +364,7 @@ export async function injectHeadlessStealth(tab) {
306
364
  // Headless rendering engines produce slightly different canvas output
307
365
  // than headed Chrome. Subtle noise breaks hash-based fingerprinting.
308
366
  try {
309
- var __canvasNoise = ((Date.now() % 997) + Math.floor(Math.random() * 997)) & 1;
367
+ var __canvasNoise = ((Date.now() & 0xFF) | 1);
310
368
  var origFill = CanvasRenderingContext2D.prototype.fillText;
311
369
  CanvasRenderingContext2D.prototype.fillText = __markNative(function fillText() {
312
370
  this.globalAlpha = 0.9995;
@@ -325,15 +383,39 @@ export async function injectHeadlessStealth(tab) {
325
383
  HTMLCanvasElement.prototype.toDataURL = __markNative(function toDataURL() {
326
384
  var ctx = this.getContext('2d');
327
385
  if (ctx) {
328
- // Add 1px noise pixel in corner (invisible but changes hash)
329
- var imgData = ctx.getImageData(0, 0, 1, 1);
330
- if (imgData) imgData.data[0] ^= __canvasNoise;
331
- ctx.putImageData(imgData, 0, 0);
386
+ // Spread noise across canvas to break hash-based fingerprinting.
387
+ // Uses a deterministic pattern so it's consistent per page load
388
+ // but varies between sessions.
389
+ var w = this.width, h = this.height;
390
+ if (w > 0 && h > 0) {
391
+ var imgData = ctx.getImageData(0, 0, Math.min(w, 4), Math.min(h, 4));
392
+ if (imgData && imgData.data) {
393
+ for (var __i = 0; __i < imgData.data.length; __i += 4) {
394
+ imgData.data[__i] ^= (__canvasNoise + __i) & 0xFF;
395
+ }
396
+ ctx.putImageData(imgData, 0, 0);
397
+ }
398
+ }
332
399
  }
333
400
  return origToDataURL.apply(this, arguments);
334
401
  });
335
402
  } catch(_) {}
336
403
 
404
+ // ── AudioContext fingerprint noise ────────────────────
405
+ // Headless Chrome's AudioContext produces slightly different output.
406
+ // Subtle noise breaks audio-based fingerprinting.
407
+ try {
408
+ var __audioSeed = ((Date.now() & 0x1F) | 1);
409
+ var origGetChannelData = AudioBuffer.prototype.getChannelData;
410
+ AudioBuffer.prototype.getChannelData = __markNative(function getChannelData(channel) {
411
+ var data = origGetChannelData.call(this, channel);
412
+ for (var __i = 0; __i < data.length; __i += 64) {
413
+ data[__i] *= 0.99999;
414
+ }
415
+ return data;
416
+ });
417
+ } catch(_) {}
418
+
337
419
  // ── window outer dimensions ──────────────────────────
338
420
  // outerWidth/Height = 0 in headless — a well-known bot signal.
339
421
  // Mirror innerWidth/Height (set by --window-size flag) so the ratio is sane.
@@ -343,9 +425,15 @@ export async function injectHeadlessStealth(tab) {
343
425
  } catch(_) {}
344
426
 
345
427
  // ── screen properties ─────────────────────────────────
428
+ // Headless Chrome often reports an 800x600 screen even when the viewport is
429
+ // 1920x1080. Keep screen metrics internally consistent with our launch flags.
346
430
  try {
347
- if (!screen.colorDepth) Object.defineProperty(screen, 'colorDepth', { get: () => 24, configurable: true });
348
- if (!screen.pixelDepth) Object.defineProperty(screen, 'pixelDepth', { get: () => 24, configurable: true });
431
+ Object.defineProperty(screen, 'width', { get: () => 1920, configurable: true });
432
+ Object.defineProperty(screen, 'height', { get: () => 1080, configurable: true });
433
+ Object.defineProperty(screen, 'availWidth', { get: () => 1920, configurable: true });
434
+ Object.defineProperty(screen, 'availHeight', { get: () => 1040, configurable: true });
435
+ Object.defineProperty(screen, 'colorDepth', { get: () => 24, configurable: true });
436
+ Object.defineProperty(screen, 'pixelDepth', { get: () => 24, configurable: true });
349
437
  } catch(_) {}
350
438
 
351
439
  // ── navigator.userAgentData (UA Client Hints) ─────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apmantza/greedysearch-pi",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Headless multi-engine AI search (Perplexity, Google AI, ChatGPT, Gemini) via browser automation. NO API KEYS needed. Grounded all-engine search fetches sources by default, with optional configurable synthesis and deep research.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -21,6 +21,11 @@
21
21
  "test:bash": "./test.sh",
22
22
  "test:bash:quick": "./test.sh quick",
23
23
  "test:bash:smoke": "./test.sh smoke",
24
+ "stealth-check": "node scripts/stealth-check.mjs",
25
+ "stealth:check": "node scripts/stealth-check.mjs",
26
+ "stealth:strict": "node scripts/stealth-check.mjs --strict",
27
+ "stealth:diff": "node scripts/stealth-check.mjs --diff",
28
+ "stealth:baseline": "node scripts/stealth-check.mjs --diff",
24
29
  "lint": "node scripts/lint.mjs",
25
30
  "check:lockfile": "node scripts/check-lockfile.mjs",
26
31
  "changelog:check": "node scripts/changelog-release.mjs --check",