human-browser 3.9.2 → 3.9.3

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.
@@ -1,341 +1,157 @@
1
1
  /**
2
- * browser-human.js
2
+ * browser-human.js — Human Browser for AI Agents
3
3
  *
4
- * Stealth browser iPhone 15 Pro, Romania residential proxy, human-like behavior.
5
- * Bypasses Cloudflare, DataDome, PerimeterX, bot detection.
4
+ * Stealth browser with residential proxies from 10+ countries.
5
+ * Appears as iPhone 15 Pro or Desktop Chrome to every website.
6
+ * Bypasses Cloudflare, DataDome, PerimeterX out of the box.
7
+ *
8
+ * Get credentials: https://humanbrowser.dev
9
+ * Support: https://t.me/virixlabs
6
10
  *
7
11
  * Usage:
8
12
  * const { launchHuman } = require('./browser-human');
9
- * const { browser, page } = await launchHuman(); // mobile (iPhone)
10
- * const { browser, page } = await launchHuman({ mobile: false }); // desktop
11
- *
12
- * Proxy config via env vars (override defaults):
13
- * HB_PROXY_SERVER — e.g. http://ro.decodo.com:13001 (full override)
14
- * HB_PROXY_USER — your proxy username (or call getTrial() for free trial)
15
- * HB_PROXY_PASS — password
16
- * HB_PROXY_COUNTRY — country code: ro, us, de, gb, fr, nl, sg... (default: ro)
17
- * HB_PROXY_SESSION — Decodo: sticky port 10001-49999 (unique IP per user)
18
- * HB_NO_PROXY — set to "1" to disable proxy entirely
19
- *
20
- * Unique IP per user (Decodo sticky sessions):
21
- * Each port in 10001-49999 = different sticky IP.
22
- * Set HB_PROXY_SESSION=<random_port> at deploy time for per-user unique IP.
23
- * Country via HB_PROXY_COUNTRY or launchHuman({ country: 'us' }).
24
- *
25
- * Supported providers (built-in presets):
26
- * brightdata — brd.superproxy.io:33335 (residential_proxy1_roma)
27
- * decodo — gate.decodo.com:10001 (Decodo/Smartproxy)
28
- * iproyal — geo.iproyal.com:12321 (IPRoyal)
29
- * nodemaven — rp.nodemavenio.com:10001 (NodeMaven)
30
- *
31
- * ⚠️ Bright Data KYC note:
32
- * GET requests work without KYC. POST requests require KYC verification
33
- * at https://brightdata.com/cp/kyc — takes ~5 min.
34
- * For full functionality (form submissions, APIs), complete KYC or use
35
- * Decodo/IPRoyal which allow POST without extra verification.
13
+ * const { browser, page } = await launchHuman({ country: 'us' });
36
14
  */
37
15
 
