fiptcha 0.1.4 → 0.1.6
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/README.md +3 -0
- package/package.json +1 -1
- package/src/captcha-grid-solver.js +139 -34
- package/src/captcha-local-solver.js +25 -14
package/README.md
CHANGED
|
@@ -56,6 +56,9 @@ Pass one of these values as `captchaType`:
|
|
|
56
56
|
| `hcaptcha` | Interacts with the widget and solves supported image-grid challenges locally when required. |
|
|
57
57
|
| `turnstile` | Performs active-browser interaction and waits for a token. It does not use the image-grid vision path. |
|
|
58
58
|
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> **3×3 image-grid stability is currently not good.** Dynamic 3×3 challenges, particularly those that replace tiles after a selection, may be unreliable. 4×4 static image-grid challenges are currently the more stable path.
|
|
61
|
+
|
|
59
62
|
Fiptcha cannot guarantee that every challenge will be solvable. CAPTCHA providers can change their UI, challenge format, and risk checks independently of the visible widget.
|
|
60
63
|
|
|
61
64
|
## `solveLocalCaptcha(page, options)`
|
package/package.json
CHANGED
|
@@ -8,7 +8,9 @@ const PROVIDERS = Object.freeze({
|
|
|
8
8
|
cells: '.rc-imageselect-tile',
|
|
9
9
|
submit: '#recaptcha-verify-button',
|
|
10
10
|
noMatch: '#recaptcha-verify-button',
|
|
11
|
-
|
|
11
|
+
// `error-dynamic-more` is also shown while replacement tiles are still
|
|
12
|
+
// expected. Treating it as a rejection aborts valid dynamic challenges.
|
|
13
|
+
error: '.rc-imageselect-error-select-more, .rc-imageselect-incorrect-response',
|
|
12
14
|
cleanPrompt(text) {
|
|
13
15
|
return text.replace(/select all (images|squares) with/ig, '')
|
|
14
16
|
.replace(/click verify once there are none left/ig, '')
|
|
@@ -16,10 +18,12 @@ const PROVIDERS = Object.freeze({
|
|
|
16
18
|
}
|
|
17
19
|
}),
|
|
18
20
|
hcaptcha: Object.freeze({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Enterprise integrations may serve the iframe from a first-party
|
|
22
|
+
// asset host, so do not require an hcaptcha.com hostname.
|
|
23
|
+
framePatterns: ['hcaptcha.com', 'hcaptcha.html', 'hcaptcha-checkbox.html', 'frame=challenge', 'frame=checkbox'],
|
|
24
|
+
instruction: '.prompt-text, .challenge-header .prompt-text, .challenge-header h2, h2',
|
|
25
|
+
grid: '.task-grid, [class*="task-grid"]',
|
|
26
|
+
cells: '.task-grid .task-image, [class*="task-grid"] [class*="task-image"]',
|
|
23
27
|
submit: '.button-submit, button[type="submit"]',
|
|
24
28
|
noMatch: '.button-submit, button[type="submit"]',
|
|
25
29
|
error: '.error-text, .challenge-error',
|
|
@@ -32,7 +36,8 @@ const PROVIDERS = Object.freeze({
|
|
|
32
36
|
|
|
33
37
|
async function findChallengeFrame(page, adapter) {
|
|
34
38
|
for (const frame of page.frames()) {
|
|
35
|
-
|
|
39
|
+
const url = String(frame.url?.() || '').toLowerCase();
|
|
40
|
+
if (!adapter.framePatterns.some((pattern) => url.includes(pattern))) continue;
|
|
36
41
|
if (await frame.locator(adapter.grid).first().isVisible({ timeout: 250 }).catch(() => false)) return frame;
|
|
37
42
|
}
|
|
38
43
|
return null;
|
|
@@ -81,18 +86,32 @@ function mapDetectionsToCells(detections, gridBox, cellBoxes, imageSize = null)
|
|
|
81
86
|
for (const detection of detections) {
|
|
82
87
|
const box = normalizeBox(detection);
|
|
83
88
|
if (!box) continue;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
const projected = {
|
|
90
|
+
left: gridBox.x + (box.xmin * scaleX),
|
|
91
|
+
top: gridBox.y + (box.ymin * scaleY),
|
|
92
|
+
right: gridBox.x + (box.xmax * scaleX),
|
|
93
|
+
bottom: gridBox.y + (box.ymax * scaleY)
|
|
94
|
+
};
|
|
95
|
+
const centerX = (projected.left + projected.right) / 2;
|
|
96
|
+
const centerY = (projected.top + projected.bottom) / 2;
|
|
97
|
+
cellBoxes.forEach((cell, index) => {
|
|
98
|
+
if (!cell) return;
|
|
99
|
+
const intersectionWidth = Math.max(0, Math.min(projected.right, cell.x + cell.width) - Math.max(projected.left, cell.x));
|
|
100
|
+
const intersectionHeight = Math.max(0, Math.min(projected.bottom, cell.y + cell.height) - Math.max(projected.top, cell.y));
|
|
101
|
+
const coverage = (intersectionWidth * intersectionHeight) / Math.max(1, cell.width * cell.height);
|
|
102
|
+
const containsCenter = centerX >= cell.x && centerX <= cell.x + cell.width
|
|
103
|
+
&& centerY >= cell.y && centerY <= cell.y + cell.height;
|
|
104
|
+
// reCAPTCHA asks for every square containing part of an object, not
|
|
105
|
+
// merely the square containing the detector box's center.
|
|
106
|
+
if (containsCenter || coverage >= 0.08) indexes.add(index);
|
|
107
|
+
});
|
|
90
108
|
}
|
|
91
109
|
return [...indexes];
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
async function classifyGrid(frame, adapter, cells, label, seenTiles) {
|
|
95
113
|
const modelLabel = normalizeModelLabel(label);
|
|
114
|
+
const mapped = new Set();
|
|
96
115
|
const grid = frame.locator(adapter.grid).first();
|
|
97
116
|
const gridBox = await grid.boundingBox().catch(() => null);
|
|
98
117
|
if (gridBox) {
|
|
@@ -100,18 +119,7 @@ async function classifyGrid(frame, adapter, cells, label, seenTiles) {
|
|
|
100
119
|
const detections = await captchaModelManager.detect(image, modelLabel);
|
|
101
120
|
const cellBoxes = [];
|
|
102
121
|
for (let index = 0; index < await cells.count(); index += 1) cellBoxes.push(await cells.nth(index).boundingBox().catch(() => null));
|
|
103
|
-
const
|
|
104
|
-
if (mapped.length) {
|
|
105
|
-
const changed = [];
|
|
106
|
-
for (const index of mapped) {
|
|
107
|
-
const image = await cells.nth(index).screenshot({ type: 'png' });
|
|
108
|
-
const fingerprint = checksumBuffer(image);
|
|
109
|
-
if (seenTiles.get(index) === fingerprint) continue;
|
|
110
|
-
seenTiles.set(index, fingerprint);
|
|
111
|
-
changed.push(index);
|
|
112
|
-
}
|
|
113
|
-
return changed;
|
|
114
|
-
}
|
|
122
|
+
for (const index of mapDetectionsToCells(detections, gridBox, cellBoxes, pngDimensions(image))) mapped.add(index);
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
const selected = [];
|
|
@@ -121,11 +129,98 @@ async function classifyGrid(frame, adapter, cells, label, seenTiles) {
|
|
|
121
129
|
const fingerprint = checksumBuffer(image);
|
|
122
130
|
if (seenTiles.get(index) === fingerprint) continue;
|
|
123
131
|
seenTiles.set(index, fingerprint);
|
|
124
|
-
|
|
132
|
+
// A whole-grid pass is fast and preserves objects spanning cell edges,
|
|
133
|
+
// while a tile pass retains detail on dense 4x4 grids. Fuse both: a
|
|
134
|
+
// partial grid result must not suppress tile-level matches elsewhere.
|
|
135
|
+
if (mapped.has(index) || (await captchaModelManager.detect(image, modelLabel)).length) selected.push(index);
|
|
125
136
|
}
|
|
126
137
|
return selected;
|
|
127
138
|
}
|
|
128
139
|
|
|
140
|
+
async function rememberClickedTile(cells, index, seenTiles, beforeFingerprint = null) {
|
|
141
|
+
// Clicking a CAPTCHA tile changes its rendered appearance (selection overlay,
|
|
142
|
+
// border, checkmark, etc.). Remember that post-click state immediately so the
|
|
143
|
+
// next sweep does not mistake the selection UI itself for a replacement image
|
|
144
|
+
// and toggle an already-correct tile back off. A genuinely replaced tile will
|
|
145
|
+
// still produce a different fingerprint on the next sweep.
|
|
146
|
+
const cell = cells.nth(index);
|
|
147
|
+
const currentFingerprint = await tileFingerprint(cell);
|
|
148
|
+
// A fast replacement may already be present by the time click() resolves.
|
|
149
|
+
// Do not mark that unseen image as processed.
|
|
150
|
+
if (beforeFingerprint && currentFingerprint && currentFingerprint !== beforeFingerprint) return;
|
|
151
|
+
const image = await cell.screenshot({ type: 'png' }).catch(() => null);
|
|
152
|
+
if (image) seenTiles.set(index, checksumBuffer(image));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function tileFingerprint(cell) {
|
|
156
|
+
const source = typeof cell.evaluate === 'function' ? await cell.evaluate((element) => {
|
|
157
|
+
const images = [...element.querySelectorAll('img')];
|
|
158
|
+
if (images.length) return images.map((image) => [image.currentSrc, image.src, image.style.transform].join('|')).join('\n');
|
|
159
|
+
const styled = [element, ...element.querySelectorAll('*')]
|
|
160
|
+
.map((candidate) => getComputedStyle(candidate).backgroundImage)
|
|
161
|
+
.filter((value) => value && value !== 'none');
|
|
162
|
+
return styled.join('\n');
|
|
163
|
+
}).catch(() => '') : '';
|
|
164
|
+
if (source) return checksumBuffer(Buffer.from(source));
|
|
165
|
+
const image = await cell.screenshot({ type: 'png' }).catch(() => null);
|
|
166
|
+
return image ? checksumBuffer(image) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function tileFingerprints(cells, indexes) {
|
|
170
|
+
const fingerprints = new Map();
|
|
171
|
+
for (const index of indexes) {
|
|
172
|
+
const fingerprint = await tileFingerprint(cells.nth(index));
|
|
173
|
+
if (fingerprint) fingerprints.set(index, fingerprint);
|
|
174
|
+
}
|
|
175
|
+
return fingerprints;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function waitForTileReplacement(page, cells, before, deadline, timeout = 2500) {
|
|
179
|
+
const replacementDeadline = Math.min(deadline, Date.now() + timeout);
|
|
180
|
+
const pending = new Map(before);
|
|
181
|
+
let changed = false;
|
|
182
|
+
while (Date.now() < replacementDeadline) {
|
|
183
|
+
for (const [index, fingerprint] of pending) {
|
|
184
|
+
const current = await tileFingerprint(cells.nth(index));
|
|
185
|
+
if (current && current !== fingerprint) {
|
|
186
|
+
pending.delete(index);
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!pending.size) {
|
|
191
|
+
// Let the replacement animation finish before inference.
|
|
192
|
+
await page.waitForTimeout(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
await page.waitForTimeout(Math.min(100, Math.max(1, replacementDeadline - Date.now())));
|
|
196
|
+
}
|
|
197
|
+
return changed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function challengeSignature(frame, adapter) {
|
|
201
|
+
const grid = frame?.locator(adapter.grid).first();
|
|
202
|
+
const image = await grid?.screenshot({ type: 'png' }).catch(() => null);
|
|
203
|
+
if (!image) return null;
|
|
204
|
+
const prompt = normalizePrompt(adapter, await visibleText(frame, adapter.instruction));
|
|
205
|
+
const count = await frame.locator(adapter.cells).count().catch(() => 0);
|
|
206
|
+
return `${prompt}\n${count}\n${checksumBuffer(image)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function waitForSubmission(page, adapter, captchaType, previousSignature, deadline, waitForToken) {
|
|
210
|
+
const transitionDeadline = Math.min(deadline, Date.now() + 5000);
|
|
211
|
+
while (Date.now() < transitionDeadline) {
|
|
212
|
+
const token = await waitForToken(page, captchaType, 0);
|
|
213
|
+
if (token) return { token, transitioned: false };
|
|
214
|
+
const frame = await findChallengeFrame(page, adapter);
|
|
215
|
+
if (frame) {
|
|
216
|
+
const signature = await challengeSignature(frame, adapter);
|
|
217
|
+
if (previousSignature && signature && signature !== previousSignature) return { token: null, transitioned: true };
|
|
218
|
+
}
|
|
219
|
+
await page.waitForTimeout(Math.min(125, Math.max(1, transitionDeadline - Date.now())));
|
|
220
|
+
}
|
|
221
|
+
return { token: null, transitioned: false };
|
|
222
|
+
}
|
|
223
|
+
|
|
129
224
|
async function visibleText(frame, selector) {
|
|
130
225
|
const matches = frame.locator(selector);
|
|
131
226
|
for (let index = 0; index < await matches.count().catch(() => 0); index += 1) {
|
|
@@ -141,21 +236,24 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
141
236
|
if (!adapter) throw new Error(`No image-grid adapter for ${captchaType}`);
|
|
142
237
|
const seenTiles = new Map();
|
|
143
238
|
let previousPrompt = '';
|
|
144
|
-
for (let round = 0; round <
|
|
239
|
+
for (let round = 0; round < 12 && Date.now() < deadline; round += 1) {
|
|
145
240
|
const frame = await findChallengeFrame(page, adapter);
|
|
146
241
|
if (!frame) throw new Error(`${captchaType} image challenge frame disappeared`);
|
|
147
242
|
const rejection = await visibleText(frame, adapter.error);
|
|
148
243
|
if (rejection && round > 0) {
|
|
149
244
|
throw new Error(`${captchaType} rejected the previous selection: ${rejection.replace(/\s+/g, ' ').trim()}`);
|
|
150
245
|
}
|
|
151
|
-
const
|
|
246
|
+
const instruction = await visibleText(frame, adapter.instruction);
|
|
247
|
+
const prompt = normalizePrompt(adapter, instruction);
|
|
248
|
+
const dynamic = captchaType === 'recaptcha_v2'
|
|
249
|
+
&& /(?:none left|new images|replaced)/i.test(instruction);
|
|
152
250
|
if (!prompt) throw new Error(`${captchaType} solver could not read the image challenge instruction`);
|
|
153
251
|
if (prompt !== previousPrompt) {
|
|
154
252
|
seenTiles.clear();
|
|
155
253
|
previousPrompt = prompt;
|
|
156
254
|
}
|
|
157
255
|
let totalSelections = 0;
|
|
158
|
-
for (let sweep = 0; sweep <
|
|
256
|
+
for (let sweep = 0; sweep < 12 && Date.now() < deadline; sweep += 1) {
|
|
159
257
|
const cells = frame.locator(adapter.cells);
|
|
160
258
|
const count = await cells.count();
|
|
161
259
|
if (![9, 16].includes(count)) throw new Error(`Unsupported ${captchaType} grid size (${count} cells)`);
|
|
@@ -163,18 +261,25 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
163
261
|
// Grid cells live inside a provider iframe. Let Playwright target the
|
|
164
262
|
// element in that frame instead of sending a page-level mouse click:
|
|
165
263
|
// the latter can miss the cell when iframe coordinates change.
|
|
166
|
-
|
|
264
|
+
const before = await tileFingerprints(cells, selected);
|
|
265
|
+
for (const index of selected) {
|
|
266
|
+
await cells.nth(index).click({ timeout: 2000 });
|
|
267
|
+
await rememberClickedTile(cells, index, seenTiles, before.get(index));
|
|
268
|
+
}
|
|
167
269
|
totalSelections += selected.length;
|
|
168
270
|
if (!selected.length) break;
|
|
169
|
-
|
|
271
|
+
// Dynamic 3x3 challenges replace selected tiles. Static and 4x4
|
|
272
|
+
// challenges do not, so stop sweeping once no replacement occurs.
|
|
273
|
+
if (!dynamic || !await waitForTileReplacement(page, cells, before, deadline)) break;
|
|
170
274
|
}
|
|
171
275
|
logs.push(`Local ${captchaType} grid round ${round + 1}: selected ${totalSelections} cells for "${prompt}"`);
|
|
276
|
+
const signature = await challengeSignature(frame, adapter);
|
|
172
277
|
await frame.locator(totalSelections ? adapter.submit : adapter.noMatch).first().click({ timeout: 2000 });
|
|
173
|
-
const
|
|
174
|
-
if (token) return token;
|
|
175
|
-
await page.waitForTimeout(350);
|
|
278
|
+
const result = await waitForSubmission(page, adapter, captchaType, signature, deadline, waitForToken);
|
|
279
|
+
if (result.token) return result.token;
|
|
280
|
+
if (!result.transitioned) await page.waitForTimeout(Math.min(350, Math.max(0, deadline - Date.now())));
|
|
176
281
|
}
|
|
177
282
|
return null;
|
|
178
283
|
}
|
|
179
284
|
|
|
180
|
-
module.exports = { PROVIDERS, normalizePrompt, normalizeModelLabel, normalizeBox, pngDimensions, mapDetectionsToCells, classifyGrid, visibleText, solveImageGrid };
|
|
285
|
+
module.exports = { PROVIDERS, normalizePrompt, normalizeModelLabel, normalizeBox, pngDimensions, mapDetectionsToCells, classifyGrid, rememberClickedTile, visibleText, tileFingerprint, tileFingerprints, waitForTileReplacement, challengeSignature, waitForSubmission, solveImageGrid };
|
|
@@ -14,8 +14,8 @@ const WIDGET_INTERACTIONS = Object.freeze({
|
|
|
14
14
|
selectors: ['#recaptcha-anchor', '[role="checkbox"]']
|
|
15
15
|
}),
|
|
16
16
|
hcaptcha: Object.freeze({
|
|
17
|
-
framePatterns: ['hcaptcha.com
|
|
18
|
-
selectors: ['#checkbox', '[role="checkbox"]', 'input[type="checkbox"]', 'label']
|
|
17
|
+
framePatterns: ['hcaptcha.com', 'hcaptcha.html', 'hcaptcha-checkbox.html', 'frame=checkbox'],
|
|
18
|
+
selectors: ['#checkbox', '.check', '#anchor', '[role="checkbox"]', 'input[type="checkbox"]', 'label']
|
|
19
19
|
}),
|
|
20
20
|
turnstile: Object.freeze({
|
|
21
21
|
framePatterns: ['challenges.cloudflare.com'],
|
|
@@ -23,6 +23,11 @@ const WIDGET_INTERACTIONS = Object.freeze({
|
|
|
23
23
|
})
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
function frameMatches(frame, patterns) {
|
|
27
|
+
const url = String(frame?.url?.() || '').toLowerCase();
|
|
28
|
+
return patterns.some((pattern) => url.includes(pattern));
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
async function readToken(page, captchaType) {
|
|
27
32
|
return page.evaluate(({ type, selectors }) => {
|
|
28
33
|
for (const selector of selectors[type] || []) {
|
|
@@ -63,7 +68,7 @@ async function waitForTokenOrChallenge(page, captchaType, timeout) {
|
|
|
63
68
|
if (token) return token;
|
|
64
69
|
if (adapter) {
|
|
65
70
|
for (const frame of page.frames?.() || []) {
|
|
66
|
-
if (!adapter.framePatterns
|
|
71
|
+
if (!frameMatches(frame, adapter.framePatterns)) continue;
|
|
67
72
|
if (await frame.locator(adapter.grid).first().isVisible({ timeout: 100 }).catch(() => false)) return null;
|
|
68
73
|
}
|
|
69
74
|
}
|
|
@@ -80,8 +85,7 @@ async function clickCheckbox(page, captchaType, timeout = 10_000) {
|
|
|
80
85
|
|
|
81
86
|
do {
|
|
82
87
|
for (const frame of page.frames?.() || []) {
|
|
83
|
-
|
|
84
|
-
if (!interaction.framePatterns.some((pattern) => frameUrl.includes(pattern))) continue;
|
|
88
|
+
if (!frameMatches(frame, interaction.framePatterns)) continue;
|
|
85
89
|
|
|
86
90
|
for (const selector of interaction.selectors) {
|
|
87
91
|
const matches = frame.locator(selector);
|
|
@@ -112,13 +116,17 @@ async function clickCheckbox(page, captchaType, timeout = 10_000) {
|
|
|
112
116
|
async function executeHcaptchaWidget(page) {
|
|
113
117
|
return page.evaluate(() => {
|
|
114
118
|
if (typeof globalThis.hcaptcha?.execute !== 'function') return false;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
const ids = [...document.querySelectorAll('[data-hcaptcha-widget-id]')]
|
|
120
|
+
.map((element) => element.getAttribute('data-hcaptcha-widget-id'))
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
for (const id of [...new Set([...ids, null])]) {
|
|
123
|
+
try {
|
|
124
|
+
const pending = id === null ? globalThis.hcaptcha.execute() : globalThis.hcaptcha.execute(id);
|
|
125
|
+
if (pending && typeof pending.catch === 'function') pending.catch(() => {});
|
|
126
|
+
return true;
|
|
127
|
+
} catch { /* try the default/next widget */ }
|
|
121
128
|
}
|
|
129
|
+
return false;
|
|
122
130
|
}).catch(() => false);
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -133,7 +141,10 @@ async function solveLocalCaptcha(page, { captchaType, timeout = 60_000, logs = [
|
|
|
133
141
|
: null;
|
|
134
142
|
let token = await readToken(page, captchaType);
|
|
135
143
|
if (!token) {
|
|
136
|
-
|
|
144
|
+
// Invisible hCaptcha has no checkbox; do not spend ten seconds looking
|
|
145
|
+
// for one before invoking its documented execute path.
|
|
146
|
+
const interactionTimeout = captchaType === 'hcaptcha' ? 2500 : 10_000;
|
|
147
|
+
const clickTimeout = Math.min(interactionTimeout, Math.max(0, deadline - Date.now()));
|
|
137
148
|
const clicked = await clickCheckbox(page, captchaType, clickTimeout)
|
|
138
149
|
.catch((error) => {
|
|
139
150
|
logs.push(`Local ${captchaType} checkbox click failed: ${error.message}`);
|
|
@@ -159,7 +170,7 @@ async function solveLocalCaptcha(page, { captchaType, timeout = 60_000, logs = [
|
|
|
159
170
|
let challengeVisible = false;
|
|
160
171
|
while (!challengeVisible && Date.now() < frameDeadline) {
|
|
161
172
|
for (const frame of page.frames?.() || []) {
|
|
162
|
-
if (!adapter.framePatterns
|
|
173
|
+
if (!frameMatches(frame, adapter.framePatterns)) continue;
|
|
163
174
|
challengeVisible = await frame.locator(adapter.grid).first().isVisible({ timeout: 100 }).catch(() => false);
|
|
164
175
|
if (challengeVisible) break;
|
|
165
176
|
}
|
|
@@ -185,4 +196,4 @@ async function solveLocalCaptcha(page, { captchaType, timeout = 60_000, logs = [
|
|
|
185
196
|
};
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
module.exports = { TOKEN_SELECTORS, WIDGET_INTERACTIONS, readToken, waitForToken, waitForTokenOrChallenge, clickCheckbox, executeHcaptchaWidget, solveLocalCaptcha };
|
|
199
|
+
module.exports = { TOKEN_SELECTORS, WIDGET_INTERACTIONS, frameMatches, readToken, waitForToken, waitForTokenOrChallenge, clickCheckbox, executeHcaptchaWidget, solveLocalCaptcha };
|