@showrun/core 0.1.10 → 0.2.0-rc.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.
- package/dist/__tests__/playwrightJsExecutor.test.d.ts +2 -0
- package/dist/__tests__/playwrightJsExecutor.test.d.ts.map +1 -0
- package/dist/__tests__/playwrightJsExecutor.test.js +191 -0
- package/dist/dsl/playwrightJsExecutor.d.ts +56 -0
- package/dist/dsl/playwrightJsExecutor.d.ts.map +1 -0
- package/dist/dsl/playwrightJsExecutor.js +134 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loader.d.ts +9 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +39 -4
- package/dist/packUtils.d.ts +4 -0
- package/dist/packUtils.d.ts.map +1 -1
- package/dist/packUtils.js +7 -0
- package/dist/registry/client.d.ts.map +1 -1
- package/dist/registry/client.js +46 -13
- package/dist/runner.d.ts +25 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +89 -38
- package/dist/types.d.ts +27 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/util/index.d.ts +66 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +21 -0
- package/dist/util/turnstile.d.ts +105 -0
- package/dist/util/turnstile.d.ts.map +1 -0
- package/dist/util/turnstile.js +242 -0
- package/package.json +2 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile detection and solving utilities.
|
|
3
|
+
*
|
|
4
|
+
* Turnstile is Cloudflare's CAPTCHA alternative. This module provides
|
|
5
|
+
* image-based detection of the Turnstile widget checkbox position,
|
|
6
|
+
* useful when shadow-DOM inspection is blocked.
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Screenshot the page
|
|
10
|
+
* 2. Detect the widget by its characteristic light/dark gray background
|
|
11
|
+
* 3. Find the checkbox at a fixed offset from the widget's left edge
|
|
12
|
+
* 4. Click the detected position
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Check if RGB values match light theme widget background.
|
|
16
|
+
* Light theme: ~RGB(245-252, 245-252, 245-252)
|
|
17
|
+
*/
|
|
18
|
+
function isLightThemePixel(r, g, b) {
|
|
19
|
+
return (r >= 245 && r <= 252 &&
|
|
20
|
+
g >= 245 && g <= 252 &&
|
|
21
|
+
b >= 245 && b <= 252 &&
|
|
22
|
+
Math.abs(r - g) < 5 &&
|
|
23
|
+
Math.abs(g - b) < 5);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if RGB values match dark theme widget background.
|
|
27
|
+
* Dark theme: ~RGB(45-55, 45-55, 45-55)
|
|
28
|
+
*/
|
|
29
|
+
function isDarkThemePixel(r, g, b) {
|
|
30
|
+
return (r >= 45 && r <= 55 &&
|
|
31
|
+
g >= 45 && g <= 55 &&
|
|
32
|
+
b >= 45 && b <= 55 &&
|
|
33
|
+
Math.abs(r - g) < 5 &&
|
|
34
|
+
Math.abs(g - b) < 5);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Detect Turnstile widget from raw pixel data.
|
|
38
|
+
*/
|
|
39
|
+
function detectFromPixels(data, width, height, channels, scale) {
|
|
40
|
+
// Step 1: Find rows with widget background pixels (try light theme first)
|
|
41
|
+
for (const theme of ['light', 'dark']) {
|
|
42
|
+
const isWidgetPixel = theme === 'light' ? isLightThemePixel : isDarkThemePixel;
|
|
43
|
+
const rowData = [];
|
|
44
|
+
for (let y = 0; y < height; y++) {
|
|
45
|
+
let count = 0;
|
|
46
|
+
let minX = width;
|
|
47
|
+
let maxX = 0;
|
|
48
|
+
for (let x = 0; x < width; x++) {
|
|
49
|
+
const idx = (y * width + x) * channels;
|
|
50
|
+
const r = data[idx];
|
|
51
|
+
const g = data[idx + 1];
|
|
52
|
+
const b = data[idx + 2];
|
|
53
|
+
if (isWidgetPixel(r, g, b)) {
|
|
54
|
+
count++;
|
|
55
|
+
minX = Math.min(minX, x);
|
|
56
|
+
maxX = Math.max(maxX, x);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (count > 100 * scale) {
|
|
60
|
+
rowData.push({ y, count, minX, maxX });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (rowData.length === 0)
|
|
64
|
+
continue;
|
|
65
|
+
// Step 2: Find contiguous bands
|
|
66
|
+
const bands = [];
|
|
67
|
+
let currentBand = null;
|
|
68
|
+
for (const row of rowData) {
|
|
69
|
+
if (!currentBand) {
|
|
70
|
+
currentBand = { startY: row.y, endY: row.y, rows: [row] };
|
|
71
|
+
}
|
|
72
|
+
else if (row.y - currentBand.endY <= 2) {
|
|
73
|
+
currentBand.endY = row.y;
|
|
74
|
+
currentBand.rows.push(row);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
bands.push(currentBand);
|
|
78
|
+
currentBand = { startY: row.y, endY: row.y, rows: [row] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (currentBand)
|
|
82
|
+
bands.push(currentBand);
|
|
83
|
+
// Step 3: Filter to bands matching Turnstile dimensions
|
|
84
|
+
// Height: 40-100px (scaled), Width: 200-400px (scaled)
|
|
85
|
+
const widgetBands = bands.filter((b) => {
|
|
86
|
+
const h = b.endY - b.startY;
|
|
87
|
+
const minX = Math.min(...b.rows.map((r) => r.minX));
|
|
88
|
+
const maxX = Math.max(...b.rows.map((r) => r.maxX));
|
|
89
|
+
const w = maxX - minX;
|
|
90
|
+
return h >= 40 * scale && h <= 100 * scale && w >= 200 * scale && w <= 400 * scale;
|
|
91
|
+
});
|
|
92
|
+
if (widgetBands.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
// Use the first matching band
|
|
95
|
+
const widgetBand = widgetBands[0];
|
|
96
|
+
const allMinX = Math.min(...widgetBand.rows.map((r) => r.minX));
|
|
97
|
+
const allMaxX = Math.max(...widgetBand.rows.map((r) => r.maxX));
|
|
98
|
+
const widget = {
|
|
99
|
+
x: allMinX,
|
|
100
|
+
y: widgetBand.startY,
|
|
101
|
+
width: allMaxX - allMinX,
|
|
102
|
+
height: widgetBand.endY - widgetBand.startY,
|
|
103
|
+
};
|
|
104
|
+
// Step 4: Checkbox is ~40px from left edge, centered vertically
|
|
105
|
+
const checkboxOffsetX = 40 * scale;
|
|
106
|
+
const checkboxCenterX = widget.x + checkboxOffsetX;
|
|
107
|
+
const checkboxCenterY = widget.y + Math.round(widget.height / 2);
|
|
108
|
+
return {
|
|
109
|
+
found: true,
|
|
110
|
+
x: checkboxCenterX,
|
|
111
|
+
y: checkboxCenterY,
|
|
112
|
+
widget,
|
|
113
|
+
theme,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { found: false, error: 'No Turnstile widget detected' };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Detect Cloudflare Turnstile checkbox position on the page.
|
|
120
|
+
*
|
|
121
|
+
* Uses image-based detection since Turnstile uses shadow-DOM
|
|
122
|
+
* which blocks direct element inspection.
|
|
123
|
+
*
|
|
124
|
+
* @param page - Playwright Page object
|
|
125
|
+
* @param options - Detection options
|
|
126
|
+
* @returns Detection result with checkbox coordinates
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const result = await detectCloudflareTurnstile(page);
|
|
131
|
+
* if (result.found) {
|
|
132
|
+
* await page.mouse.click(result.x, result.y);
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export async function detectCloudflareTurnstile(page, options = {}) {
|
|
137
|
+
const scale = options.scale ?? 1;
|
|
138
|
+
try {
|
|
139
|
+
// Take a screenshot and get raw pixel data
|
|
140
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
141
|
+
// We need to decode the PNG to get raw pixel data
|
|
142
|
+
// Use dynamic import for sharp to avoid hard dependency
|
|
143
|
+
let sharpModule;
|
|
144
|
+
try {
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
146
|
+
sharpModule = require('sharp');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return {
|
|
150
|
+
found: false,
|
|
151
|
+
error: 'sharp module not available for image processing',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const sharp = sharpModule;
|
|
155
|
+
const { data, info } = await sharp(screenshot)
|
|
156
|
+
.raw()
|
|
157
|
+
.toBuffer({ resolveWithObject: true });
|
|
158
|
+
return detectFromPixels(new Uint8Array(data), info.width, info.height, info.channels, scale);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
found: false,
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Detect and solve Cloudflare Turnstile by clicking the checkbox.
|
|
169
|
+
*
|
|
170
|
+
* This function:
|
|
171
|
+
* 1. Detects the Turnstile widget using image analysis
|
|
172
|
+
* 2. Clicks the checkbox
|
|
173
|
+
* 3. Waits briefly for the verification to process
|
|
174
|
+
*
|
|
175
|
+
* @param page - Playwright Page object
|
|
176
|
+
* @param options - Solve options
|
|
177
|
+
* @returns Solve result indicating success/failure
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* const result = await solveCloudflareTurnstile(page);
|
|
182
|
+
* if (result.success) {
|
|
183
|
+
* console.log('Turnstile solved!');
|
|
184
|
+
* } else {
|
|
185
|
+
* console.log('Failed:', result.error);
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export async function solveCloudflareTurnstile(page, options = {}) {
|
|
190
|
+
const { scale = 1, waitAfterClick = 2000, retries = 3, retryDelay = 1000, } = options;
|
|
191
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
192
|
+
// Detect the Turnstile widget
|
|
193
|
+
const detection = await detectCloudflareTurnstile(page, { scale });
|
|
194
|
+
if (!detection.found) {
|
|
195
|
+
// Maybe the page hasn't loaded yet, wait and retry
|
|
196
|
+
if (attempt < retries - 1) {
|
|
197
|
+
await page.waitForTimeout(retryDelay);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
detected: false,
|
|
203
|
+
error: detection.error || 'Turnstile widget not found',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Click the checkbox
|
|
207
|
+
try {
|
|
208
|
+
await page.mouse.click(detection.x, detection.y);
|
|
209
|
+
// Wait for verification
|
|
210
|
+
await page.waitForTimeout(waitAfterClick);
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
detected: true,
|
|
214
|
+
clickedAt: { x: detection.x, y: detection.y },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (attempt < retries - 1) {
|
|
219
|
+
await page.waitForTimeout(retryDelay);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
detected: true,
|
|
225
|
+
clickedAt: { x: detection.x, y: detection.y },
|
|
226
|
+
error: error instanceof Error ? error.message : String(error),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
detected: false,
|
|
233
|
+
error: 'Max retries exceeded',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Utility object exposed to playwright-js flows via context.util
|
|
238
|
+
*/
|
|
239
|
+
export const turnstileUtil = {
|
|
240
|
+
detectCloudflareTurnstile,
|
|
241
|
+
solveCloudflareTurnstile,
|
|
242
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@showrun/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.0",
|
|
4
4
|
"description": "Core types and utilities for Task Pack framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@jmespath-community/jmespath": "^1.3.0",
|
|
19
19
|
"camoufox-js": "^0.8.5",
|
|
20
|
+
"impit": "^0.10.1",
|
|
20
21
|
"nunjucks": "^3.2.4",
|
|
21
22
|
"otplib": "^12.0.1",
|
|
22
|
-
"impit": "^0.10.1",
|
|
23
23
|
"playwright": "^1.58.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|