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.
- package/captcha.js +46 -27
- 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
|
|
26
|
-
|
|
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
|
-
|
|
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'),
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
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) {
|