halo-agent 1.3.0 → 1.3.1

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 (2) hide show
  1. package/captcha.js +46 -27
  2. package/package.json +1 -1
package/captcha.js CHANGED
@@ -17,49 +17,56 @@ const CAPSOLVER_API = 'https://api.capsolver.com';
17
17
  * Returns { detected, type, sitekey, pageUrl }
18
18
  */
19
19
  async function detectCaptcha(page) {
20
+ // The pageUrl CapSolver wants is the top-level URL the user sees, not an
21
+ // inner iframe URL. Anchor it here so every branch returns the same thing.
22
+ const pageUrl = page.url();
23
+
20
24
  // First try the top-level frame via evaluate
21
25
  const topResult = await page.evaluate(() => {
22
- // reCAPTCHA v2 via iframe src
26
+ // reCAPTCHA v2 via iframe src — also check size=invisible in the URL
23
27
  const rcFrame = document.querySelector('iframe[src*="recaptcha/api2"], iframe[src*="google.com/recaptcha"]');
24
28
  if (rcFrame) {
25
- const match = (rcFrame.src || '').match(/[?&]k=([^&]+)/);
26
- return { detected: true, type: 'recaptcha_v2', sitekey: match?.[1] || null, pageUrl: location.href };
29
+ const src = rcFrame.src || '';
30
+ const match = src.match(/[?&]k=([^&]+)/);
31
+ const isInvisible = /[?&]size=invisible/.test(src);
32
+ return { detected: true, type: 'recaptcha_v2', sitekey: match?.[1] || null, isInvisible };
27
33
  }
28
- // reCAPTCHA via data-sitekey
34
+ // reCAPTCHA via data-sitekey — check data-size="invisible" on the element
29
35
  const rcEl = document.querySelector('.g-recaptcha[data-sitekey], [data-sitekey]:not(.h-captcha)');
30
36
  if (rcEl) {
31
- return { detected: true, type: 'recaptcha_v2', sitekey: rcEl.getAttribute('data-sitekey'), pageUrl: location.href };
37
+ const isInvisible = rcEl.getAttribute('data-size') === 'invisible';
38
+ return { detected: true, type: 'recaptcha_v2', sitekey: rcEl.getAttribute('data-sitekey'), isInvisible };
32
39
  }
33
40
  // hCAPTCHA
34
41
  const hcEl = document.querySelector('.h-captcha[data-sitekey]');
35
42
  if (hcEl) {
36
- return { detected: true, type: 'hcaptcha', sitekey: hcEl.getAttribute('data-sitekey'), pageUrl: location.href };
43
+ return { detected: true, type: 'hcaptcha', sitekey: hcEl.getAttribute('data-sitekey'), isInvisible: false };
37
44
  }
38
45
  const hcFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
39
46
  if (hcFrame) {
40
47
  const match = (hcFrame.src || '').match(/[?&]sitekey=([^&]+)/);
41
- return { detected: true, type: 'hcaptcha', sitekey: match?.[1] || null, pageUrl: location.href };
48
+ return { detected: true, type: 'hcaptcha', sitekey: match?.[1] || null, isInvisible: false };
42
49
  }
43
50
  // Cloudflare
44
51
  if (document.getElementById('cf-challenge-running') || document.querySelector('.cf-browser-verification')) {
45
- return { detected: true, type: 'cloudflare', sitekey: null, pageUrl: location.href };
52
+ return { detected: true, type: 'cloudflare', sitekey: null, isInvisible: false };
46
53
  }
47
54
  return null;
48
55
  });
49
56
 
50
- if (topResult) return topResult;
57
+ if (topResult) return { ...topResult, pageUrl };
51
58
 
52
- // Search all frames for reCAPTCHA (Ashby loads it inside a sandboxed iframe)
53
- const pageUrl = page.url();
59
+ // Search all frames for reCAPTCHA (Ashby/Greenhouse load it inside a sandboxed iframe)
54
60
  for (const frame of page.frames()) {
55
61
  if (frame === page.mainFrame()) continue;
56
62
  const frameSrc = frame.url();
57
63
 
58
- // If the frame itself is a reCAPTCHA anchor frame, extract sitekey from its URL
64
+ // If the frame itself is a reCAPTCHA anchor frame, extract sitekey + invisible from URL
59
65
  if (frameSrc.includes('recaptcha/api2/anchor') || frameSrc.includes('recaptcha/enterprise/anchor')) {
60
66
  const match = frameSrc.match(/[?&]k=([^&]+)/);
67
+ const isInvisible = /[?&]size=invisible/.test(frameSrc);
61
68
  if (match) {
62
- return { detected: true, type: 'recaptcha_v2', sitekey: match[1], pageUrl };
69
+ return { detected: true, type: 'recaptcha_v2', sitekey: match[1], pageUrl, isInvisible };
63
70
  }
64
71
  }
65
72
 
@@ -67,22 +74,29 @@ async function detectCaptcha(page) {
67
74
  try {
68
75
  const frameResult = await frame.evaluate(() => {
69
76
  const rcEl = document.querySelector('.g-recaptcha[data-sitekey], [data-sitekey]:not(.h-captcha)');
70
- if (rcEl) return { sitekey: rcEl.getAttribute('data-sitekey'), type: 'recaptcha_v2' };
77
+ if (rcEl) {
78
+ return {
79
+ sitekey: rcEl.getAttribute('data-sitekey'),
80
+ type: 'recaptcha_v2',
81
+ isInvisible: rcEl.getAttribute('data-size') === 'invisible',
82
+ };
83
+ }
71
84
  const rcFrame = document.querySelector('iframe[src*="recaptcha"]');
72
85
  if (rcFrame) {
73
- const match = (rcFrame.src || '').match(/[?&]k=([^&]+)/);
74
- return match ? { sitekey: match[1], type: 'recaptcha_v2' } : null;
86
+ const src = rcFrame.src || '';
87
+ const match = src.match(/[?&]k=([^&]+)/);
88
+ return match ? { sitekey: match[1], type: 'recaptcha_v2', isInvisible: /[?&]size=invisible/.test(src) } : null;
75
89
  }
76
90
  return null;
77
91
  }).catch(() => null);
78
92
 
79
93
  if (frameResult?.sitekey) {
80
- return { detected: true, type: frameResult.type, sitekey: frameResult.sitekey, pageUrl };
94
+ return { detected: true, type: frameResult.type, sitekey: frameResult.sitekey, pageUrl, isInvisible: !!frameResult.isInvisible };
81
95
  }
82
96
  } catch {}
83
97
  }
84
98
 
85
- return { detected: false, type: null, sitekey: null, pageUrl };
99
+ return { detected: false, type: null, sitekey: null, pageUrl, isInvisible: false };
86
100
  }
87
101
 
88
102
  /**
@@ -101,21 +115,26 @@ async function solveCaptcha(captchaInfo, apiKey) {
101
115
  ? 'HCaptchaTaskProxyless'
102
116
  : 'ReCaptchaV2TaskProxyless';
103
117
 
104
- console.log(`[captcha] Submitting ${taskType} task to CapSolver (sitekey: ${captchaInfo.sitekey.slice(0, 12)}...)`);
118
+ // CapSolver requires `isInvisible: true` for invisible reCAPTCHA — sending
119
+ // a normal v2 task for an invisible sitekey returns
120
+ // "Invalid input, please check captcha type or pageUrl and invisible".
121
+ const task = {
122
+ type: taskType,
123
+ websiteURL: captchaInfo.pageUrl,
124
+ websiteKey: captchaInfo.sitekey,
125
+ };
126
+ if (captchaInfo.type === 'recaptcha_v2' && captchaInfo.isInvisible) {
127
+ task.isInvisible = true;
128
+ }
129
+
130
+ console.log(`[captcha] Submitting ${taskType}${task.isInvisible ? ' (invisible)' : ''} task to CapSolver (sitekey: ${captchaInfo.sitekey.slice(0, 12)}..., url: ${captchaInfo.pageUrl})`);
105
131
 
106
132
  let taskId;
107
133
  try {
108
134
  const createRes = await fetch(`${CAPSOLVER_API}/createTask`, {
109
135
  method: 'POST',
110
136
  headers: { 'Content-Type': 'application/json' },
111
- body: JSON.stringify({
112
- clientKey: apiKey,
113
- task: {
114
- type: taskType,
115
- websiteURL: captchaInfo.pageUrl,
116
- websiteKey: captchaInfo.sitekey,
117
- },
118
- }),
137
+ body: JSON.stringify({ clientKey: apiKey, task }),
119
138
  });
120
139
  const createData = await createRes.json();
121
140
  if (createData.errorId !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {