neoagent 3.0.1-beta.8 → 3.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 (100) hide show
  1. package/.env.example +2 -10
  2. package/README.md +12 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +34 -8
  6. package/docs/configuration.md +11 -2
  7. package/docs/getting-started.md +10 -8
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +3 -0
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +38 -13
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1612 -214
  14. package/flutter_app/lib/main_controller.dart +398 -26
  15. package/flutter_app/lib/main_devices.dart +293 -207
  16. package/flutter_app/lib/main_models.dart +142 -0
  17. package/flutter_app/lib/main_navigation.dart +19 -1
  18. package/flutter_app/lib/main_operations.dart +288 -9
  19. package/flutter_app/lib/main_settings.dart +510 -300
  20. package/flutter_app/lib/main_shared.dart +2 -0
  21. package/flutter_app/lib/main_timeline.dart +1378 -0
  22. package/flutter_app/lib/src/backend_client.dart +149 -19
  23. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  24. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  25. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  26. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  27. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  29. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  30. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  31. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  32. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  33. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  34. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  35. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  36. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  37. package/landing/index.html +3 -3
  38. package/lib/manager.js +184 -66
  39. package/package.json +4 -1
  40. package/server/admin/access.js +12 -7
  41. package/server/admin/admin.css +78 -0
  42. package/server/admin/admin.js +436 -10
  43. package/server/admin/billing.js +165 -42
  44. package/server/admin/index.html +80 -5
  45. package/server/admin/users.js +15 -15
  46. package/server/db/database.js +125 -20
  47. package/server/http/routes.js +1 -0
  48. package/server/http/static.js +4 -2
  49. package/server/public/.last_build_id +1 -1
  50. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  51. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  52. package/server/public/canvaskit/wimp.wasm +0 -0
  53. package/server/public/flutter_bootstrap.js +2 -2
  54. package/server/public/main.dart.js +91077 -87037
  55. package/server/routes/account.js +53 -0
  56. package/server/routes/admin.js +345 -64
  57. package/server/routes/agents.js +203 -21
  58. package/server/routes/billing.js +5 -0
  59. package/server/routes/browser.js +8 -1
  60. package/server/routes/recordings.js +96 -6
  61. package/server/routes/screenHistory.js +140 -2
  62. package/server/routes/timeline.js +43 -0
  63. package/server/services/account/erasure.js +263 -0
  64. package/server/services/ai/hooks.js +4 -1
  65. package/server/services/ai/loop/agent_engine_core.js +8 -1
  66. package/server/services/ai/loop/blank_recovery.js +36 -0
  67. package/server/services/ai/loop/conversation_loop.js +166 -33
  68. package/server/services/ai/messagingFallback.js +22 -0
  69. package/server/services/ai/rate_limits.js +28 -5
  70. package/server/services/ai/systemPrompt.js +6 -5
  71. package/server/services/ai/taskAnalysis.js +2 -0
  72. package/server/services/ai/toolEvidence.js +15 -0
  73. package/server/services/ai/toolResult.js +40 -0
  74. package/server/services/ai/tools.js +163 -4
  75. package/server/services/android/controller.js +6 -2
  76. package/server/services/billing/plans.js +2 -1
  77. package/server/services/browser/anti_detection.js +192 -0
  78. package/server/services/browser/controller.js +180 -54
  79. package/server/services/desktop/auth.js +3 -0
  80. package/server/services/desktop/registry.js +50 -2
  81. package/server/services/integrations/google/calendar.js +22 -14
  82. package/server/services/manager.js +12 -42
  83. package/server/services/memory/ingestion_chunking.js +268 -0
  84. package/server/services/messaging/telnyx.js +9 -8
  85. package/server/services/recordings/manager.js +60 -27
  86. package/server/services/runtime/backends/local-vm.js +40 -22
  87. package/server/services/runtime/docker-vm-manager.js +157 -266
  88. package/server/services/runtime/guest_bootstrap.js +17 -5
  89. package/server/services/runtime/guest_image.js +188 -0
  90. package/server/services/runtime/manager.js +0 -1
  91. package/server/services/runtime/validation.js +3 -8
  92. package/server/services/social_video/service.js +60 -10
  93. package/server/services/tasks/runtime.js +234 -9
  94. package/server/services/tasks/schedule_utils.js +5 -5
  95. package/server/services/tasks/task_repository.js +13 -0
  96. package/server/services/timeline/service.js +558 -0
  97. package/server/services/wearable/gateway.js +1 -1
  98. package/server/services/websocket.js +21 -3
  99. package/server/services/desktop/screenRecorder.js +0 -292
  100. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -240,7 +240,7 @@ function validateProactiveSendMessageArgs({ purpose, normalizedMessage }) {
240
240
  }
