vektor-slipstream 1.4.4 → 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.
- package/README.md +67 -306
- package/package.json +14 -146
- package/CHANGELOG.md +0 -139
- package/LICENSE +0 -33
- package/TENETS.md +0 -189
- package/audn-log.js +0 -143
- package/axon.js +0 -389
- package/boot-patch.js +0 -33
- package/boot-screen.html +0 -210
- package/briefing.js +0 -150
- package/cerebellum.js +0 -439
- package/cloak-behaviour.js +0 -596
- package/cloak-captcha.js +0 -541
- package/cloak-core.js +0 -499
- package/cloak-identity.js +0 -484
- package/cloak-index.js +0 -261
- package/cloak-llms.js +0 -163
- package/cloak-pattern-store.js +0 -471
- package/cloak-recorder-auto.js +0 -297
- package/cloak-recorder-snippet.js +0 -119
- package/cloak-turbo-quant.js +0 -357
- package/cloak-warmup.js +0 -240
- package/cortex.js +0 -221
- package/detect-hardware.js +0 -181
- package/entity-resolver.js +0 -298
- package/errors.js +0 -66
- package/examples/example-claude-mcp.js +0 -220
- package/examples/example-langchain-researcher.js +0 -82
- package/examples/example-openai-assistant.js +0 -84
- package/examples/examples-README.md +0 -161
- package/export-import.js +0 -221
- package/forget.js +0 -148
- package/inspect.js +0 -199
- package/mistral/README-mistral.md +0 -123
- package/mistral/mistral-bridge.js +0 -218
- package/mistral/mistral-setup.js +0 -220
- package/mistral/vektor-tool-manifest.json +0 -41
- package/models/model_quantized.onnx +0 -0
- package/models/vocab.json +0 -1
- package/namespace.js +0 -186
- package/pin.js +0 -91
- package/slipstream-core-extended.js +0 -134
- package/slipstream-core.js +0 -1
- package/slipstream-db.js +0 -140
- package/slipstream-embedder.js +0 -338
- package/sovereign.js +0 -142
- package/token.js +0 -322
- package/types/index.d.ts +0 -269
- package/vektor-banner-loader.js +0 -109
- package/vektor-cli.js +0 -259
- package/vektor-licence-prompt.js +0 -128
- package/vektor-licence.js +0 -192
- package/vektor-setup.js +0 -270
- package/vektor-slipstream.dxt +0 -0
- package/vektor-tui.js +0 -373
- package/visualize.js +0 -235
package/cloak-captcha.js
DELETED
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* cloak-captcha.js
|
|
5
|
-
* Automated CAPTCHA detection and solving pipeline for Cloak.
|
|
6
|
-
*
|
|
7
|
-
* Supports:
|
|
8
|
-
* - hCaptcha → screenshot → vision model → token injection
|
|
9
|
-
* - reCAPTCHA v2 → 2captcha/CapSolver API fallback
|
|
10
|
-
* - reCAPTCHA v3 → session warmup strategy (score-based, no challenge)
|
|
11
|
-
* - Cloudflare Turnstile → fingerprint consistency pass
|
|
12
|
-
* - Audio CAPTCHA → Whisper transcription
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* const { detectCaptcha, solveCaptcha, injectCaptchaSolution } = require('./cloak-captcha');
|
|
16
|
-
* const detected = await detectCaptcha(page);
|
|
17
|
-
* if (detected) {
|
|
18
|
-
* const token = await solveCaptcha(page, detected, { visionApiKey, provider });
|
|
19
|
-
* await injectCaptchaSolution(page, detected, token);
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const https = require('https');
|
|
24
|
-
const fs = require('fs');
|
|
25
|
-
const path = require('path');
|
|
26
|
-
const os = require('os');
|
|
27
|
-
|
|
28
|
-
// ── Detection ─────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
const CAPTCHA_SIGNATURES = {
|
|
31
|
-
hcaptcha: [
|
|
32
|
-
'hcaptcha.com/1/api.js',
|
|
33
|
-
'data-hcaptcha-widget-id',
|
|
34
|
-
'h-captcha',
|
|
35
|
-
'hcaptcha-challenge',
|
|
36
|
-
],
|
|
37
|
-
recaptcha_v2: [
|
|
38
|
-
'google.com/recaptcha/api.js',
|
|
39
|
-
'g-recaptcha',
|
|
40
|
-
'data-sitekey',
|
|
41
|
-
'recaptcha-checkbox',
|
|
42
|
-
],
|
|
43
|
-
recaptcha_v3: [
|
|
44
|
-
'google.com/recaptcha/api.js?render=',
|
|
45
|
-
'grecaptcha.execute',
|
|
46
|
-
],
|
|
47
|
-
turnstile: [
|
|
48
|
-
'challenges.cloudflare.com',
|
|
49
|
-
'cf-turnstile',
|
|
50
|
-
'data-cf-turnstile',
|
|
51
|
-
],
|
|
52
|
-
audio: [
|
|
53
|
-
'audio-challenge',
|
|
54
|
-
'captcha-audio',
|
|
55
|
-
'rc-audiochallenge',
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Detect CAPTCHA type on a Playwright page.
|
|
61
|
-
* Returns { type, sitekey, element } or null.
|
|
62
|
-
*/
|
|
63
|
-
async function detectCaptcha(page) {
|
|
64
|
-
const html = await page.content().catch(() => '');
|
|
65
|
-
|
|
66
|
-
for (const [type, sigs] of Object.entries(CAPTCHA_SIGNATURES)) {
|
|
67
|
-
for (const sig of sigs) {
|
|
68
|
-
if (html.includes(sig)) {
|
|
69
|
-
// Extract sitekey if available
|
|
70
|
-
let sitekey = null;
|
|
71
|
-
const skMatch = html.match(/data-sitekey=["']([^"']+)["']/);
|
|
72
|
-
if (skMatch) sitekey = skMatch[1];
|
|
73
|
-
|
|
74
|
-
// Check if actually visible (not just in source)
|
|
75
|
-
const visible = await _isCaptchaVisible(page, type);
|
|
76
|
-
if (!visible && type !== 'recaptcha_v3') continue; // v3 is invisible by design
|
|
77
|
-
|
|
78
|
-
return { type, sitekey, visible };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function _isCaptchaVisible(page, type) {
|
|
86
|
-
const selectors = {
|
|
87
|
-
hcaptcha: 'iframe[src*="hcaptcha"]',
|
|
88
|
-
recaptcha_v2: 'iframe[src*="recaptcha"]',
|
|
89
|
-
recaptcha_v3: 'script[src*="recaptcha"]',
|
|
90
|
-
turnstile: 'iframe[src*="challenges.cloudflare"]',
|
|
91
|
-
audio: '.rc-audiochallenge',
|
|
92
|
-
};
|
|
93
|
-
const sel = selectors[type];
|
|
94
|
-
if (!sel) return false;
|
|
95
|
-
try {
|
|
96
|
-
const el = await page.$(sel);
|
|
97
|
-
return !!el;
|
|
98
|
-
} catch { return false; }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── Solving ───────────────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Solve a detected CAPTCHA. Returns token string or null.
|
|
105
|
-
*
|
|
106
|
-
* opts:
|
|
107
|
-
* provider: 'claude' | 'openai' | '2captcha' | 'capsolver' (default: 'claude')
|
|
108
|
-
* visionApiKey: API key for vision provider
|
|
109
|
-
* solverApiKey: API key for 2captcha/capsolver
|
|
110
|
-
* anthropicKey: Anthropic API key (for provider='claude')
|
|
111
|
-
*/
|
|
112
|
-
async function solveCaptcha(page, detected, opts = {}) {
|
|
113
|
-
const provider = opts.provider || 'claude';
|
|
114
|
-
|
|
115
|
-
switch (detected.type) {
|
|
116
|
-
case 'hcaptcha':
|
|
117
|
-
return _solveHcaptcha(page, { ...opts, provider });
|
|
118
|
-
|
|
119
|
-
case 'recaptcha_v2':
|
|
120
|
-
return _solveRecaptchaV2(page, detected, opts);
|
|
121
|
-
|
|
122
|
-
case 'recaptcha_v3':
|
|
123
|
-
// v3 is score-based — no token to solve, just warm the session
|
|
124
|
-
return _warmRecaptchaV3Session(page, opts);
|
|
125
|
-
|
|
126
|
-
case 'turnstile':
|
|
127
|
-
// Turnstile needs consistent fingerprint — return advisory
|
|
128
|
-
return { advisory: 'turnstile', message: 'Use persistent cloak_passport fingerprint for Turnstile.' };
|
|
129
|
-
|
|
130
|
-
case 'audio':
|
|
131
|
-
return _solveAudioCaptcha(page, opts);
|
|
132
|
-
|
|
133
|
-
default:
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── hCaptcha — screenshot + vision model ─────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
async function _solveHcaptcha(page, opts) {
|
|
141
|
-
// Switch to image challenge if audio is showing
|
|
142
|
-
try {
|
|
143
|
-
const audioBtn = await page.$('.h-captcha-audio-btn, [aria-label="Get an audio challenge"]');
|
|
144
|
-
if (audioBtn) {
|
|
145
|
-
const visualBtn = await page.$('[aria-label="Get a visual challenge"]');
|
|
146
|
-
if (visualBtn) await visualBtn.click();
|
|
147
|
-
await page.waitForTimeout(800);
|
|
148
|
-
}
|
|
149
|
-
} catch { /* ignore */ }
|
|
150
|
-
|
|
151
|
-
// Find the hCaptcha iframe
|
|
152
|
-
const frame = await _getHcaptchaFrame(page);
|
|
153
|
-
if (!frame) return null;
|
|
154
|
-
|
|
155
|
-
// Get challenge prompt text
|
|
156
|
-
let prompt = 'unknown challenge';
|
|
157
|
-
try {
|
|
158
|
-
const promptEl = await frame.$('.prompt-text, [class*="prompt"]');
|
|
159
|
-
if (promptEl) prompt = await promptEl.innerText();
|
|
160
|
-
} catch { /* ignore */ }
|
|
161
|
-
|
|
162
|
-
// Screenshot the challenge grid
|
|
163
|
-
const screenshotPath = path.join(os.tmpdir(), `hcaptcha-${Date.now()}.png`);
|
|
164
|
-
try {
|
|
165
|
-
const challengeEl = await frame.$('.task-grid, .challenge-container, .display-language');
|
|
166
|
-
if (challengeEl) {
|
|
167
|
-
await challengeEl.screenshot({ path: screenshotPath });
|
|
168
|
-
} else {
|
|
169
|
-
// Full frame screenshot
|
|
170
|
-
await frame.locator('body').screenshot({ path: screenshotPath });
|
|
171
|
-
}
|
|
172
|
-
} catch (e) {
|
|
173
|
-
// Full page screenshot fallback
|
|
174
|
-
await page.screenshot({ path: screenshotPath });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Send to vision model
|
|
178
|
-
const imageData = fs.readFileSync(screenshotPath).toString('base64');
|
|
179
|
-
let selections = null;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
if (opts.provider === 'claude' || !opts.provider) {
|
|
183
|
-
selections = await _visionSolveWithClaude(imageData, prompt, opts.anthropicKey);
|
|
184
|
-
} else if (opts.provider === 'openai') {
|
|
185
|
-
selections = await _visionSolveWithOpenAI(imageData, prompt, opts.visionApiKey);
|
|
186
|
-
}
|
|
187
|
-
} finally {
|
|
188
|
-
fs.unlink(screenshotPath, () => {});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (!selections) return null;
|
|
192
|
-
|
|
193
|
-
// Click the correct tiles
|
|
194
|
-
await _clickHcaptchaTiles(frame, selections);
|
|
195
|
-
|
|
196
|
-
// Click verify
|
|
197
|
-
try {
|
|
198
|
-
const verifyBtn = await frame.$('#checkbox, .button-submit, [data-cy="submit"]');
|
|
199
|
-
if (verifyBtn) {
|
|
200
|
-
await verifyBtn.click();
|
|
201
|
-
await page.waitForTimeout(1500);
|
|
202
|
-
}
|
|
203
|
-
} catch { /* ignore */ }
|
|
204
|
-
|
|
205
|
-
// Extract response token
|
|
206
|
-
return _extractHcaptchaToken(page);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function _visionSolveWithClaude(imageData, prompt, apiKey) {
|
|
210
|
-
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
211
|
-
if (!key) throw new Error('No Anthropic API key for CAPTCHA vision solver');
|
|
212
|
-
|
|
213
|
-
const body = JSON.stringify({
|
|
214
|
-
model: 'claude-haiku-4-5-20251001',
|
|
215
|
-
max_tokens: 256,
|
|
216
|
-
messages: [{
|
|
217
|
-
role: 'user',
|
|
218
|
-
content: [
|
|
219
|
-
{
|
|
220
|
-
type: 'image',
|
|
221
|
-
source: { type: 'base64', media_type: 'image/png', data: imageData },
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
type: 'text',
|
|
225
|
-
text: `This is an hCaptcha image challenge. The task is: "${prompt}".
|
|
226
|
-
The grid shows numbered tiles (1-9 in a 3x3 grid, left to right, top to bottom).
|
|
227
|
-
Reply ONLY with a JSON array of tile numbers that match the task, e.g.: [1,4,7]
|
|
228
|
-
If no tiles match, reply: []`,
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
}],
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const response = await _httpsPost('api.anthropic.com', '/v1/messages', body, {
|
|
235
|
-
'x-api-key': key,
|
|
236
|
-
'anthropic-version': '2023-06-01',
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const text = response?.content?.[0]?.text || '[]';
|
|
240
|
-
const match = text.match(/\[[\d,\s]*\]/);
|
|
241
|
-
return match ? JSON.parse(match[0]) : [];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function _visionSolveWithOpenAI(imageData, prompt, apiKey) {
|
|
245
|
-
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
246
|
-
if (!key) throw new Error('No OpenAI API key for CAPTCHA vision solver');
|
|
247
|
-
|
|
248
|
-
const body = JSON.stringify({
|
|
249
|
-
model: 'gpt-4o',
|
|
250
|
-
max_tokens: 100,
|
|
251
|
-
messages: [{
|
|
252
|
-
role: 'user',
|
|
253
|
-
content: [
|
|
254
|
-
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageData}` } },
|
|
255
|
-
{ type: 'text', text: `hCaptcha task: "${prompt}". Reply ONLY with JSON array of matching tile numbers 1-9. e.g. [2,5]` },
|
|
256
|
-
],
|
|
257
|
-
}],
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const response = await _httpsPost('api.openai.com', '/v1/chat/completions', body, {
|
|
261
|
-
'Authorization': `Bearer ${key}`,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const text = response?.choices?.[0]?.message?.content || '[]';
|
|
265
|
-
const match = text.match(/\[[\d,\s]*\]/);
|
|
266
|
-
return match ? JSON.parse(match[0]) : [];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function _getHcaptchaFrame(page) {
|
|
270
|
-
for (const frame of page.frames()) {
|
|
271
|
-
const url = frame.url();
|
|
272
|
-
if (url.includes('hcaptcha.com') && url.includes('challenge')) return frame;
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function _clickHcaptchaTiles(frame, tileNumbers) {
|
|
278
|
-
if (!tileNumbers || !tileNumbers.length) return;
|
|
279
|
-
const tiles = await frame.$$('.task-image, .challenge-image, [class*="image"]');
|
|
280
|
-
for (const n of tileNumbers) {
|
|
281
|
-
const tile = tiles[n - 1];
|
|
282
|
-
if (tile) {
|
|
283
|
-
await tile.click();
|
|
284
|
-
await frame.waitForTimeout(200 + Math.random() * 300); // human-like delay
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function _extractHcaptchaToken(page) {
|
|
290
|
-
try {
|
|
291
|
-
const token = await page.evaluate(() => {
|
|
292
|
-
const el = document.querySelector('[name="h-captcha-response"], textarea[id*="captcha"]');
|
|
293
|
-
return el ? el.value : null;
|
|
294
|
-
});
|
|
295
|
-
return token || null;
|
|
296
|
-
} catch { return null; }
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ── reCAPTCHA v2 — 2captcha/CapSolver API ────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
async function _solveRecaptchaV2(page, detected, opts) {
|
|
302
|
-
if (!opts.solverApiKey) {
|
|
303
|
-
return { advisory: 'recaptcha_v2', message: 'Provide solverApiKey (2captcha/capsolver) for reCAPTCHA v2.' };
|
|
304
|
-
}
|
|
305
|
-
// Submit to 2captcha
|
|
306
|
-
const submitBody = new URLSearchParams({
|
|
307
|
-
key: opts.solverApiKey,
|
|
308
|
-
method: 'userrecaptcha',
|
|
309
|
-
googlekey: detected.sitekey || '',
|
|
310
|
-
pageurl: page.url(),
|
|
311
|
-
json: '1',
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const submitted = await _httpsPost('2captcha.com', '/in.php', submitBody.toString(), {
|
|
315
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (!submitted?.request) return null;
|
|
319
|
-
const taskId = submitted.request;
|
|
320
|
-
|
|
321
|
-
// Poll for result (max 120s)
|
|
322
|
-
for (let i = 0; i < 24; i++) {
|
|
323
|
-
await _sleep(5000);
|
|
324
|
-
const result = await _httpsGet(`https://2captcha.com/res.php?key=${opts.solverApiKey}&action=get&id=${taskId}&json=1`);
|
|
325
|
-
if (result?.status === 1) return result.request;
|
|
326
|
-
if (result?.request === 'ERROR_CAPTCHA_UNSOLVABLE') return null;
|
|
327
|
-
}
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ── reCAPTCHA v3 — Session warmup ────────────────────────────────────────────
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Warm up a reCAPTCHA v3 session by simulating human-like browsing.
|
|
335
|
-
* Visit the homepage, scroll, hover, then navigate to target.
|
|
336
|
-
* This builds a 0.7+ trust score before hitting the protected page.
|
|
337
|
-
*/
|
|
338
|
-
async function _warmRecaptchaV3Session(page, opts) {
|
|
339
|
-
const currentUrl = page.url();
|
|
340
|
-
let origin;
|
|
341
|
-
try { origin = new URL(currentUrl).origin; } catch { return null; }
|
|
342
|
-
|
|
343
|
-
// Simulate human browsing — visit homepage first
|
|
344
|
-
try {
|
|
345
|
-
await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
346
|
-
await _humanScroll(page);
|
|
347
|
-
await _humanMouseMovement(page);
|
|
348
|
-
await _sleep(1000 + Math.random() * 2000);
|
|
349
|
-
|
|
350
|
-
// Visit a secondary page if possible (builds session history)
|
|
351
|
-
const links = await page.$$eval('a[href]', els =>
|
|
352
|
-
els
|
|
353
|
-
.map(el => el.href)
|
|
354
|
-
.filter(h => h.startsWith(window.location.origin) && !h.includes('#'))
|
|
355
|
-
.slice(0, 5)
|
|
356
|
-
);
|
|
357
|
-
if (links.length > 1) {
|
|
358
|
-
await page.goto(links[1], { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
359
|
-
await _humanScroll(page);
|
|
360
|
-
await _sleep(800 + Math.random() * 1200);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Return to original URL
|
|
364
|
-
await page.goto(currentUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
365
|
-
return { warmed: true, message: 'reCAPTCHA v3 session warmed — score should be 0.7+' };
|
|
366
|
-
} catch (e) {
|
|
367
|
-
return { warmed: false, message: e.message };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ── Audio CAPTCHA — Whisper ───────────────────────────────────────────────────
|
|
372
|
-
|
|
373
|
-
async function _solveAudioCaptcha(page, opts) {
|
|
374
|
-
const openaiKey = opts.visionApiKey || process.env.OPENAI_API_KEY;
|
|
375
|
-
if (!openaiKey) {
|
|
376
|
-
return { advisory: 'audio_captcha', message: 'Provide visionApiKey (OpenAI) for audio CAPTCHA solving.' };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Find audio challenge element and get audio URL
|
|
380
|
-
let audioUrl = null;
|
|
381
|
-
try {
|
|
382
|
-
audioUrl = await page.evaluate(() => {
|
|
383
|
-
const el = document.querySelector('.rc-audiochallenge-tdownload-link, audio source, [data-audio-src]');
|
|
384
|
-
return el ? (el.href || el.src || el.dataset.audioSrc) : null;
|
|
385
|
-
});
|
|
386
|
-
} catch { /* ignore */ }
|
|
387
|
-
|
|
388
|
-
if (!audioUrl) return null;
|
|
389
|
-
|
|
390
|
-
// Download audio file
|
|
391
|
-
const audioPath = path.join(os.tmpdir(), `captcha-audio-${Date.now()}.mp3`);
|
|
392
|
-
await _downloadFile(audioUrl, audioPath);
|
|
393
|
-
|
|
394
|
-
// Transcribe with Whisper
|
|
395
|
-
try {
|
|
396
|
-
const FormData = require('form-data');
|
|
397
|
-
const form = new FormData();
|
|
398
|
-
form.append('file', fs.createReadStream(audioPath));
|
|
399
|
-
form.append('model', 'whisper-1');
|
|
400
|
-
form.append('language', 'en');
|
|
401
|
-
|
|
402
|
-
const result = await _httpsPostForm('api.openai.com', '/v1/audio/transcriptions', form, {
|
|
403
|
-
'Authorization': `Bearer ${openaiKey}`,
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const text = result?.text?.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
|
|
407
|
-
return text || null;
|
|
408
|
-
} finally {
|
|
409
|
-
fs.unlink(audioPath, () => {});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ── Token injection ───────────────────────────────────────────────────────────
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Inject a solved CAPTCHA token back into the page.
|
|
417
|
-
*/
|
|
418
|
-
async function injectCaptchaSolution(page, detected, token) {
|
|
419
|
-
if (!token || typeof token !== 'string') return false;
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
await page.evaluate(({ type, token }) => {
|
|
423
|
-
if (type === 'hcaptcha') {
|
|
424
|
-
const el = document.querySelector('[name="h-captcha-response"]');
|
|
425
|
-
if (el) { el.value = token; el.dispatchEvent(new Event('change')); }
|
|
426
|
-
} else if (type === 'recaptcha_v2') {
|
|
427
|
-
const el = document.querySelector('[name="g-recaptcha-response"]');
|
|
428
|
-
if (el) { el.value = token; el.dispatchEvent(new Event('change')); }
|
|
429
|
-
// Also try the callback
|
|
430
|
-
if (window.___grecaptcha_cfg) {
|
|
431
|
-
const clients = window.___grecaptcha_cfg.clients;
|
|
432
|
-
for (const k in clients) {
|
|
433
|
-
const callback = clients[k]?.['']?.callback;
|
|
434
|
-
if (typeof callback === 'function') { callback(token); break; }
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}, { type: detected.type, token });
|
|
439
|
-
return true;
|
|
440
|
-
} catch { return false; }
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ── Human behaviour simulation helpers ───────────────────────────────────────
|
|
444
|
-
|
|
445
|
-
async function _humanScroll(page) {
|
|
446
|
-
await page.evaluate(async () => {
|
|
447
|
-
const total = document.body.scrollHeight;
|
|
448
|
-
let pos = 0;
|
|
449
|
-
while (pos < total * 0.6) {
|
|
450
|
-
const step = 80 + Math.random() * 120;
|
|
451
|
-
pos = Math.min(pos + step, total);
|
|
452
|
-
window.scrollTo({ top: pos, behavior: 'smooth' });
|
|
453
|
-
await new Promise(r => setTimeout(r, 80 + Math.random() * 120));
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function _humanMouseMovement(page) {
|
|
459
|
-
const vp = page.viewportSize() || { width: 1280, height: 720 };
|
|
460
|
-
for (let i = 0; i < 5; i++) {
|
|
461
|
-
const x = 100 + Math.random() * (vp.width - 200);
|
|
462
|
-
const y = 100 + Math.random() * (vp.height - 200);
|
|
463
|
-
await page.mouse.move(x, y, { steps: 5 + Math.floor(Math.random() * 10) });
|
|
464
|
-
await _sleep(200 + Math.random() * 400);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
469
|
-
|
|
470
|
-
function _httpsPost(host, path, body, extraHeaders = {}) {
|
|
471
|
-
return new Promise((resolve, reject) => {
|
|
472
|
-
const isString = typeof body === 'string';
|
|
473
|
-
const buf = Buffer.from(body);
|
|
474
|
-
const opts = {
|
|
475
|
-
hostname: host,
|
|
476
|
-
path,
|
|
477
|
-
method: 'POST',
|
|
478
|
-
headers: {
|
|
479
|
-
'Content-Type': isString && body.startsWith('{') ? 'application/json' : 'application/x-www-form-urlencoded',
|
|
480
|
-
'Content-Length': buf.length,
|
|
481
|
-
...extraHeaders,
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
const req = https.request(opts, res => {
|
|
485
|
-
let data = '';
|
|
486
|
-
res.setEncoding('utf8');
|
|
487
|
-
res.on('data', c => data += c);
|
|
488
|
-
res.on('end', () => {
|
|
489
|
-
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
req.on('error', reject);
|
|
493
|
-
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
494
|
-
req.write(buf);
|
|
495
|
-
req.end();
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function _httpsGet(url) {
|
|
500
|
-
return new Promise((resolve, reject) => {
|
|
501
|
-
https.get(url, res => {
|
|
502
|
-
let data = '';
|
|
503
|
-
res.setEncoding('utf8');
|
|
504
|
-
res.on('data', c => data += c);
|
|
505
|
-
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(data); } });
|
|
506
|
-
}).on('error', reject);
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function _httpsPostForm(host, path, form, extraHeaders = {}) {
|
|
511
|
-
return new Promise((resolve, reject) => {
|
|
512
|
-
const opts = {
|
|
513
|
-
hostname: host,
|
|
514
|
-
path,
|
|
515
|
-
method: 'POST',
|
|
516
|
-
headers: { ...form.getHeaders(), ...extraHeaders },
|
|
517
|
-
};
|
|
518
|
-
const req = https.request(opts, res => {
|
|
519
|
-
let data = '';
|
|
520
|
-
res.setEncoding('utf8');
|
|
521
|
-
res.on('data', c => data += c);
|
|
522
|
-
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(data); } });
|
|
523
|
-
});
|
|
524
|
-
req.on('error', reject);
|
|
525
|
-
form.pipe(req);
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function _downloadFile(url, dest) {
|
|
530
|
-
return new Promise((resolve, reject) => {
|
|
531
|
-
const file = fs.createWriteStream(dest);
|
|
532
|
-
https.get(url, res => {
|
|
533
|
-
res.pipe(file);
|
|
534
|
-
file.on('finish', () => file.close(resolve));
|
|
535
|
-
}).on('error', e => { fs.unlink(dest, () => {}); reject(e); });
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function _sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
540
|
-
|
|
541
|
-
module.exports = { detectCaptcha, solveCaptcha, injectCaptchaSolution };
|