38
- // Playwright path resolution works in multiple contexts:
39
- // 1. clawhub install → ~/.agents/skills/human-browser/
40
- // 2. workspace usage → /root/.openclaw/workspace/
41
- // 3. Clawster containers → /root/.openclaw/workspace/
42
- function _requirePlaywright() {
43
- const tries = [
44
- () => require('playwright'),
45
- () => require(`${__dirname}/../node_modules/playwright`),
46
- () => require(`${__dirname}/../../node_modules/playwright`),
47
- () => require(`${process.env.HOME || '/root'}/.openclaw/workspace/node_modules/playwright`),
48
- () => require('./node_modules/playwright'),
49
- ];
50
- for (const fn of tries) {
51
- try { return fn(); } catch (_) {}
52
- }
53
- throw new Error('[human-browser] playwright not found.\nRun: npm install playwright && npx playwright install chromium');
54
- }
55
- const { chromium } = _requirePlaywright();
56
-
57
- // ─── PROXY CONFIG ─────────────────────────────────────────────────────────────
58
- // Built-in provider presets
59
- const PROXY_PRESETS = {
60
- brightdata: {
61
- server: 'http://brd.superproxy.io:33335',
62
- usernameTemplate: (user, country, session) =>
63
- `${user}-country-${country}-session-${session}`,
64
- defaultUser: null, // set via HB_PROXY_USER or call getTrial()
65
- defaultPass: null, // set via HB_PROXY_PASS or call getTrial()
66
- defaultCountry: 'ro',
67
- },
68
- decodo: {
69
- // Country-specific hostname: {country}.decodo.com
70
- // Sticky session = port number (10001-49999), each port = unique IP
71
- serverTemplate: (country, port) => `http://${country}.decodo.com:${port}`,
72
- usernameTemplate: (user) => user,
73
- defaultUser: null, // set via HB_PROXY_USER or call getTrial()
74
- defaultPass: null, // set via HB_PROXY_PASS or call getTrial()
75
- defaultCountry: 'ro',
76
- // Port range for sticky sessions
77
- stickyPortMin: 10001,
78
- stickyPortMax: 49999,
79
- },
80
- iproyal: {
81
- server: 'http://geo.iproyal.com:12321',
82
- // IPRoyal uses password suffix for options
83
- usernameTemplate: (user) => user,
84
- passwordTemplate: (pass, country, session) =>
85
- `${pass}_country-${country}_session-${session}_lifetime-30m`,
86
- defaultUser: null,
87
- defaultPass: null,
88
- defaultCountry: 'ro',
89
- },
90
- nodemaven: {
91
- server: 'http://rp.nodemavenio.com:10001',
92
- usernameTemplate: (user, country, session) =>
93
- `${user}-country-${country}-session-${session}`,
94
- defaultUser: null,
95
- defaultPass: null,
96
- defaultCountry: 'ro',
97
- },
16
+ const { chromium } = require('playwright');
17
+ require('dotenv').config();
18
+
19
+ // ─── COUNTRY CONFIGS ──────────────────────────────────────────────────────────
20
+
21
+ const COUNTRY_META = {
22
+ ro: { locale: 'ro-RO', tz: 'Europe/Bucharest', lat: 44.4268, lon: 26.1025, lang: 'ro-RO,ro;q=0.9,en-US;q=0.8' },
23
+ us: { locale: 'en-US', tz: 'America/New_York', lat: 40.7128, lon: -74.006, lang: 'en-US,en;q=0.9' },
24
+ uk: { locale: 'en-GB', tz: 'Europe/London', lat: 51.5074, lon: -0.1278, lang: 'en-GB,en;q=0.9' },
25
+ gb: { locale: 'en-GB', tz: 'Europe/London', lat: 51.5074, lon: -0.1278, lang: 'en-GB,en;q=0.9' },
26
+ de: { locale: 'de-DE', tz: 'Europe/Berlin', lat: 52.5200, lon: 13.4050, lang: 'de-DE,de;q=0.9,en;q=0.8' },
27
+ nl: { locale: 'nl-NL', tz: 'Europe/Amsterdam', lat: 52.3676, lon: 4.9041, lang: 'nl-NL,nl;q=0.9,en;q=0.8' },
28
+ jp: { locale: 'ja-JP', tz: 'Asia/Tokyo', lat: 35.6762, lon: 139.6503, lang: 'ja-JP,ja;q=0.9,en;q=0.8' },
29
+ fr: { locale: 'fr-FR', tz: 'Europe/Paris', lat: 48.8566, lon: 2.3522, lang: 'fr-FR,fr;q=0.9,en;q=0.8' },
30
+ ca: { locale: 'en-CA', tz: 'America/Toronto', lat: 43.6532, lon: -79.3832, lang: 'en-CA,en;q=0.9' },
31
+ au: { locale: 'en-AU', tz: 'Australia/Sydney', lat: -33.8688, lon: 151.2093,lang: 'en-AU,en;q=0.9' },
32
+ sg: { locale: 'en-SG', tz: 'Asia/Singapore', lat: 1.3521, lon: 103.8198, lang: 'en-SG,en;q=0.9' },
98
33
  };
99
34
 
100
- // Active provider: env var HB_PROXY_PROVIDER or 'decodo'
101
- const ACTIVE_PROVIDER = process.env.HB_PROXY_PROVIDER || 'decodo';
102
- const preset = PROXY_PRESETS[ACTIVE_PROVIDER] || PROXY_PRESETS.brightdata;
35
+ // ─── PROXY CONFIG ─────────────────────────────────────────────────────────────
103
36
 
104
- function makeProxy(sessionId = null, country = null) {
105
- if (process.env.HB_NO_PROXY === '1') return null;
37
+ function buildProxy(country = 'ro') {
38
+ const c = country.toLowerCase();
106
39
 
107
- const cty = (country || process.env.HB_PROXY_COUNTRY || preset.defaultCountry).toLowerCase();
40
+ // Proxy config use env vars (set manually or auto-fetched via getTrial())
41
+ const PROXY_HOST = process.env.PROXY_HOST || '';
42
+ const PROXY_PORT = process.env.PROXY_PORT || '13001';
43
+ const PROXY_USER = process.env.PROXY_USER || '';
44
+ const PROXY_PASS = process.env.PROXY_PASS || '';
108
45
 
109
- // Allow full override via env vars
110
- if (process.env.HB_PROXY_SERVER && process.env.HB_PROXY_USER) {
111
- return {
112
- server: process.env.HB_PROXY_SERVER,
113
- username: process.env.HB_PROXY_USER,
114
- password: process.env.HB_PROXY_PASS || '',
115
- };
116
- }
46
+ // Also support legacy env var names for backward compatibility
47
+ const server = process.env.PROXY_SERVER || (PROXY_HOST ? `http://${PROXY_HOST}:${PROXY_PORT}` : '');
48
+ const username = process.env.PROXY_USERNAME || PROXY_USER;
49
+ const password = process.env.PROXY_PASSWORD || PROXY_PASS;
117
50
 
118
- const user = process.env.HB_PROXY_USER || preset.defaultUser;
119
- const pass = process.env.HB_PROXY_PASS || preset.defaultPass;
120
- if (!user || !pass) {
121
- console.warn(`[browser-human] No proxy credentials for provider "${ACTIVE_PROVIDER}". Set HB_PROXY_USER/HB_PROXY_PASS.`);
51
+ if (!username || !password) {
122
52
  return null;
123
53
  }
124
54
 
125
- // Decodo: sticky session via port number (10001-49999 range)
126
- // Each unique port = unique sticky IP. HB_PROXY_SESSION stores the port.
127
- let server;
128
- if (preset.serverTemplate) {
129
- const portMin = preset.stickyPortMin || 10001;
130
- const portMax = preset.stickyPortMax || 49999;
131
- const sessionPort = sessionId
132
- ? parseInt(sessionId)
133
- : (process.env.HB_PROXY_SESSION
134
- ? parseInt(process.env.HB_PROXY_SESSION)
135
- : Math.floor(Math.random() * (portMax - portMin + 1)) + portMin);
136
- server = preset.serverTemplate(cty, sessionPort);
137
- } else {
138
- const sid = sessionId || process.env.HB_PROXY_SESSION || Math.random().toString(36).slice(2, 10);
139
- server = preset.server;
140
- const username = preset.usernameTemplate(user, cty, sid);
141
- const password = preset.passwordTemplate ? preset.passwordTemplate(pass, cty, sid) : pass;
142
- return { server, username, password };
143
- }
55
+ // Inject country code into username if needed
56
+ // e.g. brd-customer-XXX-zone-YYY → brd-customer-XXX-zone-YYY-country-ro
57
+ const hasCountry = username.includes('-country-');
58
+ const finalUser = hasCountry
59
+ ? username.replace(/-country-\w+/, `-country-${c}`)
60
+ : username.includes('zone-') ? `${username}-country-${c}` : username;
144
61
 
145
- const username = preset.usernameTemplate(user, cty);
146
- const password = preset.passwordTemplate
147
- ? preset.passwordTemplate(pass, cty)
148
- : pass;
149
-
150
- return { server, username, password };
62
+ return { server, username: finalUser, password };
151
63
  }
152
64
 
153
- // Default PROXY (random session per launch)
154
- const PROXY = makeProxy();
65
+ // ─── DEVICE PROFILES ─────────────────────────────────────────────────────────
155
66
 
156
- // ─── TRIAL CREDENTIALS ───────────────────────────────────────────────────────
67
+ function buildDevice(mobile, country = 'ro') {
68
+ const meta = COUNTRY_META[country.toLowerCase()] || COUNTRY_META.ro;
157
69
 
158
- /**
159
- * Get free trial credentials from humanbrowser.dev
160
- * Sets HB_PROXY_USER, HB_PROXY_PASS, HB_PROXY_SESSION, HB_PROXY_PROVIDER
161
- * No signup needed ~1GB Romania residential + 10 captcha solves
162
- *
163
- * @example
164
- * const { launchHuman, getTrial } = require('./browser-human');
165
- * await getTrial();
166
- * const { page } = await launchHuman(); // now uses trial proxy
167
- */
168
- async function getTrial() {
169
- if (process.env.HB_PROXY_USER) {
170
- console.log('[human-browser] Credentials already set, skipping trial fetch.');
171
- return { ok: true, cached: true };
172
- }
173
- try {
174
- const https = require('https');
175
- const data = await new Promise((resolve, reject) => {
176
- https.get('https://humanbrowser.dev/api/trial', res => {
177
- let body = '';
178
- res.on('data', d => body += d);
179
- res.on('end', () => {
180
- try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
181
- });
182
- }).on('error', reject);
183
- });
184
- if (data.proxy_user || data.PROXY_USER) {
185
- const user = data.proxy_user || data.PROXY_USER;
186
- const pass = data.proxy_pass || data.PROXY_PASS;
187
- const session = data.session || data.PROXY_SESSION || String(Math.floor(Math.random() * 39999) + 10001);
188
- const provider = data.provider || 'decodo';
189
- const country = data.country || 'ro';
190
- process.env.HB_PROXY_PROVIDER = provider;
191
- process.env.HB_PROXY_USER = user;
192
- process.env.HB_PROXY_PASS = pass;
193
- process.env.HB_PROXY_SESSION = session;
194
- process.env.HB_PROXY_COUNTRY = process.env.HB_PROXY_COUNTRY || country;
195
- console.log(`[human-browser] Trial ready: ${provider} ${country.toUpperCase()} proxy`);
196
- return { ok: true, provider, country, session };
197
- }
198
- throw new Error(data.error || 'No credentials in trial response');
199
- } catch (err) {
200
- console.warn('[human-browser] Trial fetch failed:', err.message);
201
- console.warn(' → Get credentials at: https://humanbrowser.dev');
202
- return { ok: false, error: err.message };
203
- }
204
- }
205
-
206
- // iPhone 15 Pro — самый популярный iOS девайс 2024
207
- const IPHONE15 = {
208
- userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1',
209
- viewport: { width: 393, height: 852 },
210
- deviceScaleFactor: 3,
211
- isMobile: true,
212
- hasTouch: true,
213
- locale: 'ro-RO',
214
- timezoneId: 'Europe/Bucharest',
215
- geolocation: { latitude: 44.4268, longitude: 26.1025, accuracy: 50 }, // Bucharest
216
- colorScheme: 'light',
217
- // HTTP headers that iOS Safari sends
218
- extraHTTPHeaders: {
219
- 'Accept-Language': 'ro-RO,ro;q=0.9,en-US;q=0.8,en;q=0.7',
220
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
221
- 'Accept-Encoding': 'gzip, deflate, br',
222
- 'sec-fetch-dest': 'document',
223
- 'sec-fetch-mode': 'navigate',
224
- 'sec-fetch-site': 'none',
70
+ if (mobile) {
71
+ return {
72
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1',
73
+ viewport: { width: 393, height: 852 },
74
+ deviceScaleFactor: 3,
75
+ isMobile: true,
76
+ hasTouch: true,
77
+ locale: meta.locale,
78
+ timezoneId: meta.tz,
79
+ geolocation: { latitude: meta.lat, longitude: meta.lon, accuracy: 50 },
80
+ colorScheme: 'light',
81
+ extraHTTPHeaders: {
82
+ 'Accept-Language': meta.lang,
83
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
84
+ 'Accept-Encoding': 'gzip, deflate, br',
85
+ 'sec-fetch-dest': 'document',
86
+ 'sec-fetch-mode': 'navigate',
87
+ 'sec-fetch-site': 'none',
88
+ },
89
+ };
225
90
  }
226
- };
227
91
 
228
- // Desktop Chrome (Windows) — для сайтов которые не работают на мобиле
229
- const DESKTOP_RO = {
230
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
231
- viewport: { width: 1440, height: 900 },
232
- locale: 'ro-RO',
233
- timezoneId: 'Europe/Bucharest',
234
- geolocation: { latitude: 44.4268, longitude: 26.1025, accuracy: 50 },
235
- colorScheme: 'light',
236
- extraHTTPHeaders: {
237
- 'Accept-Language': 'ro-RO,ro;q=0.9,en-US;q=0.8',
238
- 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
239
- 'sec-ch-ua-mobile': '?0',
240
- 'sec-ch-ua-platform': '"Windows"',
241
- }
242
- };
92
+ return {
93
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
94
+ viewport: { width: 1440, height: 900 },
95
+ locale: meta.locale,
96
+ timezoneId: meta.tz,
97
+ geolocation: { latitude: meta.lat, longitude: meta.lon, accuracy: 50 },
98
+ colorScheme: 'light',
99
+ extraHTTPHeaders: {
100
+ 'Accept-Language': meta.lang,
101
+ 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
102
+ 'sec-ch-ua-mobile': '?0',
103
+ 'sec-ch-ua-platform': '"Windows"',
104
+ },
105
+ };
106
+ }
243
107
 
244
108
  // ─── HUMAN BEHAVIOR ───────────────────────────────────────────────────────────
245
109
 
246
- /** Random delay between min and max ms */
247
110
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
248
- const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
111
+ const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
249
112
 
250
- /**
251
- * Move mouse along a natural curved path (Bezier-like)
252
- * Not a straight line humans never move in straight lines
253
- */
254
- async function humanMouseMove(page, toX, toY, fromX = null, fromY = null) {
255
- const pos = await page.evaluate(() => ({ x: window.mouseX || 400, y: window.mouseY || 400 }));
256
- const startX = fromX ?? pos.x;
257
- const startY = fromY ?? pos.y;
258
-
259
- // Generate control points for bezier curve
260
- const cp1x = startX + rand(-80, 80);
261
- const cp1y = startY + rand(-60, 60);
262
- const cp2x = toX + rand(-50, 50);
263
- const cp2y = toY + rand(-40, 40);
264
-
113
+ async function humanMouseMove(page, toX, toY) {
114
+ const cp1x = toX + rand(-80, 80), cp1y = toY + rand(-60, 60);
115
+ const cp2x = toX + rand(-50, 50), cp2y = toY + rand(-40, 40);
116
+ const startX = rand(100, 300), startY = rand(200, 600);
265
117
  const steps = rand(12, 25);
266
-
267
118
  for (let i = 0; i <= steps; i++) {
268
119
  const t = i / steps;
269
- // Cubic bezier
270
- const x = Math.round(
271
- Math.pow(1-t, 3) * startX +
272
- 3 * Math.pow(1-t, 2) * t * cp1x +
273
- 3 * (1-t) * Math.pow(t, 2) * cp2x +
274
- Math.pow(t, 3) * toX
275
- );
276
- const y = Math.round(
277
- Math.pow(1-t, 3) * startY +
278
- 3 * Math.pow(1-t, 2) * t * cp1y +
279
- 3 * (1-t) * Math.pow(t, 2) * cp2y +
280
- Math.pow(t, 3) * toY
281
- );
120
+ const x = Math.round(Math.pow(1-t,3)*startX + 3*Math.pow(1-t,2)*t*cp1x + 3*(1-t)*t*t*cp2x + t*t*t*toX);
121
+ const y = Math.round(Math.pow(1-t,3)*startY + 3*Math.pow(1-t,2)*t*cp1y + 3*(1-t)*t*t*cp2y + t*t*t*toY);
282
122
  await page.mouse.move(x, y);
283
- // Variable speed faster in middle, slower at start/end
284
- const delay = t < 0.2 || t > 0.8 ? rand(8, 20) : rand(2, 8);
285
- await sleep(delay);
123
+ await sleep(t < 0.2 || t > 0.8 ? rand(8, 20) : rand(2, 8));
286
124
  }
287
125
  }
288
126
 
289
- /**
290
- * Human-like click with natural mouse movement
291
- */
292
- async function humanClick(page, x, y, opts = {}) {
127
+ async function humanClick(page, x, y) {
293
128
  await humanMouseMove(page, x, y);
294
- await sleep(rand(50, 180)); // Brief pause before click
129
+ await sleep(rand(50, 180));
295
130
  await page.mouse.down();
296
- await sleep(rand(40, 100)); // Hold duration
131
+ await sleep(rand(40, 100));
297
132
  await page.mouse.up();
298
- await sleep(rand(100, 300)); // Post-click pause
133
+ await sleep(rand(100, 300));
299
134
  }
300
135
 
301
- /**
302
- * Human-like type — variable speed, occasional micro-pause
303
- */
304
- async function humanType(page, selector, text, opts = {}) {
136
+ async function humanType(page, selector, text) {
305
137
  const el = await page.$(selector);
306
138
  if (!el) throw new Error(`Element not found: ${selector}`);
307
-
308
- // Click to focus
309
139
  const box = await el.boundingBox();
310
- if (box) await humanClick(page, box.x + box.width/2, box.y + box.height/2);
140
+ if (box) await humanClick(page, box.x + box.width / 2, box.y + box.height / 2);
311
141
  await sleep(rand(200, 500));
312
-
313
- // Type character by character
314
142
  for (const char of text) {
315
143
  await page.keyboard.type(char);
316
- // Variable typing speed: 80-250ms per char (average human is ~100-150ms)
317
- const delay = rand(60, 220);
318
- await sleep(delay);
319
-
320
- // Occasional longer pause (thinking)
144
+ await sleep(rand(60, 220));
321
145
  if (Math.random() < 0.08) await sleep(rand(400, 900));
322
146
  }
323
-
324
147
  await sleep(rand(200, 400));
325
148
  }
326
149
 
327
- /**
328
- * Human-like scroll — smooth, variable speed, realistic
329
- */
330
150
  async function humanScroll(page, direction = 'down', amount = null) {
331
151
  const scrollAmount = amount || rand(200, 600);
332
152
  const delta = direction === 'down' ? scrollAmount : -scrollAmount;
333
-
334
- // Move to random position first
335
153
  const vp = page.viewportSize();
336
154
  await humanMouseMove(page, rand(100, vp.width - 100), rand(200, vp.height - 200));
337
-
338
- // Scroll in small increments
339
155
  const steps = rand(4, 10);
340
156
  for (let i = 0; i < steps; i++) {
341
157
  await page.mouse.wheel(0, delta / steps + rand(-5, 5));
@@ -344,210 +160,45 @@ async function humanScroll(page, direction = 'down', amount = null) {
344
160
  await sleep(rand(200, 800));
345
161
  }
346
162
 
347
- /**
348
- * Human-like page read pause (look around the page)
349
- */
350
163
  async function humanRead(page, minMs = 1500, maxMs = 4000) {
351
164
  await sleep(rand(minMs, maxMs));
352
- // Occasional small scroll while reading
353
- if (Math.random() < 0.3) {
354
- await humanScroll(page, 'down', rand(50, 150));
355
- }
165
+ if (Math.random() < 0.3) await humanScroll(page, 'down', rand(50, 150));
356
166
  }
357
167
 
358
- // ─── 2CAPTCHA SOLVER ──────────────────────────────────────────────────────────
168
+ // ─── LAUNCH ───────────────────────────────────────────────────────────────────
359
169
 
360
170
  /**
361
- * Auto-detect and solve any captcha on the current page via 2captcha.com
171
+ * Launch a human-like browser with residential proxy
362
172
  *
363
- * Supported: reCAPTCHA v2, reCAPTCHA v3, hCaptcha, Cloudflare Turnstile
364
- *
365
- * Usage:
366
- * const { token, type } = await solveCaptcha(page);
367
- * // Token is auto-injected into the page. You just submit the form.
173
+ * @param {Object} opts
174
+ * @param {string} opts.country - 'ro'|'us'|'uk'|'de'|'nl'|'jp'|'fr'|'ca'|'au'|'sg' (default: 'ro')
175
+ * @param {boolean} opts.mobile - iPhone 15 Pro (true) or Desktop Chrome (false). Default: true
176
+ * @param {boolean} opts.useProxy - Enable residential proxy. Default: true
177
+ * @param {boolean} opts.headless - Headless mode. Default: true
368
178
  *
369
- * Options:
370
- * apiKey — 2captcha API key (default: env TWOCAPTCHA_KEY)
371
- * action — reCAPTCHA v3 action (default: 'verify')
372
- * minScore — reCAPTCHA v3 min score (default: 0.7)
373
- * timeout — max wait ms (default: 120000)
374
- * verbose — log progress (default: false)
375
- */
376
- async function solveCaptcha(page, opts = {}) {
377
- const {
378
- apiKey = process.env.TWOCAPTCHA_KEY || '14cbfeed64fea439d5c055111d6760e5',
379
- action = 'verify',
380
- minScore = 0.7,
381
- timeout = 120000,
382
- verbose = false,
383
- } = opts;
384
-
385
- if (!apiKey) throw new Error('[2captcha] No API key. Set TWOCAPTCHA_KEY env or pass opts.apiKey');
386
-
387
- const log = verbose ? (...a) => console.log('[2captcha]', ...a) : () => {};
388
- const pageUrl = page.url();
389
-
390
- // ─── Auto-detect captcha type ───────────────────────────────────────────────
391
- const detected = await page.evaluate(() => {
392
- // reCAPTCHA v2/v3
393
- const rc = document.querySelector('.g-recaptcha, [data-sitekey]');
394
- if (rc) {
395
- const sitekey = rc.getAttribute('data-sitekey') || rc.getAttribute('data-key');
396
- const version = rc.getAttribute('data-version') || (typeof window.grecaptcha !== 'undefined' && 'v2');
397
- return { type: 'recaptcha', sitekey, version: version === 'v3' ? 'v3' : 'v2' };
398
- }
399
- // hCaptcha
400
- const hc = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]');
401
- if (hc) {
402
- const sitekey = hc.getAttribute('data-sitekey') || hc.getAttribute('data-hcaptcha-sitekey');
403
- return { type: 'hcaptcha', sitekey };
404
- }
405
- // Cloudflare Turnstile
406
- const ts = document.querySelector('.cf-turnstile, [data-cf-turnstile-sitekey]');
407
- if (ts) {
408
- const sitekey = ts.getAttribute('data-sitekey') || ts.getAttribute('data-cf-turnstile-sitekey');
409
- return { type: 'turnstile', sitekey };
410
- }
411
- // Also check script tags for sitekeys
412
- const scripts = [...document.scripts].map(s => s.src + s.textContent);
413
- const combined = scripts.join(' ');
414
- const rcMatch = combined.match(/(?:sitekey|data-sitekey)['":\s]+([A-Za-z0-9_-]{40,})/);
415
- if (rcMatch) return { type: 'recaptcha', sitekey: rcMatch[1], version: 'v2' };
416
-
417
- return null;
418
- });
419
-
420
- if (!detected || !detected.sitekey) {
421
- throw new Error('[2captcha] No captcha detected on page. Check manually.');
422
- }
423
-
424
- log(`Detected ${detected.type} v${detected.version || ''}`, detected.sitekey.slice(0, 20) + '...');
425
- log(`Page: ${pageUrl}`);
426
-
427
- // ─── Route: trial proxy OR direct 2captcha ─────────────────────────────────
428
- const captchaProxyUrl = opts.captchaUrl || process.env.CAPTCHA_URL;
429
- const captchaToken = opts.captchaToken || process.env.CAPTCHA_TOKEN;
430
- let token = null;
431
-
432
- if (captchaProxyUrl && captchaToken) {
433
- // Trial mode: VPS proxy handles 2captcha + tracks usage
434
- log(`Using trial captcha proxy: ${captchaProxyUrl}`);
435
- const methodMap = { recaptcha: detected.version === 'v3' ? 'recaptcha_v3' : 'recaptcha_v2', hcaptcha: 'hcaptcha', turnstile: 'turnstile' };
436
- const resp = await fetch(captchaProxyUrl, {
437
- method: 'POST',
438
- headers: { 'Content-Type': 'application/json' },
439
- body: JSON.stringify({ trial_token: captchaToken, sitekey: detected.sitekey, method: methodMap[detected.type] || 'recaptcha_v2', pageurl: pageUrl, action, min_score: minScore }),
440
- signal: AbortSignal.timeout(timeout),
441
- });
442
- const data = await resp.json();
443
- if (!data.ok) {
444
- const err = new Error(data.error || 'Captcha proxy failed');
445
- err.upgrade_url = data.upgrade_url || 'https://humanbrowser.dev';
446
- err.solves_remaining = data.solves_remaining ?? 0;
447
- throw err;
448
- }
449
- token = data.token;
450
- log(`✅ Solved via proxy! Solves remaining: ${data.solves_remaining}`);
451
- } else {
452
- // Direct 2captcha mode
453
- if (!apiKey) throw new Error('[2captcha] No API key. Get a trial at humanbrowser.dev');
454
- let submitUrl = `https://2captcha.com/in.php?key=${apiKey}&json=1&pageurl=${encodeURIComponent(pageUrl)}&googlekey=${encodeURIComponent(detected.sitekey)}`;
455
- if (detected.type === 'recaptcha') {
456
- submitUrl += `&method=userrecaptcha`;
457
- if (detected.version === 'v3') submitUrl += `&version=v3&action=${action}&min_score=${minScore}`;
458
- } else if (detected.type === 'hcaptcha') {
459
- submitUrl += `&method=hcaptcha&sitekey=${encodeURIComponent(detected.sitekey)}`;
460
- } else if (detected.type === 'turnstile') {
461
- submitUrl += `&method=turnstile&sitekey=${encodeURIComponent(detected.sitekey)}`;
462
- }
463
- const submitResp = await fetch(submitUrl);
464
- const submitData = await submitResp.json();
465
- if (!submitData.status || submitData.status !== 1) throw new Error(`[2captcha] Submit failed: ${JSON.stringify(submitData)}`);
466
- const taskId = submitData.request;
467
- log(`Task submitted: ${taskId} — waiting for workers...`);
468
-
469
- const maxAttempts = Math.floor(timeout / 5000);
470
- for (let i = 0; i < maxAttempts; i++) {
471
- await sleep(i === 0 ? 15000 : 5000);
472
- const pollResp = await fetch(`https://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`);
473
- const pollData = await pollResp.json();
474
- if (pollData.status === 1) { token = pollData.request; log(`✅ Solved!`); break; }
475
- if (pollData.request !== 'CAPCHA_NOT_READY') throw new Error(`[2captcha] Poll error: ${JSON.stringify(pollData)}`);
476
- log(`⏳ Attempt ${i + 1}/${maxAttempts} — not ready yet...`);
477
- }
478
- if (!token) throw new Error('[2captcha] Timeout waiting for captcha solution');
479
- }
480
-
481
- // ─── Inject token into page ─────────────────────────────────────────────────
482
- await page.evaluate(({ type, token }) => {
483
- // reCAPTCHA
484
- if (type === 'recaptcha' || type === 'turnstile') {
485
- const textarea = document.querySelector('#g-recaptcha-response, [name="g-recaptcha-response"]');
486
- if (textarea) {
487
- textarea.style.display = 'block';
488
- textarea.value = token;
489
- textarea.dispatchEvent(new Event('change', { bubbles: true }));
490
- }
491
- // Also try callback
492
- if (typeof window.___grecaptcha_cfg !== 'undefined') {
493
- try {
494
- const clients = window.___grecaptcha_cfg.clients;
495
- if (clients) {
496
- Object.values(clients).forEach(client => {
497
- Object.values(client).forEach(widget => {
498
- if (widget && typeof widget.callback === 'function') {
499
- widget.callback(token);
500
- }
501
- });
502
- });
503
- }
504
- } catch (_) {}
505
- }
506
- }
507
- // hCaptcha
508
- if (type === 'hcaptcha') {
509
- const textarea = document.querySelector('[name="h-captcha-response"], #h-captcha-response');
510
- if (textarea) {
511
- textarea.style.display = 'block';
512
- textarea.value = token;
513
- textarea.dispatchEvent(new Event('change', { bubbles: true }));
514
- }
515
- }
516
- // Turnstile
517
- if (type === 'turnstile') {
518
- const input = document.querySelector('[name="cf-turnstile-response"]');
519
- if (input) {
520
- input.value = token;
521
- input.dispatchEvent(new Event('change', { bubbles: true }));
522
- }
523
- }
524
- }, { type: detected.type, token });
525
-
526
- log('✅ Token injected into page');
527
- return { token, type: detected.type, sitekey: detected.sitekey };
528
- }
529
-
530
- // ─── LAUNCH ───────────────────────────────────────────────────────────────────
531
-
532
- /**
533
- * Launch a human-like browser session
534
- * @param {Object} opts
535
- * @param {boolean} opts.mobile - Use iPhone 15 (default: true)
536
- * @param {boolean} opts.useProxy - Use residential proxy (default: true)
537
- * @param {boolean} opts.headless - Headless mode (default: true)
538
- * @param {string} opts.country - Proxy country code: 'ro','us','de','gb','fr'... (default: env HB_PROXY_COUNTRY or 'ro')
539
- * @param {string} opts.session - Sticky session ID / Decodo port (default: random unique)
179
+ * @returns {{ browser, ctx, page, humanClick, humanType, humanScroll, humanRead, sleep, rand }}
540
180
  */
541
181
  async function launchHuman(opts = {}) {
542
182
  const {
543
- mobile = true,
183
+ country = 'ro',
184
+ mobile = true,
544
185
  useProxy = true,
545
186
  headless = true,
546
- country = null,
547
- session = null,
548
187
  } = opts;
549
188
 
550
- const device = mobile ? IPHONE15 : DESKTOP_RO;
189
+ // Auto-fetch trial credentials if no proxy is configured
190
+ if (useProxy && !process.env.PROXY_USER && !process.env.PROXY_SERVER && !process.env.PROXY_USERNAME) {
191
+ try {
192
+ await getTrial();
193
+ } catch (e) {
194
+ console.warn('⚠️ Could not fetch trial credentials:', e.message);
195
+ console.warn(' Get credentials at: https://humanbrowser.dev');
196
+ }
197
+ }
198
+
199
+ const meta = COUNTRY_META[country.toLowerCase()] || COUNTRY_META.ro;
200
+ const device = buildDevice(mobile, country);
201
+ const proxy = useProxy ? buildProxy(country) : null;
551
202
 
552
203
  const browser = await chromium.launch({
553
204
  headless,
@@ -555,7 +206,7 @@ async function launchHuman(opts = {}) {
555
206
  '--no-sandbox',
556
207
  '--disable-setuid-sandbox',
557
208
  '--ignore-certificate-errors',
558
- '--disable-blink-features=AutomationControlled', // Hide webdriver flag!
209
+ '--disable-blink-features=AutomationControlled',
559
210
  '--disable-features=IsolateOrigins,site-per-process',
560
211
  '--disable-web-security',
561
212
  ],
@@ -566,109 +217,202 @@ async function launchHuman(opts = {}) {
566
217
  ignoreHTTPSErrors: true,
567
218
  permissions: ['geolocation', 'notifications'],
568
219
  };
569
-
570
- if (useProxy) {
571
- // Each unique session = unique sticky IP. Same session = same IP.
572
- ctxOpts.proxy = makeProxy(session, country);
573
- }
220
+ if (proxy) ctxOpts.proxy = proxy;
574
221
 
575
222
  const ctx = await browser.newContext(ctxOpts);
576
223
 
577
- // Anti-detection: override navigator properties
578
- await ctx.addInitScript(() => {
579
- // Hide webdriver
580
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
581
-
582
- // Fix plugins (mobile has none, that's normal for Safari)
583
- if (!navigator.plugins.length) {
584
- // Leave as-is for mobile
585
- }
586
-
587
- // Override chrome object (not present in Safari)
588
- // delete window.chrome; // Not needed for iPhone UA
589
-
590
- // Realistic touch events for iOS
591
- Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 5 });
592
-
593
- // Platform
594
- Object.defineProperty(navigator, 'platform', { get: () => 'iPhone' });
595
-
596
- // Language
597
- Object.defineProperty(navigator, 'language', { get: () => 'ro-RO' });
598
- Object.defineProperty(navigator, 'languages', { get: () => ['ro-RO', 'ro', 'en-US', 'en'] });
599
-
600
- // Screen (iPhone 15 Pro)
601
- Object.defineProperty(screen, 'width', { get: () => 393 });
602
- Object.defineProperty(screen, 'height', { get: () => 852 });
603
- Object.defineProperty(screen, 'availWidth', { get: () => 393 });
604
- Object.defineProperty(screen, 'availHeight', { get: () => 852 });
605
-
606
- // Hardware concurrency (iPhone has 6 cores)
607
- Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 6 });
608
-
609
- // Memory (4GB iPhone)
610
- // Object.defineProperty(navigator, 'deviceMemory', { get: () => 4 }); // Safari doesn't expose this
611
-
612
- // Connection (LTE/5G)
613
- if (navigator.connection) {
614
- Object.defineProperty(navigator.connection, 'effectiveType', { get: () => '4g' });
615
- Object.defineProperty(navigator.connection, 'rtt', { get: () => rand(30, 80) });
616
- }
617
-
618
- function rand(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }
619
- });
224
+ // Anti-detection overrides
225
+ await ctx.addInitScript((m) => {
226
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
227
+ Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 5 });
228
+ Object.defineProperty(navigator, 'platform', { get: () => m.mobile ? 'iPhone' : 'Win32' });
229
+ Object.defineProperty(navigator, 'hardwareConcurrency',{ get: () => m.mobile ? 6 : 8 });
230
+ Object.defineProperty(navigator, 'language', { get: () => m.locale });
231
+ Object.defineProperty(navigator, 'languages', { get: () => [m.locale, 'en'] });
232
+ }, { mobile, locale: meta.locale });
620
233
 
621
234
  const page = await ctx.newPage();
622
235
 
623
- // Add realistic touch simulation for mobile
624
- if (mobile) {
625
- await page.addInitScript(() => {
626
- // Simulate touch
627
- window.ontouchstart = null;
628
- window.ontouchmove = null;
629
- window.ontouchend = null;
236
+ return { browser, ctx, page, humanClick, humanMouseMove, humanType, humanScroll, humanRead, sleep, rand };
237
+ }
238
+
239
+ // ─── TRIAL ────────────────────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * Get free trial credentials from humanbrowser.dev
243
+ * Fetches shared trial proxy (~100MB, Romania). Sets env vars automatically.
244
+ *
245
+ * Usage:
246
+ * const { launchHuman, getTrial } = require('./browser-human');
247
+ * await getTrial(); // sets PROXY_USER/PASS in process.env
248
+ * const { page } = await launchHuman(); // now uses trial credentials
249
+ *
250
+ * When trial runs out → throws { code: 'TRIAL_EXHAUSTED', cta_url: '...' }
251
+ */
252
+ async function getTrial() {
253
+ let https;
254
+ try { https = require('https'); } catch { https = require('http'); }
255
+
256
+ return new Promise((resolve, reject) => {
257
+ const req = https.get('https://humanbrowser.dev/api/trial', (res) => {
258
+ let body = '';
259
+ res.on('data', chunk => body += chunk);
260
+ res.on('end', () => {
261
+ try {
262
+ const data = JSON.parse(body);
263
+ if (data.error || res.statusCode !== 200) {
264
+ const err = new Error(data.error || 'Trial unavailable');
265
+ err.code = 'TRIAL_UNAVAILABLE';
266
+ err.cta_url = 'https://humanbrowser.dev';
267
+ return reject(err);
268
+ }
269
+ // Auto-set env vars so launchHuman() picks them up
270
+ process.env.PROXY_HOST = data.proxy_host;
271
+ process.env.PROXY_PORT = data.proxy_port;
272
+ process.env.PROXY_USER = data.proxy_user;
273
+ process.env.PROXY_PASS = data.proxy_pass;
274
+
275
+ console.log('🎉 Human Browser trial activated! (~100MB Romania residential IP)');
276
+ console.log(' Upgrade at: https://humanbrowser.dev\n');
277
+ resolve(data);
278
+ } catch (e) {
279
+ reject(e);
280
+ }
281
+ });
630
282
  });
631
- }
283
+ req.on('error', (e) => {
284
+ const err = new Error('Could not reach humanbrowser.dev: ' + e.message);
285
+ err.code = 'TRIAL_NETWORK_ERROR';
286
+ reject(err);
287
+ });
288
+ req.setTimeout(10000, () => { req.destroy(); reject(new Error('Trial request timed out')); });
289
+ });
290
+ }
632
291
 
633
- return { browser, ctx, page, humanClick, humanMouseMove, humanType, humanScroll, humanRead, sleep, rand };
292
+ // ─── SHADOW DOM UTILITIES ─────────────────────────────────────────────────────
293
+
294
+ /**
295
+ * Query an element inside shadow DOM (any depth).
296
+ * Use when page.$() returns null but element is visible on screen.
297
+ */
298
+ async function shadowQuery(page, selector) {
299
+ return page.evaluate((sel) => {
300
+ function q(root, s) {
301
+ const el = root.querySelector(s); if (el) return el;
302
+ for (const n of root.querySelectorAll('*')) if (n.shadowRoot) { const f = q(n.shadowRoot, s); if (f) return f; }
303
+ }
304
+ return q(document, sel);
305
+ }, selector);
306
+ }
307
+
308
+ /**
309
+ * Fill an input inside shadow DOM.
310
+ * Uses native input setter to trigger React/Angular onChange properly.
311
+ */
312
+ async function shadowFill(page, selector, value) {
313
+ await page.evaluate(({ sel, val }) => {
314
+ function q(root, s) {
315
+ const el = root.querySelector(s); if (el) return el;
316
+ for (const n of root.querySelectorAll('*')) if (n.shadowRoot) { const f = q(n.shadowRoot, s); if (f) return f; }
317
+ }
318
+ const el = q(document, sel);
319
+ if (!el) throw new Error('shadowFill: not found: ' + sel);
320
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
321
+ setter.call(el, val);
322
+ el.dispatchEvent(new Event('input', { bubbles: true }));
323
+ el.dispatchEvent(new Event('change', { bubbles: true }));
324
+ }, { sel: selector, val: value });
325
+ }
326
+
327
+ /**
328
+ * Click a button by its text label, searching through shadow DOM.
329
+ */
330
+ async function shadowClickButton(page, buttonText) {
331
+ await page.evaluate((text) => {
332
+ function find(root) {
333
+ for (const b of root.querySelectorAll('button')) if (b.textContent.trim() === text) return b;
334
+ for (const n of root.querySelectorAll('*')) if (n.shadowRoot) { const f = find(n.shadowRoot); if (f) return f; }
335
+ }
336
+ const btn = find(document);
337
+ if (!btn) throw new Error('shadowClickButton: not found: ' + text);
338
+ btn.click();
339
+ }, buttonText);
340
+ }
341
+
342
+ /**
343
+ * Dump all inputs/buttons visible on page, including inside shadow roots.
344
+ * Use for debugging when form elements aren't found.
345
+ */
346
+ async function dumpInteractiveElements(page) {
347
+ return page.evaluate(() => {
348
+ const res = [];
349
+ function collect(root) {
350
+ for (const el of root.querySelectorAll('input,textarea,button,select,[contenteditable]')) {
351
+ const rect = el.getBoundingClientRect();
352
+ if (rect.width > 0 && rect.height > 0)
353
+ res.push({ tag: el.tagName, name: el.name || '', id: el.id || '', type: el.type || '', text: el.textContent?.trim().slice(0, 25) || '', placeholder: el.placeholder?.slice(0, 25) || '' });
354
+ }
355
+ for (const n of root.querySelectorAll('*')) if (n.shadowRoot) collect(n.shadowRoot);
356
+ }
357
+ collect(document);
358
+ return res;
359
+ });
360
+ }
361
+
362
+ // ─── RICH TEXT EDITOR UTILITIES ───────────────────────────────────────────────
363
+
364
+ /**
365
+ * Paste text into a Lexical/ProseMirror/Quill/Draft.js rich text editor.
366
+ * Uses clipboard API — works where keyboard.type() and fill() fail.
367
+ *
368
+ * Common selectors:
369
+ * '[data-lexical-editor]' — Reddit, Meta apps
370
+ * '.public-DraftEditor-content' — Draft.js (Twitter, Quora)
371
+ * '.ql-editor' — Quill
372
+ * '.ProseMirror' — Linear, Confluence
373
+ * '[contenteditable="true"]' — generic
374
+ */
375
+ async function pasteIntoEditor(page, editorSelector, text) {
376
+ const el = await page.$(editorSelector);
377
+ if (!el) throw new Error('pasteIntoEditor: editor not found: ' + editorSelector);
378
+ await el.click();
379
+ await new Promise(r => setTimeout(r, 300));
380
+ // Write to clipboard via execCommand (works in Playwright context)
381
+ await page.evaluate((t) => {
382
+ const ta = document.createElement('textarea');
383
+ ta.value = t;
384
+ document.body.appendChild(ta);
385
+ ta.select();
386
+ document.execCommand('copy');
387
+ document.body.removeChild(ta);
388
+ }, text);
389
+ await page.keyboard.press('Control+a');
390
+ await new Promise(r => setTimeout(r, 100));
391
+ await page.keyboard.press('Control+v');
392
+ await new Promise(r => setTimeout(r, 500));
634
393
  }
635
394
 
636
- // ─── EXPORT ───────────────────────────────────────────────────────────────────
637
- module.exports = {
638
- launchHuman,
639
- getTrial,
395
+ module.exports = {
396
+ launchHuman, getTrial,
640
397
  humanClick, humanMouseMove, humanType, humanScroll, humanRead,
641
- solveCaptcha,
642
- sleep, rand,
643
- PROXY, makeProxy, IPHONE15, DESKTOP_RO
398
+ shadowQuery, shadowFill, shadowClickButton, dumpInteractiveElements,
399
+ pasteIntoEditor,
400
+ sleep, rand, COUNTRY_META,
644
401
  };
645
402
 
646
403
  // ─── QUICK TEST ───────────────────────────────────────────────────────────────
647
404
  if (require.main === module) {
405
+ const country = process.argv[2] || 'ro';
406
+ console.log(`🧪 Testing Human Browser — country: ${country.toUpperCase()}\n`);
648
407
  (async () => {
649
- console.log('🧪 Testing human browser (iPhone 15, Romania)...\n');
650
-
651
- const { browser, page, humanScroll, humanRead } = await launchHuman({ mobile: true });
652
-
408
+ const { browser, page } = await launchHuman({ country, mobile: true });
653
409
  await page.goto('https://ipinfo.io/json', { waitUntil: 'domcontentloaded', timeout: 30000 });
654
410
  const info = JSON.parse(await page.textContent('body'));
655
- console.log(`✅ IP: ${info.ip}`);
411
+ console.log(`✅ IP: ${info.ip}`);
656
412
  console.log(`✅ Country: ${info.country} (${info.city})`);
657
- console.log(`✅ Org: ${info.org}`);
658
- console.log(`✅ Timezone: ${info.timezone}`);
659
-
660
- // Test UA
661
- const ua = await page.evaluate(() => navigator.userAgent);
662
- console.log(`\n✅ User-Agent: ${ua.slice(0, 80)}...`);
663
-
664
- const platform = await page.evaluate(() => navigator.platform);
665
- const lang = await page.evaluate(() => navigator.language);
666
- const touch = await page.evaluate(() => navigator.maxTouchPoints);
667
- console.log(`✅ Platform: ${platform}`);
668
- console.log(`✅ Language: ${lang}`);
669
- console.log(`✅ Touch points: ${touch}`);
670
-
413
+ console.log(`✅ Org: ${info.org}`);
414
+ console.log(`✅ TZ: ${info.timezone}`);
671
415
  await browser.close();
672
- console.log('\n🎉 All good! Browser is fully configured.');
416
+ console.log('\n🎉 Human Browser is ready.');
673
417
  })().catch(console.error);
674
418
  }