fiptcha 0.1.5 → 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 +128 -36
- 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,21 +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
|
|
|
129
|
-
async function rememberClickedTile(cells, index, seenTiles) {
|
|
140
|
+
async function rememberClickedTile(cells, index, seenTiles, beforeFingerprint = null) {
|
|
130
141
|
// Clicking a CAPTCHA tile changes its rendered appearance (selection overlay,
|
|
131
142
|
// border, checkmark, etc.). Remember that post-click state immediately so the
|
|
132
143
|
// next sweep does not mistake the selection UI itself for a replacement image
|
|
133
144
|
// and toggle an already-correct tile back off. A genuinely replaced tile will
|
|
134
145
|
// still produce a different fingerprint on the next sweep.
|
|
135
|
-
const
|
|
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);
|
|
136
152
|
if (image) seenTiles.set(index, checksumBuffer(image));
|
|
137
153
|
}
|
|
138
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
|
+
|
|
139
224
|
async function visibleText(frame, selector) {
|
|
140
225
|
const matches = frame.locator(selector);
|
|
141
226
|
for (let index = 0; index < await matches.count().catch(() => 0); index += 1) {
|
|
@@ -151,21 +236,24 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
151
236
|
if (!adapter) throw new Error(`No image-grid adapter for ${captchaType}`);
|
|
152
237
|
const seenTiles = new Map();
|
|
153
238
|
let previousPrompt = '';
|
|
154
|
-
for (let round = 0; round <
|
|
239
|
+
for (let round = 0; round < 12 && Date.now() < deadline; round += 1) {
|
|
155
240
|
const frame = await findChallengeFrame(page, adapter);
|
|
156
241
|
if (!frame) throw new Error(`${captchaType} image challenge frame disappeared`);
|
|
157
242
|
const rejection = await visibleText(frame, adapter.error);
|
|
158
243
|
if (rejection && round > 0) {
|
|
159
244
|
throw new Error(`${captchaType} rejected the previous selection: ${rejection.replace(/\s+/g, ' ').trim()}`);
|
|
160
245
|
}
|
|
161
|
-
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);
|
|
162
250
|
if (!prompt) throw new Error(`${captchaType} solver could not read the image challenge instruction`);
|
|
163
251
|
if (prompt !== previousPrompt) {
|
|
164
252
|
seenTiles.clear();
|
|
165
253
|
previousPrompt = prompt;
|
|
166
254
|
}
|
|
167
255
|
let totalSelections = 0;
|
|
168
|
-
for (let sweep = 0; sweep <
|
|
256
|
+
for (let sweep = 0; sweep < 12 && Date.now() < deadline; sweep += 1) {
|
|
169
257
|
const cells = frame.locator(adapter.cells);
|
|
170
258
|
const count = await cells.count();
|
|
171
259
|
if (![9, 16].includes(count)) throw new Error(`Unsupported ${captchaType} grid size (${count} cells)`);
|
|
@@ -173,21 +261,25 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
173
261
|
// Grid cells live inside a provider iframe. Let Playwright target the
|
|
174
262
|
// element in that frame instead of sending a page-level mouse click:
|
|
175
263
|
// the latter can miss the cell when iframe coordinates change.
|
|
264
|
+
const before = await tileFingerprints(cells, selected);
|
|
176
265
|
for (const index of selected) {
|
|
177
266
|
await cells.nth(index).click({ timeout: 2000 });
|
|
178
|
-
await rememberClickedTile(cells, index, seenTiles);
|
|
267
|
+
await rememberClickedTile(cells, index, seenTiles, before.get(index));
|
|
179
268
|
}
|
|
180
269
|
totalSelections += selected.length;
|
|
181
270
|
if (!selected.length) break;
|
|
182
|
-
|
|
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;
|
|
183
274
|
}
|
|
184
275
|
logs.push(`Local ${captchaType} grid round ${round + 1}: selected ${totalSelections} cells for "${prompt}"`);
|
|
276
|
+
const signature = await challengeSignature(frame, adapter);
|
|
185
277
|
await frame.locator(totalSelections ? adapter.submit : adapter.noMatch).first().click({ timeout: 2000 });
|
|
186
|
-
const
|
|
187
|
-
if (token) return token;
|
|
188
|
-
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())));
|
|
189
281
|
}
|
|
190
282
|
return null;
|
|
191
283
|
}
|
|
192
284
|
|
|
193
|
-
module.exports = { PROVIDERS, normalizePrompt, normalizeModelLabel, normalizeBox, pngDimensions, mapDetectionsToCells, classifyGrid, rememberClickedTile, 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 };
|