aether-mcp-server 2.0.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,303 @@
1
+ "use strict";
2
+ /**
3
+ * Captcha solver — tries human simulation first (no API key needed).
4
+ * Falls back to third-party service only when explicitly requested.
5
+ *
6
+ * Human approach:
7
+ * 1. Find the captcha widget's viewport coordinates via evaluate()
8
+ * 2. Move the mouse along a cubic Bezier arc with per-step jitter + random delays
9
+ * 3. Click the checkbox with a natural press/release gap
10
+ * 4. Wait and check whether the captcha cleared
11
+ *
12
+ * This works for: Cloudflare Turnstile, reCAPTCHA v2 checkbox, hCaptcha checkbox.
13
+ * Image-grid challenges require the paid service fallback.
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.humanSolveCaptcha = humanSolveCaptcha;
20
+ exports.detectAndSolve = detectAndSolve;
21
+ // ---------------------------------------------------------------------------
22
+ // Utilities
23
+ // ---------------------------------------------------------------------------
24
+ function rng(min, max) { return min + Math.random() * (max - min); }
25
+ function rngInt(min, max) { return Math.round(rng(min, max)); }
26
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
27
+ /** Cubic Bezier interpolation at t∈[0,1] */
28
+ function bezier(t, p0, p1, p2, p3) {
29
+ const u = 1 - t;
30
+ return {
31
+ x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,
32
+ y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y,
33
+ };
34
+ }
35
+ /**
36
+ * Move the mouse from `from` to `to` using a cubic Bezier arc.
37
+ * Control points are randomised so each path looks unique.
38
+ * `sendCommand` must be the CDP channel (Input.dispatchMouseEvent).
39
+ */
40
+ async function humanMouseMove(from, to, sendCommand) {
41
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
42
+ // ~1 step per 4px, 20-60 steps
43
+ const steps = Math.max(20, Math.min(60, Math.round(dist / 4)));
44
+ // Randomise control points (arc that curves left or right organically)
45
+ const spread = dist * rng(0.3, 0.6);
46
+ const angle = Math.atan2(to.y - from.y, to.x - from.x) + Math.PI / 2;
47
+ const sign = Math.random() < 0.5 ? 1 : -1;
48
+ const cp1 = {
49
+ x: from.x + (to.x - from.x) * rng(0.1, 0.3) + Math.cos(angle) * spread * sign * rng(0.3, 1),
50
+ y: from.y + (to.y - from.y) * rng(0.1, 0.3) + Math.sin(angle) * spread * sign * rng(0.3, 1),
51
+ };
52
+ const cp2 = {
53
+ x: from.x + (to.x - from.x) * rng(0.7, 0.9) + Math.cos(angle) * spread * sign * rng(0.1, 0.5),
54
+ y: from.y + (to.y - from.y) * rng(0.7, 0.9) + Math.sin(angle) * spread * sign * rng(0.1, 0.5),
55
+ };
56
+ for (let i = 1; i <= steps; i++) {
57
+ const t = i / steps;
58
+ // Ease-in-out: slow start, fast middle, slow end
59
+ const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
60
+ const pt = bezier(eased, from, cp1, cp2, to);
61
+ // Small per-step jitter simulates hand tremor
62
+ const jx = rng(-0.8, 0.8);
63
+ const jy = rng(-0.8, 0.8);
64
+ await sendCommand("Input.dispatchMouseEvent", {
65
+ type: "mouseMoved",
66
+ x: Math.round(pt.x + jx),
67
+ y: Math.round(pt.y + jy),
68
+ buttons: 0,
69
+ pointerType: "mouse",
70
+ });
71
+ // Variable delay: faster in the middle, slower near target
72
+ const nearEnd = i > steps * 0.85;
73
+ await sleep(nearEnd ? rng(8, 20) : rng(2, 8));
74
+ }
75
+ }
76
+ /** Human-like click: slight pre-click pause, natural press/release gap */
77
+ async function humanClick(pos, sendCommand) {
78
+ // Brief hover pause before pressing
79
+ await sleep(rng(80, 180));
80
+ const x = Math.round(pos.x + rng(-2, 2));
81
+ const y = Math.round(pos.y + rng(-2, 2));
82
+ await sendCommand("Input.dispatchMouseEvent", {
83
+ type: "mousePressed", x, y, button: "left", clickCount: 1, pointerType: "mouse",
84
+ });
85
+ await sleep(rng(60, 140)); // human hold time
86
+ await sendCommand("Input.dispatchMouseEvent", {
87
+ type: "mouseReleased", x, y, button: "left", clickCount: 1, pointerType: "mouse",
88
+ });
89
+ }
90
+ const FIND_WIDGET_SCRIPT = `(function() {
91
+ function rect(el) {
92
+ const r = el.getBoundingClientRect();
93
+ return { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) };
94
+ }
95
+ // Cloudflare Turnstile
96
+ let el = document.querySelector('iframe[src*="challenges.cloudflare.com"]');
97
+ if (el) { const r = rect(el); return { ...r, type: 'turnstile', checkboxX: 28, checkboxY: 28 }; }
98
+ el = document.querySelector('.cf-turnstile iframe');
99
+ if (el) { const r = rect(el); return { ...r, type: 'turnstile', checkboxX: 28, checkboxY: 28 }; }
100
+
101
+ // hCaptcha
102
+ el = document.querySelector('iframe[src*="hcaptcha.com/captcha"]');
103
+ if (!el) el = document.querySelector('.h-captcha iframe');
104
+ if (el) { const r = rect(el); return { ...r, type: 'hcaptcha', checkboxX: 22, checkboxY: Math.round(r.height / 2) }; }
105
+
106
+ // reCAPTCHA v2 checkbox iframe
107
+ el = document.querySelector('iframe[title="reCAPTCHA"]');
108
+ if (!el) el = document.querySelector('iframe[src*="recaptcha/api2/anchor"]');
109
+ if (!el) el = document.querySelector('iframe[src*="google.com/recaptcha"][src*="anchor"]');
110
+ if (el) { const r = rect(el); return { ...r, type: 'recaptcha_v2', checkboxX: 28, checkboxY: Math.round(r.height / 2) }; }
111
+
112
+ // Generic: any [data-sitekey] container
113
+ el = document.querySelector('[data-sitekey]');
114
+ if (el) { const r = rect(el); return { ...r, type: 'unknown', checkboxX: Math.round(r.width / 2), checkboxY: Math.round(r.height / 2) }; }
115
+
116
+ return null;
117
+ })()`;
118
+ /** Returns true if any captcha widget is still present after solving attempt */
119
+ const STILL_PRESENT_SCRIPT = `(function() {
120
+ const sel = [
121
+ 'iframe[src*="challenges.cloudflare.com"]',
122
+ 'iframe[src*="hcaptcha.com/captcha"]',
123
+ 'iframe[title="reCAPTCHA"]',
124
+ 'iframe[src*="recaptcha/api2/anchor"]',
125
+ '.cf-turnstile,.h-captcha,.g-recaptcha',
126
+ ];
127
+ return sel.some(s => {
128
+ const el = document.querySelector(s);
129
+ if (!el) return false;
130
+ // Consider "gone" if element exists but has zero dimensions (hidden after solve)
131
+ const r = el.getBoundingClientRect();
132
+ return r.width > 0 && r.height > 0;
133
+ });
134
+ })()`;
135
+ // ---------------------------------------------------------------------------
136
+ // Human solver — primary strategy
137
+ // ---------------------------------------------------------------------------
138
+ async function humanSolveCaptcha(evaluate, sendCommand, currentMouse = { x: rngInt(200, 400), y: rngInt(200, 400) }, opts = {}) {
139
+ const widget = await evaluate(FIND_WIDGET_SCRIPT).catch(() => null);
140
+ if (!widget)
141
+ return { success: false, error: "No clickable captcha widget found in viewport." };
142
+ // Build the exact click target from widget geometry
143
+ const target = {
144
+ x: widget.left + widget.checkboxX,
145
+ y: widget.top + widget.checkboxY,
146
+ };
147
+ // 1. Natural mouse arc from current position → just outside the widget → checkbox
148
+ const approachX = target.x + rng(-40, 40);
149
+ const approachY = target.y + rng(-60, -30);
150
+ await humanMouseMove(currentMouse, { x: approachX, y: approachY }, sendCommand);
151
+ await sleep(rng(60, 150)); // micro-pause before committing to click
152
+ await humanMouseMove({ x: approachX, y: approachY }, target, sendCommand);
153
+ // 2. Click
154
+ await humanClick(target, sendCommand);
155
+ // 3. Wait for the widget to process
156
+ const waitMs = opts.waitAfterClick ?? 8_000;
157
+ const checkInterval = 800;
158
+ const deadline = Date.now() + waitMs;
159
+ while (Date.now() < deadline) {
160
+ await sleep(checkInterval);
161
+ const stillPresent = await evaluate(STILL_PRESENT_SCRIPT).catch(() => true);
162
+ if (!stillPresent) {
163
+ return { success: true, type: widget.type, method: "human" };
164
+ }
165
+ // Move mouse slightly while waiting (humans don't freeze)
166
+ await humanMouseMove(target, { x: target.x + rng(-15, 15), y: target.y + rng(-15, 15) }, sendCommand);
167
+ }
168
+ return {
169
+ success: false,
170
+ type: widget.type,
171
+ method: "human",
172
+ error: "Captcha widget still present after click — may need image challenge or service fallback.",
173
+ };
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Third-party service fallback (opt-in via useService: true)
177
+ // ---------------------------------------------------------------------------
178
+ const https_1 = __importDefault(require("https"));
179
+ function httpsPost(url, body, ct = "application/json") {
180
+ return new Promise((res, rej) => {
181
+ const u = new URL(url);
182
+ const req = https_1.default.request({ hostname: u.hostname, path: u.pathname + u.search, method: "POST",
183
+ headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(body) } }, (r) => { let d = ""; r.on("data", c => d += c); r.on("end", () => res(d)); });
184
+ req.on("error", rej);
185
+ req.write(body);
186
+ req.end();
187
+ });
188
+ }
189
+ function httpsGet(url) {
190
+ return new Promise((res, rej) => {
191
+ https_1.default.get(url, (r) => { let d = ""; r.on("data", c => d += c); r.on("end", () => res(d)); }).on("error", rej);
192
+ });
193
+ }
194
+ const EXTRACT_SCRIPT = `(function(){
195
+ const checks=[
196
+ ()=>{const e=document.querySelector('iframe[src*="challenges.cloudflare.com"],.cf-turnstile [data-sitekey],.cf-turnstile');if(e){const sk=e.getAttribute('data-sitekey')||(e.src&&new URLSearchParams(e.src.split('?')[1]||'').get('sitekey'));if(sk)return{type:'turnstile',sitekey:sk};}},
197
+ ()=>{const e=document.querySelector('iframe[src*="hcaptcha"],.h-captcha[data-sitekey]');if(e){const sk=e.getAttribute('data-sitekey')||(e.src&&new URLSearchParams(e.src.split('?')[1]||'').get('sitekey'));if(sk)return{type:'hcaptcha',sitekey:sk};}},
198
+ ()=>{const s=document.querySelector('script[src*="recaptcha/api.js"]');if(s){const r=new URLSearchParams((s.src||'').split('?')[1]||'').get('render');if(r&&r!=='explicit')return{type:'recaptcha_v3',sitekey:r};}},
199
+ ()=>{const e=document.querySelector('.g-recaptcha,[data-sitekey],iframe[src*="recaptcha"]');if(e){const sk=e.getAttribute('data-sitekey')||(e.src&&(e.src.match(/[?&]k=([^&]+)/)||[])[1]);if(sk)return{type:'recaptcha_v2',sitekey:sk};}},
200
+ ];
201
+ for(const c of checks){try{const r=c();if(r?.sitekey)return r;}catch(e){}}
202
+ return null;
203
+ })()`;
204
+ async function serviceSolve(info, opts) {
205
+ const svc = opts.service ?? process.env.CAPTCHA_SERVICE ?? "2captcha";
206
+ const apiKey = opts.apiKey ?? process.env.CAPTCHA_API_KEY ?? "";
207
+ const timeout = opts.timeout ?? 120_000;
208
+ const pollInt = opts.pollInterval ?? 5_000;
209
+ if (!apiKey)
210
+ throw new Error("CAPTCHA_API_KEY env var not set. Set it or use human mode (default).");
211
+ if (svc === "capsolver") {
212
+ const typeMap = {
213
+ recaptcha_v2: "ReCaptchaV2TaskProxyLess", recaptcha_v3: "ReCaptchaV3TaskProxyLess",
214
+ hcaptcha: "HCaptchaTaskProxyLess", turnstile: "AntiTurnstileTaskProxyLess",
215
+ };
216
+ const task = { type: typeMap[info.type] || "ReCaptchaV2TaskProxyLess", websiteURL: info.pageUrl, websiteKey: info.sitekey };
217
+ if (info.type === "recaptcha_v3")
218
+ task.pageAction = info.action || "verify";
219
+ const sub = JSON.parse(await httpsPost("https://api.capsolver.com/createTask", JSON.stringify({ clientKey: apiKey, task })));
220
+ if (sub.errorId)
221
+ throw new Error(`CapSolver: ${sub.errorDescription}`);
222
+ const tid = String(sub.taskId);
223
+ const dl = Date.now() + timeout;
224
+ while (Date.now() < dl) {
225
+ await sleep(pollInt);
226
+ const r = JSON.parse(await httpsPost("https://api.capsolver.com/getTaskResult", JSON.stringify({ clientKey: apiKey, taskId: tid })));
227
+ if (r.errorId)
228
+ throw new Error(`CapSolver: ${r.errorDescription}`);
229
+ if (r.status === "ready")
230
+ return String(r.solution?.gRecaptchaResponse || r.solution?.token || "");
231
+ }
232
+ }
233
+ else {
234
+ // 2captcha
235
+ const methodMap = { recaptcha_v2: "userrecaptcha", recaptcha_v3: "userrecaptcha", hcaptcha: "hcaptcha", turnstile: "turnstile" };
236
+ const params = { key: apiKey, pageurl: info.pageUrl, json: "1", method: methodMap[info.type] || "userrecaptcha" };
237
+ if (info.type === "recaptcha_v2" || info.type === "recaptcha_v3")
238
+ params.googlekey = info.sitekey;
239
+ else
240
+ params.sitekey = info.sitekey;
241
+ if (info.type === "recaptcha_v3") {
242
+ params.version = "v3";
243
+ params.action = info.action || "verify";
244
+ params.min_score = "0.3";
245
+ }
246
+ const sub = JSON.parse(await httpsPost("https://2captcha.com/in.php", new URLSearchParams(params).toString(), "application/x-www-form-urlencoded"));
247
+ if (sub.status !== 1)
248
+ throw new Error(`2captcha: ${sub.request}`);
249
+ const tid = String(sub.request);
250
+ const dl = Date.now() + timeout;
251
+ while (Date.now() < dl) {
252
+ await sleep(pollInt);
253
+ const r = JSON.parse(await httpsGet(`https://2captcha.com/res.php?key=${apiKey}&action=get&id=${tid}&json=1`));
254
+ if (r.status === 1)
255
+ return String(r.request);
256
+ if (r.request !== "CAPCHA_NOT_READY")
257
+ throw new Error(`2captcha: ${r.request}`);
258
+ }
259
+ }
260
+ throw new Error("Service timed out waiting for captcha solution.");
261
+ }
262
+ function buildInjectScript(type, token) {
263
+ const t = JSON.stringify(token);
264
+ if (type === "recaptcha_v2" || type === "recaptcha_v3")
265
+ return `(function(tk){
266
+ document.querySelectorAll('textarea[name="g-recaptcha-response"],#g-recaptcha-response').forEach(e=>{e.value=tk;e.innerHTML=tk;});
267
+ const cfg=window.___grecaptcha_cfg;
268
+ if(cfg?.clients)Object.values(cfg.clients).forEach(c=>{(function w(o,d){if(d>6||!o||typeof o!=='object')return;Object.values(o).forEach(v=>{if(typeof v==='function'){try{v(tk);}catch(e){}}else w(v,d+1);});})(c,0);});
269
+ })(${t})`;
270
+ if (type === "hcaptcha")
271
+ return `(function(tk){
272
+ document.querySelectorAll('[name="h-captcha-response"]').forEach(e=>{e.value=tk;});
273
+ document.querySelectorAll('.h-captcha,[data-hcaptcha-sitekey]').forEach(w=>{const cb=w.getAttribute('data-callback');if(cb&&typeof window[cb]==='function')try{window[cb](tk);}catch(e){}});
274
+ })(${t})`;
275
+ if (type === "turnstile")
276
+ return `(function(tk){
277
+ document.querySelectorAll('[name="cf-turnstile-response"]').forEach(e=>{e.value=tk;});
278
+ document.querySelectorAll('.cf-turnstile').forEach(w=>{const cb=w.getAttribute('data-callback');if(cb&&typeof window[cb]==='function')try{window[cb](tk);}catch(e){}});
279
+ })(${t})`;
280
+ return `(function(tk){['g-recaptcha-response','h-captcha-response','cf-turnstile-response'].forEach(n=>{document.querySelectorAll('[name="'+n+'"]').forEach(e=>{e.value=tk;});});})(${t})`;
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Main entry point used by cdp-bridge
284
+ // ---------------------------------------------------------------------------
285
+ async function detectAndSolve(evaluate, sendCommand, pageUrl, currentMouse, opts = {}) {
286
+ // Default: human simulation — no API key required
287
+ if (!opts.useService) {
288
+ return humanSolveCaptcha(evaluate, sendCommand, currentMouse, opts);
289
+ }
290
+ // Explicit service mode
291
+ const raw = await evaluate(EXTRACT_SCRIPT).catch(() => null);
292
+ if (!raw?.sitekey)
293
+ return { success: false, error: "No solvable captcha found for service mode." };
294
+ const info = { type: raw.type, sitekey: raw.sitekey, pageUrl, action: raw.action };
295
+ try {
296
+ const token = await serviceSolve(info, opts);
297
+ await evaluate(buildInjectScript(info.type, token)).catch(() => { });
298
+ return { success: true, type: info.type, method: "service", token };
299
+ }
300
+ catch (err) {
301
+ return { success: false, type: info.type, method: "service", error: err.message };
302
+ }
303
+ }