@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.
@@ -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.1.10",
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": {