@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.
- package/CHANGELOG.md +25 -1
- package/README.md +25 -0
- package/docs/analysis.md +233 -0
- package/docs/inspiration2.md +190 -0
- package/docs/stealthbrowsermcp.md +807 -0
- package/extractors/common.mjs +116 -28
- package/package.json +6 -1
- package/scripts/stealth-check.mjs +663 -0
package/extractors/common.mjs
CHANGED
|
@@ -129,7 +129,18 @@ export async function getOrOpenTab(tabPrefix) {
|
|
|
129
129
|
"Target.createTarget",
|
|
130
130
|
'{"url":"about:blank"}',
|
|
131
131
|
]);
|
|
132
|
-
|
|
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
|
|
225
|
-
// A literal false
|
|
226
|
-
//
|
|
227
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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()
|
|
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
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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.
|
|
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",
|