241
241
 
242
242
  if (normalizedPurpose === 'no_response') {
243
- if (normalizedMessage !== '[NO RESPONSE]') {
243
+ if (normalizedMessage && normalizedMessage !== '[NO RESPONSE]') {
244
244
  return {
245
245
  ok: false,
246
246
  error: 'purpose=no_response requires content "[NO RESPONSE]".',
@@ -276,7 +276,33 @@ function getRunState(engine, runId) {
276
276
 
277
277
  function hasAlreadySentProactiveMessage({ triggerSource, runState, deliveryState, allowMultipleProactiveMessages }) {
278
278
  if (!isProactiveTrigger(triggerSource) || allowMultipleProactiveMessages) return false;
279
- return Boolean(runState?.messagingSent || deliveryState?.messagingSent);
279
+ return Boolean(
280
+ runState?.messagingSent
281
+ || deliveryState?.messagingSent
282
+ || runState?.proactiveMessageStaged
283
+ || deliveryState?.proactiveMessageStaged
284
+ );
285
+ }
286
+
287
+ function markProactiveMessageStaged({ runState, deliveryState, platform, to, content, purpose, mediaPath = null }) {
288
+ const staged = {
289
+ platform: String(platform || '').trim(),
290
+ to: String(to || '').trim(),
291
+ content: String(content || '').trim(),
292
+ purpose: String(purpose || '').trim().toLowerCase(),
293
+ mediaPath: mediaPath || null,
294
+ stagedAt: new Date().toISOString(),
295
+ };
296
+
297
+ if (runState) {
298
+ runState.proactiveMessageStaged = true;
299
+ runState.stagedProactiveMessage = staged;
300
+ }
301
+
302
+ if (deliveryState) {
303
+ deliveryState.proactiveMessageStaged = true;
304
+ deliveryState.stagedProactiveMessage = staged;
305
+ }
280
306
  }
281
307
 
282
308
  function markProactiveMessageSent({ runState, deliveryState, content }) {
@@ -411,7 +437,9 @@ function getAvailableTools(app, options = {}) {
411
437
  url: { type: 'string', description: 'URL to navigate to' },
412
438
  screenshot: { type: 'boolean', description: 'Take a screenshot (default true)' },
413
439
  waitFor: { type: 'string', description: 'CSS selector to wait for' },
414
- fullPage: { type: 'boolean', description: 'Full page screenshot (default false)' }
440
+ fullPage: { type: 'boolean', description: 'Full page screenshot (default false)' },
441
+ referrerMode: { type: 'string', enum: ['direct', 'google', 'current'], description: 'Navigation referrer strategy for the VM browser (default direct). google sends a Google referrer; current navigates from the current page when possible.' },
442
+ challengeRetry: { type: 'boolean', description: 'Retry once with a Google referrer if a known bot challenge is detected (default true).' }
415
443
  },
416
444
  required: ['url']
417
445
  }
@@ -1476,6 +1504,30 @@ function getAvailableTools(app, options = {}) {
1476
1504
  required: ['query']
1477
1505
  }
1478
1506
  },
1507
+ {
1508
+ name: 'list_chats',
1509
+ description: 'List all known groups and conversations across connected messaging platforms. Use this when the user doesn\'t know the exact group name or chat ID — it returns every chat that has ever sent a message, with platform, chat ID, display name, and whether it\'s a group or DM.',
1510
+ parameters: {
1511
+ type: 'object',
1512
+ properties: {
1513
+ platform: { type: 'string', description: 'Filter to a specific platform (e.g. telegram, whatsapp, discord). Omit to list chats across all platforms.' },
1514
+ }
1515
+ }
1516
+ },
1517
+ {
1518
+ name: 'read_messages',
1519
+ description: 'Read recent messages from a connected messaging platform group or conversation. Use this to get recaps, search history, or summarize what\'s been going on in a Telegram group, WhatsApp chat, Discord channel, Slack channel, or any other connected platform. Messages are stored locally from inbound traffic.',
1520
+ parameters: {
1521
+ type: 'object',
1522
+ properties: {
1523
+ platform: { type: 'string', description: 'Platform name (e.g. telegram, whatsapp, discord, slack, signal, teams). Omit to search across all connected platforms.' },
1524
+ chat_id: { type: 'string', description: 'Exact platform chat or group ID. Use this when known.' },
1525
+ group_name: { type: 'string', description: 'Group, channel, or chat name to find (case-insensitive partial match). Use when you don\'t know the exact chat_id.' },
1526
+ limit: { type: 'number', description: 'Max messages to return (default 20, max 100).' },
1527
+ search: { type: 'string', description: 'Optional keyword to filter messages by content.' },
1528
+ }
1529
+ }
1530
+ },
1479
1531
  {
1480
1532
  name: 'social_video_extract',
1481
1533
  description: 'Extract title, description, transcript, and one representative frame image from a public social video URL (YouTube, TikTok, Instagram, or X) without social API keys.',
@@ -1737,7 +1789,9 @@ async function executeTool(toolName, args, context, engine) {
1737
1789
  return { ...await provider.navigate(args.url, {
1738
1790
  screenshot: args.screenshot !== false,
1739
1791
  waitFor: args.waitFor,
1740
- fullPage: args.fullPage
1792
+ fullPage: args.fullPage,
1793
+ referrerMode: args.referrerMode,
1794
+ challengeRetry: args.challengeRetry
1741
1795
  }), backend };
1742
1796
  }
1743
1797
 
@@ -2245,6 +2299,91 @@ async function executeTool(toolName, args, context, engine) {
2245
2299
  };
2246
2300
  }
2247
2301
 
2302
+ case 'list_chats': {
2303
+ const listPlatform = typeof args.platform === 'string' ? args.platform.trim().toLowerCase() : null;
2304
+
2305
+ let listQuery = `SELECT platform, platform_chat_id, metadata
2306
+ FROM messages
2307
+ WHERE user_id = ? AND platform_chat_id IS NOT NULL AND platform != 'web'`;
2308
+ const listParams = [userId];
2309
+
2310
+ if (listPlatform) { listQuery += ' AND platform = ?'; listParams.push(listPlatform); }
2311
+ listQuery += ' ORDER BY id DESC LIMIT 2000';
2312
+
2313
+ const listRows = db.prepare(listQuery).all(...listParams);
2314
+
2315
+ const chatMap = new Map();
2316
+ for (const row of listRows) {
2317
+ const key = `${row.platform}:${row.platform_chat_id}`;
2318
+ if (chatMap.has(key)) continue;
2319
+ let meta = {};
2320
+ try { meta = row.metadata ? JSON.parse(row.metadata) : {}; } catch {}
2321
+ const isGroup = String(meta.isGroup || '').match(/^(true|1)$/i) != null && String(meta.isGroup || '') !== '';
2322
+ const name = (meta.groupName || meta.group_name || meta.guildName || meta.guild_name || meta.chatName || meta.chat_name || '').trim()
2323
+ || (meta.senderName || meta.sender_name || '').trim()
2324
+ || null;
2325
+ chatMap.set(key, {
2326
+ platform: row.platform,
2327
+ chat_id: row.platform_chat_id,
2328
+ name,
2329
+ type: isGroup ? 'group' : 'dm',
2330
+ });
2331
+ }
2332
+
2333
+ const chats = Array.from(chatMap.values());
2334
+ return {
2335
+ count: chats.length,
2336
+ chats,
2337
+ };
2338
+ }
2339
+
2340
+ case 'read_messages': {
2341
+ const msgLimit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
2342
+ const msgPlatform = typeof args.platform === 'string' ? args.platform.trim().toLowerCase() : null;
2343
+ const msgChatId = typeof args.chat_id === 'string' ? args.chat_id.trim() : null;
2344
+ const msgGroupName = typeof args.group_name === 'string' ? args.group_name.trim().toLowerCase() : null;
2345
+ const msgSearch = typeof args.search === 'string' ? args.search.trim() : null;
2346
+
2347
+ let msgQuery = "SELECT role, content, platform, platform_chat_id, metadata, created_at FROM messages WHERE user_id = ? AND role = 'user'";
2348
+ const msgParams = [userId];
2349
+
2350
+ if (msgPlatform) { msgQuery += ' AND platform = ?'; msgParams.push(msgPlatform); }
2351
+ if (msgChatId) { msgQuery += ' AND platform_chat_id = ?'; msgParams.push(msgChatId); }
2352
+ if (msgSearch) { msgQuery += ' AND content LIKE ?'; msgParams.push(`%${msgSearch}%`); }
2353
+
2354
+ msgQuery += ' ORDER BY created_at DESC LIMIT ?';
2355
+ msgParams.push(msgGroupName ? Math.min(msgLimit * 10, 500) : msgLimit);
2356
+
2357
+ const msgRows = db.prepare(msgQuery).all(...msgParams);
2358
+
2359
+ let filteredRows = msgRows;
2360
+ if (msgGroupName) {
2361
+ filteredRows = msgRows.filter((row) => {
2362
+ try {
2363
+ const meta = row.metadata ? JSON.parse(row.metadata) : {};
2364
+ const name = (meta.groupName || meta.group_name || meta.guildName || meta.guild_name || meta.chatName || meta.chat_name || row.platform_chat_id || '').toLowerCase();
2365
+ return name.includes(msgGroupName);
2366
+ } catch { return false; }
2367
+ }).slice(0, msgLimit);
2368
+ }
2369
+
2370
+ return {
2371
+ count: filteredRows.length,
2372
+ messages: filteredRows.reverse().map((row) => {
2373
+ let meta = {};
2374
+ try { meta = row.metadata ? JSON.parse(row.metadata) : {}; } catch {}
2375
+ return {
2376
+ platform: row.platform,
2377
+ chat_id: row.platform_chat_id,
2378
+ group_name: meta.groupName || meta.group_name || meta.guildName || meta.guild_name || null,
2379
+ sender: meta.senderName || meta.sender_name || null,
2380
+ content: row.content,
2381
+ timestamp: row.created_at,
2382
+ };
2383
+ }),
2384
+ };
2385
+ }
2386
+
2248
2387
  case 'social_video_extract': {
2249
2388
  const service = socialVideo();
2250
2389
  if (!service || typeof service.extractFromUrl !== 'function') {
@@ -2376,6 +2515,26 @@ async function executeTool(toolName, args, context, engine) {
2376
2515
  };
2377
2516
  }
2378
2517
 
2518
+ if (isProactiveTrigger(triggerSource) && context.stageProactiveMessages === true && !suppressReply) {
2519
+ markProactiveMessageStaged({
2520
+ runState,
2521
+ deliveryState,
2522
+ platform: args.platform,
2523
+ to: args.to,
2524
+ content: normalizedMessage,
2525
+ purpose: args.purpose,
2526
+ mediaPath: args.media_path,
2527
+ });
2528
+ return {
2529
+ success: true,
2530
+ staged: true,
2531
+ purpose: normalizeSendMessagePurpose(args.purpose),
2532
+ platform: args.platform,
2533
+ to: args.to,
2534
+ content: normalizedMessage,
2535
+ };
2536
+ }
2537
+
2379
2538
  const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
2380
2539
  agentId,
2381
2540
  mediaPath: args.media_path,
@@ -582,7 +582,8 @@ class AndroidController {
582
582
  });
583
583
 
584
584
  progress('Waiting for Android to boot (can take 2–5 min on first run)…');
585
- await this.#waitForBoot();
585
+ // Abort early if the emulator process dies, instead of polling until timeout.
586
+ await this.#waitForBoot({ isAlive: () => this.#isPidAlive(proc.pid) });
586
587
 
587
588
  writeState(this.userId, { bootstrapped: true, starting: false, startupPhase: null, lastStartError: null });
588
589
  console.log(`[Android] Emulator ready on ${this.adbSerial}`);
@@ -593,10 +594,13 @@ class AndroidController {
593
594
  });
594
595
  }
595
596
 
596
- async #waitForBoot(timeoutMs = 10 * 60 * 1000) {
597
+ async #waitForBoot({ timeoutMs = 10 * 60 * 1000, isAlive = () => true } = {}) {
597
598
  const adb = adbBin(this.sdkDir);
598
599
  const deadline = Date.now() + timeoutMs;
599
600
  while (Date.now() < deadline) {
601
+ if (!isAlive()) {
602
+ throw new Error('Emulator process exited before Android finished booting (check virtualization/KVM support and the system image).');
603
+ }
600
604
  try {
601
605
  const r = spawnSync(adb, ['-s', this.adbSerial, 'shell', 'getprop', 'sys.boot_completed'], { encoding: 'utf8', timeout: 5000 });
602
606
  if (r.stdout?.trim() === '1') return;
@@ -42,7 +42,8 @@ function validatePlanData(data, requireName = true) {
42
42
 
43
43
  function createPlan(data) {
44
44
  validatePlanData(data, true);
45
- const id = data.id || `plan_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
45
+ const id = data.id || `plan_${randomUUID().replace(/-/g, '')}`;
46
+ // UTC timestamp without timezone marker — SQLite stores it as TEXT in ISO-like format.
46
47
  const now = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
47
48
  db.prepare(`
48
49
  INSERT INTO billing_plans
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const USER_AGENTS = Object.freeze([
6
+ {
7
+ ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
8
+ platform: 'Win32',
9
+ webglVendor: 'Google Inc. (NVIDIA)',
10
+ webglRenderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
11
+ },
12
+ {
13
+ ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
14
+ platform: 'Win32',
15
+ webglVendor: 'Google Inc. (Intel)',
16
+ webglRenderer: 'ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)',
17
+ },
18
+ {
19
+ ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
20
+ platform: 'MacIntel',
21
+ webglVendor: 'Google Inc. (Apple)',
22
+ webglRenderer: 'ANGLE (Apple, Apple M2, OpenGL 4.1)',
23
+ },
24
+ {
25
+ ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
26
+ platform: 'MacIntel',
27
+ webglVendor: 'Google Inc. (Apple)',
28
+ webglRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
29
+ },
30
+ {
31
+ ua: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
32
+ platform: 'Linux x86_64',
33
+ webglVendor: 'Google Inc. (Intel)',
34
+ webglRenderer: 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 620, OpenGL 4.6)',
35
+ },
36
+ ]);
37
+
38
+ const VIEWPORTS = Object.freeze([
39
+ { width: 1920, height: 1080, weight: 35 },
40
+ { width: 1366, height: 768, weight: 26 },
41
+ { width: 1536, height: 864, weight: 16 },
42
+ { width: 1280, height: 720, weight: 9 },
43
+ { width: 1440, height: 900, weight: 9 },
44
+ { width: 1600, height: 900, weight: 5 },
45
+ ]);
46
+
47
+ const REFERRER_MODES = new Set(['direct', 'google', 'current']);
48
+
49
+ function hashInt(value) {
50
+ const hash = crypto.createHash('sha256').update(String(value || 'default')).digest();
51
+ return hash.readUInt32BE(0);
52
+ }
53
+
54
+ function chooseWeighted(items, seed) {
55
+ const total = items.reduce((sum, item) => sum + Number(item.weight || 1), 0);
56
+ let cursor = Math.abs(Number(seed) || 0) % total;
57
+ for (const item of items) {
58
+ cursor -= Number(item.weight || 1);
59
+ if (cursor < 0) return item;
60
+ }
61
+ return items[0];
62
+ }
63
+
64
+ function chooseBrowserIdentity(profileKey) {
65
+ const seed = hashInt(profileKey);
66
+ const profile = USER_AGENTS[seed % USER_AGENTS.length];
67
+ const viewport = chooseWeighted(VIEWPORTS, seed >>> 3);
68
+ return {
69
+ userAgent: profile.ua,
70
+ platform: profile.platform,
71
+ viewport: { width: viewport.width, height: viewport.height },
72
+ webglVendor: profile.webglVendor,
73
+ webglRenderer: profile.webglRenderer,
74
+ hardwareConcurrency: [4, 6, 8, 12, 16][seed % 5],
75
+ deviceMemory: [4, 8, 16][(seed >>> 5) % 3],
76
+ };
77
+ }
78
+
79
+ function normalizeReferrerMode(value) {
80
+ const normalized = String(value || '').trim().toLowerCase();
81
+ return REFERRER_MODES.has(normalized) ? normalized : 'direct';
82
+ }
83
+
84
+ function normalizeChallengeRetry(value) {
85
+ return value !== false;
86
+ }
87
+
88
+ function detectBotChallenge(input = {}) {
89
+ const title = String(input.title || '').trim();
90
+ const url = String(input.url || '').trim();
91
+ const html = String(input.html || '');
92
+ const text = String(input.text || input.pageContent || '');
93
+ const haystack = `${title}\n${url}\n${html.slice(0, 200000)}\n${text.slice(0, 50000)}`.toLowerCase();
94
+
95
+ if (title === 'Just a moment...' || haystack.includes('cf-turnstile-response') || haystack.includes('/cdn-cgi/challenge-platform/')) {
96
+ return { detected: true, provider: 'cloudflare' };
97
+ }
98
+ if (haystack.includes('please verify you are a human') || haystack.includes('press & hold to confirm')) {
99
+ return { detected: true, provider: 'perimeterx' };
100
+ }
101
+ if (haystack.includes('datadome') || haystack.includes('geo.captcha-delivery.com')) {
102
+ return { detected: true, provider: 'datadome' };
103
+ }
104
+ if (
105
+ haystack.includes('unusual traffic')
106
+ || haystack.includes('are you a robot')
107
+ || haystack.includes('verify that you are human')
108
+ || haystack.includes('complete the security check')
109
+ ) {
110
+ return { detected: true, provider: 'generic' };
111
+ }
112
+ return { detected: false, provider: null };
113
+ }
114
+
115
+ function rand(min, max) {
116
+ return Math.floor(Math.random() * (max - min + 1)) + min;
117
+ }
118
+
119
+ function easeInOutQuad(t) {
120
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
121
+ }
122
+
123
+ function easeOutQuad(t) {
124
+ return 1 - (1 - t) * (1 - t);
125
+ }
126
+
127
+ function easeInOutSine(t) {
128
+ return -(Math.cos(Math.PI * t) - 1) / 2;
129
+ }
130
+
131
+ function bezierPoint(points, t) {
132
+ let current = points.map((point) => ({ x: point.x, y: point.y }));
133
+ while (current.length > 1) {
134
+ current = current.slice(0, -1).map((point, index) => ({
135
+ x: point.x + (current[index + 1].x - point.x) * t,
136
+ y: point.y + (current[index + 1].y - point.y) * t,
137
+ }));
138
+ }
139
+ return current[0];
140
+ }
141
+
142
+ function generateHumanMousePath(from, to, viewport = {}) {
143
+ const start = {
144
+ x: Number.isFinite(Number(from?.x)) ? Number(from.x) : 0,
145
+ y: Number.isFinite(Number(from?.y)) ? Number(from.y) : 0,
146
+ };
147
+ const end = {
148
+ x: Number.isFinite(Number(to?.x)) ? Number(to.x) : 0,
149
+ y: Number.isFinite(Number(to?.y)) ? Number(to.y) : 0,
150
+ };
151
+ const distance = Math.hypot(end.x - start.x, end.y - start.y);
152
+ if (distance < 2) return [start, end];
153
+
154
+ const width = Math.max(1, Number(viewport.width || 1440));
155
+ const height = Math.max(1, Number(viewport.height || 900));
156
+ const steps = Math.max(12, Math.min(90, Math.round(18 + distance / 18 + rand(-5, 8))));
157
+ const boundaryX = Math.min(90, Math.max(10, distance * 0.12));
158
+ const boundaryY = Math.min(90, Math.max(10, distance * 0.12));
159
+ const controlCount = rand(1, 3);
160
+ const controls = [];
161
+ for (let i = 1; i <= controlCount; i += 1) {
162
+ const ratio = i / (controlCount + 1);
163
+ controls.push({
164
+ x: Math.max(0, Math.min(width, start.x + (end.x - start.x) * ratio + rand(-boundaryX, boundaryX))),
165
+ y: Math.max(0, Math.min(height, start.y + (end.y - start.y) * ratio + rand(-boundaryY, boundaryY))),
166
+ });
167
+ }
168
+
169
+ const tweens = [easeInOutQuad, easeOutQuad, easeInOutSine];
170
+ const tween = tweens[rand(0, tweens.length - 1)];
171
+ const curve = [start, ...controls, end];
172
+ const path = [];
173
+ for (let i = 0; i < steps; i += 1) {
174
+ const point = bezierPoint(curve, tween(i / (steps - 1)));
175
+ path.push({
176
+ x: Math.round(Math.max(0, Math.min(width, point.x))),
177
+ y: Math.round(Math.max(0, Math.min(height, point.y + (i > 0 && i < steps - 1 ? rand(-1, 1) : 0)))),
178
+ });
179
+ }
180
+ path[0] = { x: Math.round(start.x), y: Math.round(start.y) };
181
+ path[path.length - 1] = { x: Math.round(end.x), y: Math.round(end.y) };
182
+ return path;
183
+ }
184
+
185
+ module.exports = {
186
+ chooseBrowserIdentity,
187
+ detectBotChallenge,
188
+ generateHumanMousePath,
189
+ normalizeChallengeRetry,
190
+ normalizeReferrerMode,
191
+ rand,
192
+ };