@aluvia/sdk 1.4.1 → 2.0.1
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/CHANGELOG.md +194 -0
- package/README.md +162 -477
- package/dist/cjs/api/apiUtils.js +4 -1
- package/dist/cjs/client/AluviaClient.js +30 -32
- package/dist/cjs/client/BlockDetection.js +69 -87
- package/dist/cjs/client/rules.js +12 -2
- package/dist/cjs/connect.js +2 -2
- package/dist/cjs/index.js +12 -1
- package/dist/cjs/session/lock.js +40 -4
- package/dist/esm/api/apiUtils.js +4 -1
- package/dist/esm/client/AluviaClient.js +38 -40
- package/dist/esm/client/BlockDetection.js +69 -87
- package/dist/esm/client/rules.js +12 -2
- package/dist/esm/connect.js +2 -2
- package/dist/esm/index.js +6 -4
- package/dist/esm/session/lock.js +40 -4
- package/dist/types/client/AluviaClient.d.ts +2 -2
- package/dist/types/client/BlockDetection.d.ts +4 -4
- package/dist/types/client/types.d.ts +11 -11
- package/dist/types/index.d.ts +9 -7
- package/package.json +15 -23
- package/dist/cjs/bin/account.js +0 -31
- package/dist/cjs/bin/api-helpers.js +0 -58
- package/dist/cjs/bin/cli-adapter.js +0 -16
- package/dist/cjs/bin/cli.js +0 -245
- package/dist/cjs/bin/close.js +0 -120
- package/dist/cjs/bin/geos.js +0 -10
- package/dist/cjs/bin/mcp-helpers.js +0 -57
- package/dist/cjs/bin/open.js +0 -317
- package/dist/cjs/bin/session.js +0 -259
- package/dist/esm/bin/account.js +0 -28
- package/dist/esm/bin/api-helpers.js +0 -53
- package/dist/esm/bin/cli-adapter.js +0 -8
- package/dist/esm/bin/cli.js +0 -242
- package/dist/esm/bin/close.js +0 -117
- package/dist/esm/bin/geos.js +0 -7
- package/dist/esm/bin/mcp-helpers.js +0 -51
- package/dist/esm/bin/open.js +0 -280
- package/dist/esm/bin/session.js +0 -252
- package/dist/types/bin/account.d.ts +0 -1
- package/dist/types/bin/api-helpers.d.ts +0 -20
- package/dist/types/bin/cli-adapter.d.ts +0 -8
- package/dist/types/bin/cli.d.ts +0 -2
- package/dist/types/bin/close.d.ts +0 -1
- package/dist/types/bin/geos.d.ts +0 -1
- package/dist/types/bin/mcp-helpers.d.ts +0 -28
- package/dist/types/bin/open.d.ts +0 -21
- package/dist/types/bin/session.d.ts +0 -11
package/dist/cjs/api/apiUtils.js
CHANGED
|
@@ -23,7 +23,10 @@ function asErrorEnvelope(value) {
|
|
|
23
23
|
const message = error['message'];
|
|
24
24
|
if (typeof code !== 'string' || typeof message !== 'string')
|
|
25
25
|
return null;
|
|
26
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: { code, message, details: error['details'] },
|
|
29
|
+
};
|
|
27
30
|
}
|
|
28
31
|
function formatErrorDetails(details) {
|
|
29
32
|
if (details == null)
|
|
@@ -61,9 +61,9 @@ class AluviaClient {
|
|
|
61
61
|
this.pageStates = new WeakMap();
|
|
62
62
|
/** Promise-based mutex to serialize handleDetectionResult's critical section. */
|
|
63
63
|
this._detectionMutex = Promise.resolve();
|
|
64
|
-
const apiKey = String(options.apiKey ??
|
|
64
|
+
const apiKey = String(options.apiKey ?? '').trim();
|
|
65
65
|
if (!apiKey) {
|
|
66
|
-
throw new errors_js_1.MissingApiKeyError(
|
|
66
|
+
throw new errors_js_1.MissingApiKeyError('Aluvia apiKey is required');
|
|
67
67
|
}
|
|
68
68
|
const strict = options.strict ?? true;
|
|
69
69
|
this.options = { ...options, apiKey, strict };
|
|
@@ -71,12 +71,12 @@ class AluviaClient {
|
|
|
71
71
|
if (connectionId !== undefined && !Number.isFinite(connectionId)) {
|
|
72
72
|
throw new Error('connectionId must be a finite number');
|
|
73
73
|
}
|
|
74
|
-
const apiBaseUrl = options.apiBaseUrl ??
|
|
74
|
+
const apiBaseUrl = options.apiBaseUrl ?? 'https://api.aluvia.io/v1';
|
|
75
75
|
const pollIntervalMs = Math.max(options.pollIntervalMs ?? 5000, 1000);
|
|
76
76
|
const timeoutMs = options.timeoutMs;
|
|
77
|
-
const gatewayProtocol = options.gatewayProtocol ??
|
|
78
|
-
const gatewayPort = options.gatewayPort ?? (gatewayProtocol ===
|
|
79
|
-
const logLevel = options.logLevel ??
|
|
77
|
+
const gatewayProtocol = options.gatewayProtocol ?? 'http';
|
|
78
|
+
const gatewayPort = options.gatewayPort ?? (gatewayProtocol === 'https' ? 8443 : 8080);
|
|
79
|
+
const logLevel = options.logLevel ?? 'info';
|
|
80
80
|
this.logger = new logger_js_1.Logger(logLevel);
|
|
81
81
|
// Create ConfigManager
|
|
82
82
|
this.configManager = new ConfigManager_js_1.ConfigManager({
|
|
@@ -98,7 +98,7 @@ class AluviaClient {
|
|
|
98
98
|
});
|
|
99
99
|
// Initialize block detection if configured
|
|
100
100
|
if (options.blockDetection !== undefined || options.startPlaywright) {
|
|
101
|
-
this.logger.debug(
|
|
101
|
+
this.logger.debug('Initializing block detection');
|
|
102
102
|
const detectionConfig = options.blockDetection ?? { enabled: true };
|
|
103
103
|
this.blockDetection = new BlockDetection_js_1.BlockDetection(detectionConfig, this.logger);
|
|
104
104
|
}
|
|
@@ -115,10 +115,9 @@ class AluviaClient {
|
|
|
115
115
|
};
|
|
116
116
|
this.pageStates.set(page, pageState);
|
|
117
117
|
// Capture navigation responses on main frame
|
|
118
|
-
page.on(
|
|
118
|
+
page.on('response', (response) => {
|
|
119
119
|
try {
|
|
120
|
-
if (response.request().isNavigationRequest() &&
|
|
121
|
-
response.request().frame() === page.mainFrame()) {
|
|
120
|
+
if (response.request().isNavigationRequest() && response.request().frame() === page.mainFrame()) {
|
|
122
121
|
pageState.lastResponse = response;
|
|
123
122
|
pageState.skipFullPass = false;
|
|
124
123
|
pageState.fastResult = null;
|
|
@@ -129,7 +128,7 @@ class AluviaClient {
|
|
|
129
128
|
}
|
|
130
129
|
});
|
|
131
130
|
// Fast pass at domcontentloaded
|
|
132
|
-
page.on(
|
|
131
|
+
page.on('domcontentloaded', async () => {
|
|
133
132
|
if (!this.blockDetection)
|
|
134
133
|
return;
|
|
135
134
|
try {
|
|
@@ -146,13 +145,13 @@ class AluviaClient {
|
|
|
146
145
|
}
|
|
147
146
|
});
|
|
148
147
|
// Full pass at load
|
|
149
|
-
page.on(
|
|
148
|
+
page.on('load', async () => {
|
|
150
149
|
if (!this.blockDetection || pageState.skipFullPass)
|
|
151
150
|
return;
|
|
152
151
|
try {
|
|
153
152
|
// Wait for networkidle with timeout cap
|
|
154
153
|
try {
|
|
155
|
-
await page.waitForLoadState(
|
|
154
|
+
await page.waitForLoadState('networkidle', {
|
|
156
155
|
timeout: this.blockDetection.getNetworkIdleTimeoutMs(),
|
|
157
156
|
});
|
|
158
157
|
}
|
|
@@ -168,7 +167,7 @@ class AluviaClient {
|
|
|
168
167
|
}
|
|
169
168
|
});
|
|
170
169
|
// SPA detection via framenavigated
|
|
171
|
-
page.on(
|
|
170
|
+
page.on('framenavigated', async (frame) => {
|
|
172
171
|
if (!this.blockDetection)
|
|
173
172
|
return;
|
|
174
173
|
try {
|
|
@@ -197,14 +196,14 @@ class AluviaClient {
|
|
|
197
196
|
* Attaches page listeners to all existing and future pages in a context.
|
|
198
197
|
*/
|
|
199
198
|
attachBlockDetectionListener(context) {
|
|
200
|
-
this.logger.debug(
|
|
199
|
+
this.logger.debug('Attaching block detection listener to context');
|
|
201
200
|
// Attach to existing pages
|
|
202
201
|
try {
|
|
203
202
|
const existingPages = context.pages();
|
|
204
203
|
for (const page of existingPages) {
|
|
205
204
|
this.attachPageListeners(page);
|
|
206
205
|
// Check if page has already loaded (not about:blank)
|
|
207
|
-
if (page.url() !==
|
|
206
|
+
if (page.url() !== 'about:blank' && this.blockDetection) {
|
|
208
207
|
this.blockDetection
|
|
209
208
|
.analyzeFull(page, null)
|
|
210
209
|
.then((result) => {
|
|
@@ -220,7 +219,7 @@ class AluviaClient {
|
|
|
220
219
|
// Ignore errors
|
|
221
220
|
}
|
|
222
221
|
// Attach to future pages
|
|
223
|
-
context.on(
|
|
222
|
+
context.on('page', (page) => {
|
|
224
223
|
this.logger.debug(`New page detected: ${page.url()}`);
|
|
225
224
|
this.attachPageListeners(page);
|
|
226
225
|
});
|
|
@@ -254,15 +253,16 @@ class AluviaClient {
|
|
|
254
253
|
if (!this.blockDetection.isAutoUnblock())
|
|
255
254
|
return;
|
|
256
255
|
// Check if auto-reload should fire for this blockStatus
|
|
257
|
-
const shouldReload = result.blockStatus ===
|
|
258
|
-
(result.blockStatus ===
|
|
259
|
-
this.blockDetection.isAutoUnblockOnSuspected());
|
|
256
|
+
const shouldReload = result.blockStatus === 'blocked' ||
|
|
257
|
+
(result.blockStatus === 'suspected' && this.blockDetection.isAutoUnblockOnSuspected());
|
|
260
258
|
if (!shouldReload)
|
|
261
259
|
return;
|
|
262
260
|
// Serialize the critical section: persistent-block state, rule updates, reload.
|
|
263
261
|
// This prevents concurrent handlers from reading stale retriedUrls/persistentHostnames.
|
|
264
262
|
let release;
|
|
265
|
-
const gate = new Promise((resolve) => {
|
|
263
|
+
const gate = new Promise((resolve) => {
|
|
264
|
+
release = resolve;
|
|
265
|
+
});
|
|
266
266
|
const acquired = this._detectionMutex;
|
|
267
267
|
this._detectionMutex = this._detectionMutex.then(() => gate);
|
|
268
268
|
await acquired;
|
|
@@ -338,9 +338,7 @@ class AluviaClient {
|
|
|
338
338
|
this.startPromise = (async () => {
|
|
339
339
|
// Fetch initial configuration (may throw InvalidApiKeyError or ApiError)
|
|
340
340
|
await this.configManager.init();
|
|
341
|
-
const browserInstance = this.options.startPlaywright
|
|
342
|
-
? await this._initPlaywright()
|
|
343
|
-
: undefined;
|
|
341
|
+
const browserInstance = this.options.startPlaywright ? await this._initPlaywright() : undefined;
|
|
344
342
|
// Keep config fresh so routing decisions update without restarting.
|
|
345
343
|
this.configManager.startPolling();
|
|
346
344
|
try {
|
|
@@ -365,10 +363,10 @@ class AluviaClient {
|
|
|
365
363
|
if (!d)
|
|
366
364
|
return;
|
|
367
365
|
try {
|
|
368
|
-
if (typeof d.close ===
|
|
366
|
+
if (typeof d.close === 'function') {
|
|
369
367
|
await d.close();
|
|
370
368
|
}
|
|
371
|
-
else if (typeof d.destroy ===
|
|
369
|
+
else if (typeof d.destroy === 'function') {
|
|
372
370
|
d.destroy();
|
|
373
371
|
}
|
|
374
372
|
}
|
|
@@ -546,20 +544,20 @@ class AluviaClient {
|
|
|
546
544
|
*/
|
|
547
545
|
async _initPlaywright() {
|
|
548
546
|
try {
|
|
549
|
-
const pw = await Promise.resolve().then(() => __importStar(require(
|
|
547
|
+
const pw = await Promise.resolve().then(() => __importStar(require('playwright')));
|
|
550
548
|
// @ts-ignore
|
|
551
549
|
return pw.chromium;
|
|
552
550
|
}
|
|
553
551
|
catch {
|
|
554
552
|
// Playwright not installed — attempt auto-install
|
|
555
|
-
this.logger.info(
|
|
556
|
-
const { execSync } = await Promise.resolve().then(() => __importStar(require(
|
|
553
|
+
this.logger.info('Playwright not found. Installing playwright...');
|
|
554
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
557
555
|
try {
|
|
558
|
-
execSync(
|
|
559
|
-
stdio:
|
|
556
|
+
execSync('npm install playwright', {
|
|
557
|
+
stdio: 'inherit',
|
|
560
558
|
cwd: process.cwd(),
|
|
561
559
|
});
|
|
562
|
-
const pw = await Promise.resolve().then(() => __importStar(require(
|
|
560
|
+
const pw = await Promise.resolve().then(() => __importStar(require('playwright')));
|
|
563
561
|
// @ts-ignore
|
|
564
562
|
return pw.chromium;
|
|
565
563
|
}
|
|
@@ -3,47 +3,36 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.BlockDetection = void 0;
|
|
5
5
|
const DEFAULT_CHALLENGE_SELECTORS = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
'#challenge-form',
|
|
7
|
+
'#challenge-running',
|
|
8
|
+
'.cf-browser-verification',
|
|
9
9
|
'iframe[src*="recaptcha"]',
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
'.g-recaptcha',
|
|
11
|
+
'#px-captcha',
|
|
12
12
|
'iframe[src*="hcaptcha"]',
|
|
13
|
-
|
|
13
|
+
'.h-captcha',
|
|
14
14
|
];
|
|
15
15
|
const TITLE_KEYWORDS = [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
];
|
|
23
|
-
const STRONG_TEXT_KEYWORDS = [
|
|
24
|
-
"captcha",
|
|
25
|
-
"access denied",
|
|
26
|
-
"verify you are human",
|
|
27
|
-
"bot detection",
|
|
28
|
-
];
|
|
29
|
-
const WEAK_TEXT_KEYWORDS = [
|
|
30
|
-
"blocked",
|
|
31
|
-
"forbidden",
|
|
32
|
-
"cloudflare",
|
|
33
|
-
"please verify",
|
|
34
|
-
"unusual activity",
|
|
16
|
+
'access denied',
|
|
17
|
+
'blocked',
|
|
18
|
+
'forbidden',
|
|
19
|
+
'security check',
|
|
20
|
+
'attention required',
|
|
21
|
+
'just a moment',
|
|
35
22
|
];
|
|
23
|
+
const STRONG_TEXT_KEYWORDS = ['captcha', 'access denied', 'verify you are human', 'bot detection'];
|
|
24
|
+
const WEAK_TEXT_KEYWORDS = ['blocked', 'forbidden', 'cloudflare', 'please verify', 'unusual activity'];
|
|
36
25
|
function escapeRegex(str) {
|
|
37
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g,
|
|
26
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
27
|
}
|
|
39
28
|
const WEAK_TEXT_REGEXES = WEAK_TEXT_KEYWORDS.map((keyword) => ({
|
|
40
29
|
keyword,
|
|
41
|
-
regex: new RegExp(
|
|
30
|
+
regex: new RegExp('\\b' + escapeRegex(keyword) + '\\b', 'i'),
|
|
42
31
|
}));
|
|
43
32
|
const CHALLENGE_DOMAIN_PATTERNS = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
'/cdn-cgi/challenge-platform/',
|
|
34
|
+
'challenges.cloudflare.com',
|
|
35
|
+
'geo.captcha-delivery.com',
|
|
47
36
|
];
|
|
48
37
|
/**
|
|
49
38
|
* BlockDetection handles detection of website blocks, CAPTCHAs, and WAF challenges
|
|
@@ -87,9 +76,9 @@ class BlockDetection {
|
|
|
87
76
|
// --- Scoring Engine ---
|
|
88
77
|
computeScore(signals) {
|
|
89
78
|
if (signals.length === 0)
|
|
90
|
-
return { score: 0, blockStatus:
|
|
79
|
+
return { score: 0, blockStatus: 'clear' };
|
|
91
80
|
const score = 1 - signals.reduce((product, s) => product * (1 - s.weight), 1);
|
|
92
|
-
const blockStatus = score >= 0.7 ?
|
|
81
|
+
const blockStatus = score >= 0.7 ? 'blocked' : score >= 0.4 ? 'suspected' : 'clear';
|
|
93
82
|
return { score, blockStatus };
|
|
94
83
|
}
|
|
95
84
|
// --- Fast-pass Signal Detectors ---
|
|
@@ -102,15 +91,15 @@ class BlockDetection {
|
|
|
102
91
|
name: `http_status_${status}`,
|
|
103
92
|
weight: 0.85,
|
|
104
93
|
details: `HTTP ${status} response`,
|
|
105
|
-
source:
|
|
94
|
+
source: 'fast',
|
|
106
95
|
};
|
|
107
96
|
}
|
|
108
97
|
if (status === 503) {
|
|
109
98
|
return {
|
|
110
|
-
name:
|
|
99
|
+
name: 'http_status_503',
|
|
111
100
|
weight: 0.6,
|
|
112
|
-
details:
|
|
113
|
-
source:
|
|
101
|
+
details: 'HTTP 503 response',
|
|
102
|
+
source: 'fast',
|
|
114
103
|
};
|
|
115
104
|
}
|
|
116
105
|
return null;
|
|
@@ -121,23 +110,22 @@ class BlockDetection {
|
|
|
121
110
|
return signals;
|
|
122
111
|
try {
|
|
123
112
|
const headers = response.headers?.() ?? {};
|
|
124
|
-
const cfMitigated = headers[
|
|
125
|
-
if (cfMitigated &&
|
|
126
|
-
cfMitigated.toLowerCase().includes("challenge")) {
|
|
113
|
+
const cfMitigated = headers['cf-mitigated'];
|
|
114
|
+
if (cfMitigated && cfMitigated.toLowerCase().includes('challenge')) {
|
|
127
115
|
signals.push({
|
|
128
|
-
name:
|
|
116
|
+
name: 'waf_header_cf_mitigated',
|
|
129
117
|
weight: 0.9,
|
|
130
118
|
details: `cf-mitigated: ${cfMitigated}`,
|
|
131
|
-
source:
|
|
119
|
+
source: 'fast',
|
|
132
120
|
});
|
|
133
121
|
}
|
|
134
|
-
const server = headers[
|
|
135
|
-
if (server && server.toLowerCase().includes(
|
|
122
|
+
const server = headers['server'];
|
|
123
|
+
if (server && server.toLowerCase().includes('cloudflare')) {
|
|
136
124
|
signals.push({
|
|
137
|
-
name:
|
|
125
|
+
name: 'waf_header_cloudflare',
|
|
138
126
|
weight: 0.1,
|
|
139
127
|
details: `server: ${server}`,
|
|
140
|
-
source:
|
|
128
|
+
source: 'fast',
|
|
141
129
|
});
|
|
142
130
|
}
|
|
143
131
|
}
|
|
@@ -153,10 +141,10 @@ class BlockDetection {
|
|
|
153
141
|
for (const keyword of this.allTitleKeywords) {
|
|
154
142
|
if (title.includes(keyword.toLowerCase())) {
|
|
155
143
|
return {
|
|
156
|
-
name:
|
|
144
|
+
name: 'title_keyword',
|
|
157
145
|
weight: 0.8,
|
|
158
146
|
details: `Title contains "${keyword}"`,
|
|
159
|
-
source:
|
|
147
|
+
source: 'full',
|
|
160
148
|
};
|
|
161
149
|
}
|
|
162
150
|
}
|
|
@@ -178,10 +166,10 @@ class BlockDetection {
|
|
|
178
166
|
}, selectors);
|
|
179
167
|
if (found) {
|
|
180
168
|
return {
|
|
181
|
-
name:
|
|
169
|
+
name: 'challenge_selector',
|
|
182
170
|
weight: 0.8,
|
|
183
171
|
details: `Challenge selector found: ${found}`,
|
|
184
|
-
source:
|
|
172
|
+
source: 'full',
|
|
185
173
|
};
|
|
186
174
|
}
|
|
187
175
|
}
|
|
@@ -194,30 +182,27 @@ class BlockDetection {
|
|
|
194
182
|
const signals = [];
|
|
195
183
|
try {
|
|
196
184
|
const text = useInnerText
|
|
197
|
-
? await page.evaluate(() => document.body?.innerText ??
|
|
198
|
-
: await page.evaluate(() => document.body?.textContent ??
|
|
185
|
+
? await page.evaluate(() => document.body?.innerText ?? '')
|
|
186
|
+
: await page.evaluate(() => document.body?.textContent ?? '');
|
|
199
187
|
const textLower = text.toLowerCase();
|
|
200
188
|
if (text.length < 50) {
|
|
201
189
|
signals.push({
|
|
202
|
-
name:
|
|
190
|
+
name: 'visible_text_short',
|
|
203
191
|
weight: 0.2,
|
|
204
192
|
details: `Visible text very short (${text.length} chars)`,
|
|
205
|
-
source:
|
|
193
|
+
source: 'full',
|
|
206
194
|
});
|
|
207
195
|
}
|
|
208
196
|
// Strong keywords (substring match, short page < 500 chars)
|
|
209
197
|
if (text.length < 500) {
|
|
210
|
-
const allStrong = [
|
|
211
|
-
...STRONG_TEXT_KEYWORDS,
|
|
212
|
-
...this.config.extraKeywords,
|
|
213
|
-
];
|
|
198
|
+
const allStrong = [...STRONG_TEXT_KEYWORDS, ...this.config.extraKeywords];
|
|
214
199
|
for (const keyword of allStrong) {
|
|
215
200
|
if (textLower.includes(keyword.toLowerCase())) {
|
|
216
201
|
signals.push({
|
|
217
|
-
name:
|
|
202
|
+
name: 'visible_text_keyword_strong',
|
|
218
203
|
weight: 0.6,
|
|
219
204
|
details: `Strong keyword "${keyword}" on short page`,
|
|
220
|
-
source:
|
|
205
|
+
source: 'full',
|
|
221
206
|
});
|
|
222
207
|
break;
|
|
223
208
|
}
|
|
@@ -227,10 +212,10 @@ class BlockDetection {
|
|
|
227
212
|
for (const { keyword, regex } of WEAK_TEXT_REGEXES) {
|
|
228
213
|
if (regex.test(text)) {
|
|
229
214
|
signals.push({
|
|
230
|
-
name:
|
|
215
|
+
name: 'visible_text_keyword_weak',
|
|
231
216
|
weight: 0.15,
|
|
232
217
|
details: `Weak keyword "${keyword}" found with word boundary`,
|
|
233
|
-
source:
|
|
218
|
+
source: 'full',
|
|
234
219
|
});
|
|
235
220
|
break;
|
|
236
221
|
}
|
|
@@ -244,17 +229,16 @@ class BlockDetection {
|
|
|
244
229
|
async detectTextToHtmlRatio(page) {
|
|
245
230
|
try {
|
|
246
231
|
const result = await page.evaluate(() => {
|
|
247
|
-
const html = document.documentElement?.outerHTML ??
|
|
248
|
-
const text = document.body?.textContent ??
|
|
232
|
+
const html = document.documentElement?.outerHTML ?? '';
|
|
233
|
+
const text = document.body?.textContent ?? '';
|
|
249
234
|
return { htmlLength: html.length, textLength: text.length };
|
|
250
235
|
});
|
|
251
|
-
if (result.htmlLength >= 1000 &&
|
|
252
|
-
result.textLength / result.htmlLength < 0.03) {
|
|
236
|
+
if (result.htmlLength >= 1000 && result.textLength / result.htmlLength < 0.03) {
|
|
253
237
|
return {
|
|
254
|
-
name:
|
|
238
|
+
name: 'low_text_ratio',
|
|
255
239
|
weight: 0.2,
|
|
256
240
|
details: `Low text/HTML ratio: ${result.textLength}/${result.htmlLength}`,
|
|
257
|
-
source:
|
|
241
|
+
source: 'full',
|
|
258
242
|
};
|
|
259
243
|
}
|
|
260
244
|
}
|
|
@@ -278,7 +262,7 @@ class BlockDetection {
|
|
|
278
262
|
break;
|
|
279
263
|
const redirectResponse = redirectedFrom.response?.();
|
|
280
264
|
hops.push({
|
|
281
|
-
url: redirectedFrom.url?.() ??
|
|
265
|
+
url: redirectedFrom.url?.() ?? '',
|
|
282
266
|
statusCode: redirectResponse?.status?.() ?? 0,
|
|
283
267
|
});
|
|
284
268
|
req = redirectedFrom;
|
|
@@ -291,24 +275,24 @@ class BlockDetection {
|
|
|
291
275
|
for (const pattern of CHALLENGE_DOMAIN_PATTERNS) {
|
|
292
276
|
if (hop.url.includes(pattern)) {
|
|
293
277
|
signals.push({
|
|
294
|
-
name:
|
|
278
|
+
name: 'redirect_to_challenge',
|
|
295
279
|
weight: 0.7,
|
|
296
280
|
details: `Redirect through challenge domain: ${hop.url}`,
|
|
297
|
-
source:
|
|
281
|
+
source: 'full',
|
|
298
282
|
});
|
|
299
283
|
return { signals, chain };
|
|
300
284
|
}
|
|
301
285
|
}
|
|
302
286
|
}
|
|
303
287
|
// Also check the final response URL
|
|
304
|
-
const finalUrl = response.url?.() ??
|
|
288
|
+
const finalUrl = response.url?.() ?? '';
|
|
305
289
|
for (const pattern of CHALLENGE_DOMAIN_PATTERNS) {
|
|
306
290
|
if (finalUrl.includes(pattern)) {
|
|
307
291
|
signals.push({
|
|
308
|
-
name:
|
|
292
|
+
name: 'redirect_to_challenge',
|
|
309
293
|
weight: 0.7,
|
|
310
294
|
details: `Final URL is challenge domain: ${finalUrl}`,
|
|
311
|
-
source:
|
|
295
|
+
source: 'full',
|
|
312
296
|
});
|
|
313
297
|
break;
|
|
314
298
|
}
|
|
@@ -325,7 +309,7 @@ class BlockDetection {
|
|
|
325
309
|
const meta = document.querySelector('meta[http-equiv="refresh"]');
|
|
326
310
|
if (!meta)
|
|
327
311
|
return null;
|
|
328
|
-
const content = meta.getAttribute(
|
|
312
|
+
const content = meta.getAttribute('content') ?? '';
|
|
329
313
|
const match = content.match(/url\s*=\s*(.+)/i);
|
|
330
314
|
return match ? match[1].trim() : null;
|
|
331
315
|
});
|
|
@@ -333,10 +317,10 @@ class BlockDetection {
|
|
|
333
317
|
for (const pattern of CHALLENGE_DOMAIN_PATTERNS) {
|
|
334
318
|
if (refreshUrl.includes(pattern)) {
|
|
335
319
|
return {
|
|
336
|
-
name:
|
|
320
|
+
name: 'meta_refresh_challenge',
|
|
337
321
|
weight: 0.65,
|
|
338
322
|
details: `Meta refresh to challenge URL: ${refreshUrl}`,
|
|
339
|
-
source:
|
|
323
|
+
source: 'full',
|
|
340
324
|
};
|
|
341
325
|
}
|
|
342
326
|
}
|
|
@@ -356,7 +340,7 @@ class BlockDetection {
|
|
|
356
340
|
const url = page.url();
|
|
357
341
|
const hostname = this.extractHostname(url);
|
|
358
342
|
if (!this.config.enabled) {
|
|
359
|
-
return this.makeResult(url, hostname, [],
|
|
343
|
+
return this.makeResult(url, hostname, [], 'fast', []);
|
|
360
344
|
}
|
|
361
345
|
const signals = [];
|
|
362
346
|
const statusSignal = this.detectHttpStatus(response);
|
|
@@ -364,7 +348,7 @@ class BlockDetection {
|
|
|
364
348
|
signals.push(statusSignal);
|
|
365
349
|
const headerSignals = this.detectResponseHeaders(response);
|
|
366
350
|
signals.push(...headerSignals);
|
|
367
|
-
const result = this.makeResult(url, hostname, signals,
|
|
351
|
+
const result = this.makeResult(url, hostname, signals, 'fast', []);
|
|
368
352
|
this.logResult(result);
|
|
369
353
|
return result;
|
|
370
354
|
}
|
|
@@ -399,12 +383,10 @@ class BlockDetection {
|
|
|
399
383
|
const url = page.url();
|
|
400
384
|
const hostname = this.extractHostname(url);
|
|
401
385
|
if (!this.config.enabled) {
|
|
402
|
-
return this.makeResult(url, hostname, [],
|
|
386
|
+
return this.makeResult(url, hostname, [], 'full', []);
|
|
403
387
|
}
|
|
404
388
|
// Start with fast-pass signals
|
|
405
|
-
const signals = fastResult
|
|
406
|
-
? [...fastResult.signals]
|
|
407
|
-
: [];
|
|
389
|
+
const signals = fastResult ? [...fastResult.signals] : [];
|
|
408
390
|
// If no fast pass was done and we have a response, run fast detectors
|
|
409
391
|
if (!fastResult && response) {
|
|
410
392
|
const statusSignal = this.detectHttpStatus(response);
|
|
@@ -413,7 +395,7 @@ class BlockDetection {
|
|
|
413
395
|
const headerSignals = this.detectResponseHeaders(response);
|
|
414
396
|
signals.push(...headerSignals);
|
|
415
397
|
}
|
|
416
|
-
signals.push(...await this.runContentDetectors(page));
|
|
398
|
+
signals.push(...(await this.runContentDetectors(page)));
|
|
417
399
|
const { signals: redirectSignals, chain } = this.detectRedirectChain(response);
|
|
418
400
|
signals.push(...redirectSignals);
|
|
419
401
|
return this.reEvaluateIfSuspected(page, url, hostname, signals, chain);
|
|
@@ -425,7 +407,7 @@ class BlockDetection {
|
|
|
425
407
|
const url = page.url();
|
|
426
408
|
const hostname = this.extractHostname(url);
|
|
427
409
|
if (!this.config.enabled) {
|
|
428
|
-
return this.makeResult(url, hostname, [],
|
|
410
|
+
return this.makeResult(url, hostname, [], 'full', []);
|
|
429
411
|
}
|
|
430
412
|
const signals = await this.runContentDetectors(page);
|
|
431
413
|
return this.reEvaluateIfSuspected(page, url, hostname, signals, []);
|
|
@@ -433,14 +415,14 @@ class BlockDetection {
|
|
|
433
415
|
async reEvaluateIfSuspected(page, url, hostname, signals, redirectChain) {
|
|
434
416
|
const preliminary = this.computeScore(signals);
|
|
435
417
|
if (preliminary.score >= 0.4 && preliminary.score < 0.7) {
|
|
436
|
-
const nonTextSignals = signals.filter((s) => !s.name.startsWith(
|
|
418
|
+
const nonTextSignals = signals.filter((s) => !s.name.startsWith('visible_text_'));
|
|
437
419
|
const innerTextSignals = await this.detectVisibleText(page, true);
|
|
438
420
|
nonTextSignals.push(...innerTextSignals);
|
|
439
|
-
const result = this.makeResult(url, hostname, nonTextSignals,
|
|
421
|
+
const result = this.makeResult(url, hostname, nonTextSignals, 'full', redirectChain);
|
|
440
422
|
this.logResult(result);
|
|
441
423
|
return result;
|
|
442
424
|
}
|
|
443
|
-
const result = this.makeResult(url, hostname, signals,
|
|
425
|
+
const result = this.makeResult(url, hostname, signals, 'full', redirectChain);
|
|
444
426
|
this.logResult(result);
|
|
445
427
|
return result;
|
|
446
428
|
}
|
package/dist/cjs/client/rules.js
CHANGED
|
@@ -62,7 +62,12 @@ function matchPattern(hostname, pattern) {
|
|
|
62
62
|
*/
|
|
63
63
|
function normalizeRules(rules) {
|
|
64
64
|
if (!rules || rules.length === 0) {
|
|
65
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
positiveRules: [],
|
|
67
|
+
negativeRules: [],
|
|
68
|
+
hasCatchAll: false,
|
|
69
|
+
empty: true,
|
|
70
|
+
};
|
|
66
71
|
}
|
|
67
72
|
const trimmed = rules
|
|
68
73
|
.filter((r) => typeof r === 'string')
|
|
@@ -70,7 +75,12 @@ function normalizeRules(rules) {
|
|
|
70
75
|
.filter((r) => r.length > 0)
|
|
71
76
|
.filter((r) => r !== 'auto');
|
|
72
77
|
if (trimmed.length === 0) {
|
|
73
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
positiveRules: [],
|
|
80
|
+
negativeRules: [],
|
|
81
|
+
hasCatchAll: false,
|
|
82
|
+
empty: true,
|
|
83
|
+
};
|
|
74
84
|
}
|
|
75
85
|
const negativeRules = [];
|
|
76
86
|
const positiveRules = [];
|
package/dist/cjs/connect.js
CHANGED
|
@@ -96,8 +96,8 @@ async function connect(sessionName) {
|
|
|
96
96
|
let context;
|
|
97
97
|
let page;
|
|
98
98
|
try {
|
|
99
|
-
context = browser.contexts()[0] ?? await browser.newContext();
|
|
100
|
-
page = context.pages()[0] ?? await context.newPage();
|
|
99
|
+
context = browser.contexts()[0] ?? (await browser.newContext());
|
|
100
|
+
page = context.pages()[0] ?? (await context.newPage());
|
|
101
101
|
}
|
|
102
102
|
catch (err) {
|
|
103
103
|
await browser.close().catch(() => { });
|
package/dist/cjs/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Aluvia Client Node
|
|
3
3
|
// Main entry point
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.ConnectError = exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = exports.connect = exports.AluviaApi = exports.AluviaClient = void 0;
|
|
5
|
+
exports.toLockData = exports.listSessions = exports.generateSessionName = exports.validateSessionName = exports.getLogFilePath = exports.isProcessAlive = exports.removeLock = exports.readLock = exports.writeLock = exports.ConnectError = exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = exports.connect = exports.AluviaApi = exports.AluviaClient = void 0;
|
|
6
6
|
// Public class
|
|
7
7
|
var AluviaClient_js_1 = require("./client/AluviaClient.js");
|
|
8
8
|
Object.defineProperty(exports, "AluviaClient", { enumerable: true, get: function () { return AluviaClient_js_1.AluviaClient; } });
|
|
@@ -18,3 +18,14 @@ Object.defineProperty(exports, "InvalidApiKeyError", { enumerable: true, get: fu
|
|
|
18
18
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return errors_js_1.ApiError; } });
|
|
19
19
|
Object.defineProperty(exports, "ProxyStartError", { enumerable: true, get: function () { return errors_js_1.ProxyStartError; } });
|
|
20
20
|
Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
|
|
21
|
+
// Session lock utilities (used by CLI)
|
|
22
|
+
var lock_js_1 = require("./session/lock.js");
|
|
23
|
+
Object.defineProperty(exports, "writeLock", { enumerable: true, get: function () { return lock_js_1.writeLock; } });
|
|
24
|
+
Object.defineProperty(exports, "readLock", { enumerable: true, get: function () { return lock_js_1.readLock; } });
|
|
25
|
+
Object.defineProperty(exports, "removeLock", { enumerable: true, get: function () { return lock_js_1.removeLock; } });
|
|
26
|
+
Object.defineProperty(exports, "isProcessAlive", { enumerable: true, get: function () { return lock_js_1.isProcessAlive; } });
|
|
27
|
+
Object.defineProperty(exports, "getLogFilePath", { enumerable: true, get: function () { return lock_js_1.getLogFilePath; } });
|
|
28
|
+
Object.defineProperty(exports, "validateSessionName", { enumerable: true, get: function () { return lock_js_1.validateSessionName; } });
|
|
29
|
+
Object.defineProperty(exports, "generateSessionName", { enumerable: true, get: function () { return lock_js_1.generateSessionName; } });
|
|
30
|
+
Object.defineProperty(exports, "listSessions", { enumerable: true, get: function () { return lock_js_1.listSessions; } });
|
|
31
|
+
Object.defineProperty(exports, "toLockData", { enumerable: true, get: function () { return lock_js_1.toLockData; } });
|