@aluvia/sdk 1.0.0 → 1.3.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 +425 -256
- package/dist/cjs/api/account.js +10 -74
- package/dist/cjs/api/apiUtils.js +80 -0
- package/dist/cjs/api/geos.js +2 -63
- package/dist/cjs/api/request.js +8 -2
- package/dist/cjs/bin/account.js +31 -0
- package/dist/cjs/bin/api-helpers.js +58 -0
- package/dist/cjs/bin/cli.js +245 -0
- package/dist/cjs/bin/close.js +120 -0
- package/dist/cjs/bin/geos.js +10 -0
- package/dist/cjs/bin/mcp-helpers.js +57 -0
- package/dist/cjs/bin/mcp-server.js +220 -0
- package/dist/cjs/bin/mcp-tools.js +90 -0
- package/dist/cjs/bin/open.js +293 -0
- package/dist/cjs/bin/session.js +259 -0
- package/dist/cjs/client/AluviaClient.js +411 -150
- package/dist/cjs/client/BlockDetection.js +486 -0
- package/dist/cjs/client/ConfigManager.js +26 -23
- package/dist/cjs/client/PageLoadDetection.js +175 -0
- package/dist/cjs/client/ProxyServer.js +4 -2
- package/dist/cjs/client/logger.js +4 -0
- package/dist/cjs/client/rules.js +38 -49
- package/dist/cjs/connect.js +117 -0
- package/dist/cjs/errors.js +12 -1
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/session/lock.js +186 -0
- package/dist/esm/api/account.js +2 -66
- package/dist/esm/api/apiUtils.js +71 -0
- package/dist/esm/api/geos.js +2 -63
- package/dist/esm/api/request.js +8 -2
- package/dist/esm/bin/account.js +28 -0
- package/dist/esm/bin/api-helpers.js +53 -0
- package/dist/esm/bin/cli.js +242 -0
- package/dist/esm/bin/close.js +117 -0
- package/dist/esm/bin/geos.js +7 -0
- package/dist/esm/bin/mcp-helpers.js +51 -0
- package/dist/esm/bin/mcp-server.js +185 -0
- package/dist/esm/bin/mcp-tools.js +78 -0
- package/dist/esm/bin/open.js +256 -0
- package/dist/esm/bin/session.js +252 -0
- package/dist/esm/client/AluviaClient.js +384 -156
- package/dist/esm/client/BlockDetection.js +482 -0
- package/dist/esm/client/ConfigManager.js +21 -18
- package/dist/esm/client/PageLoadDetection.js +171 -0
- package/dist/esm/client/ProxyServer.js +5 -3
- package/dist/esm/client/logger.js +4 -0
- package/dist/esm/client/rules.js +36 -49
- package/dist/esm/connect.js +81 -0
- package/dist/esm/errors.js +10 -0
- package/dist/esm/index.js +5 -3
- package/dist/esm/session/lock.js +142 -0
- package/dist/types/api/AluviaApi.d.ts +2 -7
- package/dist/types/api/account.d.ts +1 -16
- package/dist/types/api/apiUtils.d.ts +28 -0
- package/dist/types/api/geos.d.ts +1 -1
- package/dist/types/bin/account.d.ts +1 -0
- package/dist/types/bin/api-helpers.d.ts +20 -0
- package/dist/types/bin/cli.d.ts +2 -0
- package/dist/types/bin/close.d.ts +1 -0
- package/dist/types/bin/geos.d.ts +1 -0
- package/dist/types/bin/mcp-helpers.d.ts +28 -0
- package/dist/types/bin/mcp-server.d.ts +2 -0
- package/dist/types/bin/mcp-tools.d.ts +46 -0
- package/dist/types/bin/open.d.ts +21 -0
- package/dist/types/bin/session.d.ts +11 -0
- package/dist/types/client/AluviaClient.d.ts +51 -4
- package/dist/types/client/BlockDetection.d.ts +96 -0
- package/dist/types/client/ConfigManager.d.ts +6 -1
- package/dist/types/client/PageLoadDetection.d.ts +93 -0
- package/dist/types/client/logger.d.ts +2 -0
- package/dist/types/client/rules.d.ts +18 -0
- package/dist/types/client/types.d.ts +69 -46
- package/dist/types/connect.d.ts +18 -0
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/index.d.ts +7 -5
- package/dist/types/session/lock.d.ts +43 -0
- package/package.json +11 -2
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// AluviaClient - Main public class for Aluvia Client
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
37
|
exports.AluviaClient = void 0;
|
|
5
38
|
const ConfigManager_js_1 = require("./ConfigManager.js");
|
|
@@ -8,30 +41,42 @@ const errors_js_1 = require("../errors.js");
|
|
|
8
41
|
const adapters_js_1 = require("./adapters.js");
|
|
9
42
|
const logger_js_1 = require("./logger.js");
|
|
10
43
|
const AluviaApi_js_1 = require("../api/AluviaApi.js");
|
|
44
|
+
const BlockDetection_js_1 = require("./BlockDetection.js");
|
|
45
|
+
const net = __importStar(require("node:net"));
|
|
11
46
|
/**
|
|
12
47
|
* AluviaClient is the main entry point for the Aluvia Client.
|
|
13
48
|
*
|
|
14
49
|
* It manages the local proxy server and configuration polling.
|
|
15
50
|
*/
|
|
16
51
|
class AluviaClient {
|
|
52
|
+
/** Read-only access to the connection ID from ConfigManager. */
|
|
53
|
+
get connectionId() {
|
|
54
|
+
return this.configManager.connectionId;
|
|
55
|
+
}
|
|
17
56
|
constructor(options) {
|
|
18
57
|
this.connection = null;
|
|
19
58
|
this.started = false;
|
|
20
59
|
this.startPromise = null;
|
|
21
|
-
|
|
60
|
+
this.blockDetection = null;
|
|
61
|
+
this.pageStates = new WeakMap();
|
|
62
|
+
/** Promise-based mutex to serialize handleDetectionResult's critical section. */
|
|
63
|
+
this._detectionMutex = Promise.resolve();
|
|
64
|
+
const apiKey = String(options.apiKey ?? "").trim();
|
|
22
65
|
if (!apiKey) {
|
|
23
|
-
throw new errors_js_1.MissingApiKeyError(
|
|
66
|
+
throw new errors_js_1.MissingApiKeyError("Aluvia apiKey is required");
|
|
24
67
|
}
|
|
25
|
-
const localProxy = options.localProxy ?? true;
|
|
26
68
|
const strict = options.strict ?? true;
|
|
27
|
-
this.options = { ...options, apiKey,
|
|
28
|
-
const connectionId = Number(options.connectionId)
|
|
29
|
-
|
|
30
|
-
|
|
69
|
+
this.options = { ...options, apiKey, strict };
|
|
70
|
+
const connectionId = options.connectionId != null ? Number(options.connectionId) : undefined;
|
|
71
|
+
if (connectionId !== undefined && !Number.isFinite(connectionId)) {
|
|
72
|
+
throw new Error('connectionId must be a finite number');
|
|
73
|
+
}
|
|
74
|
+
const apiBaseUrl = options.apiBaseUrl ?? "https://api.aluvia.io/v1";
|
|
75
|
+
const pollIntervalMs = Math.max(options.pollIntervalMs ?? 5000, 1000);
|
|
31
76
|
const timeoutMs = options.timeoutMs;
|
|
32
|
-
const gatewayProtocol = options.gatewayProtocol ??
|
|
33
|
-
const gatewayPort = options.gatewayPort ?? (gatewayProtocol ===
|
|
34
|
-
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";
|
|
35
80
|
this.logger = new logger_js_1.Logger(logLevel);
|
|
36
81
|
// Create ConfigManager
|
|
37
82
|
this.configManager = new ConfigManager_js_1.ConfigManager({
|
|
@@ -51,13 +96,233 @@ class AluviaClient {
|
|
|
51
96
|
apiBaseUrl,
|
|
52
97
|
timeoutMs,
|
|
53
98
|
});
|
|
99
|
+
// Initialize block detection if configured
|
|
100
|
+
if (options.blockDetection !== undefined || options.startPlaywright) {
|
|
101
|
+
this.logger.debug("Initializing block detection");
|
|
102
|
+
const detectionConfig = options.blockDetection ?? { enabled: true };
|
|
103
|
+
this.blockDetection = new BlockDetection_js_1.BlockDetection(detectionConfig, this.logger);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Attaches per-page listeners for two-pass detection and SPA navigation.
|
|
108
|
+
*/
|
|
109
|
+
attachPageListeners(page) {
|
|
110
|
+
const pageState = {
|
|
111
|
+
lastResponse: null,
|
|
112
|
+
lastAnalysisTs: 0,
|
|
113
|
+
skipFullPass: false,
|
|
114
|
+
fastResult: null,
|
|
115
|
+
};
|
|
116
|
+
this.pageStates.set(page, pageState);
|
|
117
|
+
// Capture navigation responses on main frame
|
|
118
|
+
page.on("response", (response) => {
|
|
119
|
+
try {
|
|
120
|
+
if (response.request().isNavigationRequest() &&
|
|
121
|
+
response.request().frame() === page.mainFrame()) {
|
|
122
|
+
pageState.lastResponse = response;
|
|
123
|
+
pageState.skipFullPass = false;
|
|
124
|
+
pageState.fastResult = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Ignore errors
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Fast pass at domcontentloaded
|
|
132
|
+
page.on("domcontentloaded", async () => {
|
|
133
|
+
if (!this.blockDetection)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
const result = await this.blockDetection.analyzeFast(page, pageState.lastResponse);
|
|
137
|
+
pageState.fastResult = result;
|
|
138
|
+
pageState.lastAnalysisTs = Date.now();
|
|
139
|
+
if (result.score >= 0.9) {
|
|
140
|
+
pageState.skipFullPass = true;
|
|
141
|
+
await this.handleDetectionResult(result, page);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.logger.warn(`Error in fast-pass detection: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Full pass at load
|
|
149
|
+
page.on("load", async () => {
|
|
150
|
+
if (!this.blockDetection || pageState.skipFullPass)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
// Wait for networkidle with timeout cap
|
|
154
|
+
try {
|
|
155
|
+
await page.waitForLoadState("networkidle", {
|
|
156
|
+
timeout: this.blockDetection.getNetworkIdleTimeoutMs(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Timeout is ok, proceed anyway
|
|
161
|
+
}
|
|
162
|
+
const result = await this.blockDetection.analyzeFull(page, pageState.lastResponse, pageState.fastResult ?? undefined);
|
|
163
|
+
pageState.lastAnalysisTs = Date.now();
|
|
164
|
+
await this.handleDetectionResult(result, page);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.logger.warn(`Error in full-pass detection: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// SPA detection via framenavigated
|
|
171
|
+
page.on("framenavigated", async (frame) => {
|
|
172
|
+
if (!this.blockDetection)
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
// Only handle main frame
|
|
176
|
+
if (frame !== page.mainFrame())
|
|
177
|
+
return;
|
|
178
|
+
// Debounce per-page
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
if (now - pageState.lastAnalysisTs < 200)
|
|
181
|
+
return;
|
|
182
|
+
// Wait 50ms and check if a new response arrived
|
|
183
|
+
const responseBefore = pageState.lastResponse;
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
185
|
+
if (pageState.lastResponse !== responseBefore)
|
|
186
|
+
return; // Not SPA
|
|
187
|
+
const result = await this.blockDetection.analyzeSpa(page);
|
|
188
|
+
pageState.lastAnalysisTs = Date.now();
|
|
189
|
+
await this.handleDetectionResult(result, page);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
this.logger.warn(`Error in SPA detection: ${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Attaches page listeners to all existing and future pages in a context.
|
|
198
|
+
*/
|
|
199
|
+
attachBlockDetectionListener(context) {
|
|
200
|
+
this.logger.debug("Attaching block detection listener to context");
|
|
201
|
+
// Attach to existing pages
|
|
202
|
+
try {
|
|
203
|
+
const existingPages = context.pages();
|
|
204
|
+
for (const page of existingPages) {
|
|
205
|
+
this.attachPageListeners(page);
|
|
206
|
+
// Check if page has already loaded (not about:blank)
|
|
207
|
+
if (page.url() !== "about:blank" && this.blockDetection) {
|
|
208
|
+
this.blockDetection
|
|
209
|
+
.analyzeFull(page, null)
|
|
210
|
+
.then((result) => {
|
|
211
|
+
return this.handleDetectionResult(result, page);
|
|
212
|
+
})
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
this.logger.warn(`Error analyzing existing page: ${error.message}`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Ignore errors
|
|
221
|
+
}
|
|
222
|
+
// Attach to future pages
|
|
223
|
+
context.on("page", (page) => {
|
|
224
|
+
this.logger.debug(`New page detected: ${page.url()}`);
|
|
225
|
+
this.attachPageListeners(page);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Handle a detection result: fire callback, check persistent block, reload if needed.
|
|
230
|
+
*
|
|
231
|
+
* The auto-unblock critical section (persistent-block checks, rule updates, page reload)
|
|
232
|
+
* is serialized via a promise-based mutex to prevent concurrent calls from reading stale
|
|
233
|
+
* state and producing duplicate rule additions or missed persistent-block escalation.
|
|
234
|
+
*/
|
|
235
|
+
async handleDetectionResult(result, page) {
|
|
236
|
+
if (!this.blockDetection)
|
|
237
|
+
return;
|
|
238
|
+
// Fire user's onDetection callback for all tiers (including clear).
|
|
239
|
+
// Pass a shallow clone so later internal mutations (e.g. persistentBlock)
|
|
240
|
+
// don't affect the object the user received.
|
|
241
|
+
// This runs outside the mutex so user code can execute concurrently
|
|
242
|
+
// without risk of deadlock if it triggers navigation.
|
|
243
|
+
const onDetection = this.blockDetection.getOnDetection();
|
|
244
|
+
if (onDetection) {
|
|
245
|
+
try {
|
|
246
|
+
const snapshot = { ...result, signals: [...result.signals] };
|
|
247
|
+
await onDetection(snapshot, page);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.logger.warn(`Error in onDetection callback: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// If auto-reload is disabled, stop here (detection-only mode)
|
|
254
|
+
if (!this.blockDetection.isAutoUnblock())
|
|
255
|
+
return;
|
|
256
|
+
// Check if auto-reload should fire for this blockStatus
|
|
257
|
+
const shouldReload = result.blockStatus === "blocked" ||
|
|
258
|
+
(result.blockStatus === "suspected" &&
|
|
259
|
+
this.blockDetection.isAutoUnblockOnSuspected());
|
|
260
|
+
if (!shouldReload)
|
|
261
|
+
return;
|
|
262
|
+
// Serialize the critical section: persistent-block state, rule updates, reload.
|
|
263
|
+
// This prevents concurrent handlers from reading stale retriedUrls/persistentHostnames.
|
|
264
|
+
let release;
|
|
265
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
266
|
+
const acquired = this._detectionMutex;
|
|
267
|
+
this._detectionMutex = this._detectionMutex.then(() => gate);
|
|
268
|
+
await acquired;
|
|
269
|
+
try {
|
|
270
|
+
await this._handleAutoUnblock(result, page);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
release();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Auto-unblock critical section. Must only be called under _detectionMutex.
|
|
278
|
+
*/
|
|
279
|
+
async _handleAutoUnblock(result, page) {
|
|
280
|
+
const url = result.url;
|
|
281
|
+
const hostname = result.hostname;
|
|
282
|
+
// Check persistent block escalation
|
|
283
|
+
if (this.blockDetection.persistentHostnames.has(hostname)) {
|
|
284
|
+
result.persistentBlock = true;
|
|
285
|
+
this.logger.warn(`Persistent block on ${hostname}, skipping reload`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (this.blockDetection.retriedUrls.has(url)) {
|
|
289
|
+
// Second block for this URL - mark hostname as persistent
|
|
290
|
+
result.persistentBlock = true;
|
|
291
|
+
this.blockDetection.persistentHostnames.add(hostname);
|
|
292
|
+
this.logger.warn(`Persistent block detected for ${hostname} after retry of ${url}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// First block for this URL — cap set size to prevent unbounded growth
|
|
296
|
+
if (this.blockDetection.retriedUrls.size >= 10000) {
|
|
297
|
+
this.blockDetection.retriedUrls.clear();
|
|
298
|
+
}
|
|
299
|
+
this.blockDetection.retriedUrls.add(url);
|
|
300
|
+
// Add hostname to proxy routing rules
|
|
301
|
+
try {
|
|
302
|
+
const config = this.configManager.getConfig();
|
|
303
|
+
const currentRules = config?.rules ?? [];
|
|
304
|
+
if (!currentRules.includes(hostname)) {
|
|
305
|
+
this.logger.info(`Auto-adding ${hostname} to routing rules due to detection (blockStatus: ${result.blockStatus})`);
|
|
306
|
+
await this.updateRules([...currentRules, hostname]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.logger.warn(`Failed to auto-add rule for ${hostname}: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
// Reload page
|
|
313
|
+
try {
|
|
314
|
+
this.logger.info(`Reloading page after adding ${hostname} to rules`);
|
|
315
|
+
await page.reload();
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
this.logger.warn(`Failed to reload page for ${hostname}: ${error.message}`);
|
|
319
|
+
}
|
|
54
320
|
}
|
|
55
321
|
/**
|
|
56
322
|
* Start the Aluvia Client connection:
|
|
57
323
|
* - Fetch initial account connection config from Aluvia.
|
|
58
324
|
* - Start polling for config updates.
|
|
59
|
-
* -
|
|
60
|
-
* - If localProxy is disabled: do NOT start a local proxy; adapters use gateway proxy settings.
|
|
325
|
+
* - Start a local HTTP proxy on 127.0.0.1:<localPort or free port>.
|
|
61
326
|
*
|
|
62
327
|
* Returns the active connection with host/port/url and a stop() method.
|
|
63
328
|
*/
|
|
@@ -71,41 +336,27 @@ class AluviaClient {
|
|
|
71
336
|
return this.startPromise;
|
|
72
337
|
}
|
|
73
338
|
this.startPromise = (async () => {
|
|
74
|
-
const localProxyEnabled = this.options.localProxy === true;
|
|
75
339
|
// Fetch initial configuration (may throw InvalidApiKeyError or ApiError)
|
|
76
340
|
await this.configManager.init();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
341
|
+
const browserInstance = this.options.startPlaywright
|
|
342
|
+
? await this._initPlaywright()
|
|
343
|
+
: undefined;
|
|
344
|
+
// Keep config fresh so routing decisions update without restarting.
|
|
345
|
+
this.configManager.startPolling();
|
|
346
|
+
try {
|
|
347
|
+
const { host, port, url } = await this.proxyServer.start(this.options.localPort);
|
|
83
348
|
let nodeAgents = null;
|
|
84
349
|
let undiciDispatcher = null;
|
|
85
350
|
let undiciFetchFn = null;
|
|
86
|
-
const cfgAtStart = this.configManager.getConfig();
|
|
87
|
-
const serverUrlAtStart = (() => {
|
|
88
|
-
if (!cfgAtStart)
|
|
89
|
-
return '';
|
|
90
|
-
const { protocol, host, port } = cfgAtStart.rawProxy;
|
|
91
|
-
return `${protocol}://${host}:${port}`;
|
|
92
|
-
})();
|
|
93
|
-
const getProxyUrlForHttpClients = () => {
|
|
94
|
-
const cfg = this.configManager.getConfig();
|
|
95
|
-
if (!cfg)
|
|
96
|
-
return 'http://127.0.0.1';
|
|
97
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
98
|
-
return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
99
|
-
};
|
|
100
351
|
const getNodeAgents = () => {
|
|
101
352
|
if (!nodeAgents) {
|
|
102
|
-
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(
|
|
353
|
+
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(url);
|
|
103
354
|
}
|
|
104
355
|
return nodeAgents;
|
|
105
356
|
};
|
|
106
357
|
const getUndiciDispatcher = () => {
|
|
107
358
|
if (!undiciDispatcher) {
|
|
108
|
-
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(
|
|
359
|
+
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(url);
|
|
109
360
|
}
|
|
110
361
|
return undiciDispatcher;
|
|
111
362
|
};
|
|
@@ -114,10 +365,10 @@ class AluviaClient {
|
|
|
114
365
|
if (!d)
|
|
115
366
|
return;
|
|
116
367
|
try {
|
|
117
|
-
if (typeof d.close ===
|
|
368
|
+
if (typeof d.close === "function") {
|
|
118
369
|
await d.close();
|
|
119
370
|
}
|
|
120
|
-
else if (typeof d.destroy ===
|
|
371
|
+
else if (typeof d.destroy === "function") {
|
|
121
372
|
d.destroy();
|
|
122
373
|
}
|
|
123
374
|
}
|
|
@@ -126,6 +377,7 @@ class AluviaClient {
|
|
|
126
377
|
}
|
|
127
378
|
};
|
|
128
379
|
const stop = async () => {
|
|
380
|
+
await this.proxyServer.stop();
|
|
129
381
|
this.configManager.stopPolling();
|
|
130
382
|
nodeAgents?.http?.destroy?.();
|
|
131
383
|
nodeAgents?.https?.destroy?.();
|
|
@@ -135,43 +387,46 @@ class AluviaClient {
|
|
|
135
387
|
this.connection = null;
|
|
136
388
|
this.started = false;
|
|
137
389
|
};
|
|
390
|
+
// Launch browser if Playwright was requested
|
|
391
|
+
let launchedBrowser = undefined;
|
|
392
|
+
let launchedBrowserContext = undefined;
|
|
393
|
+
let browserCdpUrl;
|
|
394
|
+
if (browserInstance) {
|
|
395
|
+
const proxySettings = (0, adapters_js_1.toPlaywrightProxySettings)(url);
|
|
396
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
397
|
+
const cdpPort = await AluviaClient.findFreePort();
|
|
398
|
+
try {
|
|
399
|
+
launchedBrowser = await browserInstance.launch({
|
|
400
|
+
proxy: proxySettings,
|
|
401
|
+
headless: this.options.headless !== false,
|
|
402
|
+
args: [`--remote-debugging-port=${cdpPort}`],
|
|
403
|
+
});
|
|
404
|
+
browserCdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
if (attempt === 2 || !err.message?.includes('EADDRINUSE'))
|
|
409
|
+
throw err;
|
|
410
|
+
this.logger.debug(`Port ${cdpPort} taken, retrying browser launch`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
launchedBrowserContext = await launchedBrowser.newContext();
|
|
414
|
+
// Attach block detection
|
|
415
|
+
this.attachBlockDetectionListener(launchedBrowserContext);
|
|
416
|
+
}
|
|
417
|
+
const stopWithBrowser = async () => {
|
|
418
|
+
if (launchedBrowser)
|
|
419
|
+
await launchedBrowser.close();
|
|
420
|
+
await stop();
|
|
421
|
+
};
|
|
138
422
|
// Build connection object
|
|
139
423
|
const connection = {
|
|
140
|
-
host
|
|
141
|
-
port
|
|
142
|
-
url
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return '';
|
|
147
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
148
|
-
return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
149
|
-
},
|
|
150
|
-
asPlaywright: () => {
|
|
151
|
-
const cfg = this.configManager.getConfig();
|
|
152
|
-
if (!cfg)
|
|
153
|
-
return { server: '' };
|
|
154
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
155
|
-
return {
|
|
156
|
-
...(0, adapters_js_1.toPlaywrightProxySettings)(`${protocol}://${host}:${port}`),
|
|
157
|
-
username,
|
|
158
|
-
password,
|
|
159
|
-
};
|
|
160
|
-
},
|
|
161
|
-
asPuppeteer: () => {
|
|
162
|
-
const cfg = this.configManager.getConfig();
|
|
163
|
-
if (!cfg)
|
|
164
|
-
return [];
|
|
165
|
-
const { protocol, host, port } = cfg.rawProxy;
|
|
166
|
-
return (0, adapters_js_1.toPuppeteerArgs)(`${protocol}://${host}:${port}`);
|
|
167
|
-
},
|
|
168
|
-
asSelenium: () => {
|
|
169
|
-
const cfg = this.configManager.getConfig();
|
|
170
|
-
if (!cfg)
|
|
171
|
-
return '';
|
|
172
|
-
const { protocol, host, port } = cfg.rawProxy;
|
|
173
|
-
return (0, adapters_js_1.toSeleniumArgs)(`${protocol}://${host}:${port}`);
|
|
174
|
-
},
|
|
424
|
+
host,
|
|
425
|
+
port,
|
|
426
|
+
url,
|
|
427
|
+
asPlaywright: () => (0, adapters_js_1.toPlaywrightProxySettings)(url),
|
|
428
|
+
asPuppeteer: () => (0, adapters_js_1.toPuppeteerArgs)(url),
|
|
429
|
+
asSelenium: () => (0, adapters_js_1.toSeleniumArgs)(url),
|
|
175
430
|
asNodeAgents: () => getNodeAgents(),
|
|
176
431
|
asAxiosConfig: () => (0, adapters_js_1.toAxiosConfig)(getNodeAgents()),
|
|
177
432
|
asGotOptions: () => (0, adapters_js_1.toGotOptions)(getNodeAgents()),
|
|
@@ -182,84 +437,20 @@ class AluviaClient {
|
|
|
182
437
|
}
|
|
183
438
|
return undiciFetchFn;
|
|
184
439
|
},
|
|
185
|
-
|
|
186
|
-
|
|
440
|
+
browser: launchedBrowser,
|
|
441
|
+
browserContext: launchedBrowserContext,
|
|
442
|
+
cdpUrl: browserCdpUrl,
|
|
443
|
+
stop: stopWithBrowser,
|
|
444
|
+
close: stopWithBrowser,
|
|
187
445
|
};
|
|
188
446
|
this.connection = connection;
|
|
189
447
|
this.started = true;
|
|
190
448
|
return connection;
|
|
191
449
|
}
|
|
192
|
-
|
|
193
|
-
this.configManager.startPolling();
|
|
194
|
-
// localProxy === true
|
|
195
|
-
const { host, port, url } = await this.proxyServer.start(this.options.localPort);
|
|
196
|
-
let nodeAgents = null;
|
|
197
|
-
let undiciDispatcher = null;
|
|
198
|
-
let undiciFetchFn = null;
|
|
199
|
-
const getNodeAgents = () => {
|
|
200
|
-
if (!nodeAgents) {
|
|
201
|
-
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(url);
|
|
202
|
-
}
|
|
203
|
-
return nodeAgents;
|
|
204
|
-
};
|
|
205
|
-
const getUndiciDispatcher = () => {
|
|
206
|
-
if (!undiciDispatcher) {
|
|
207
|
-
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(url);
|
|
208
|
-
}
|
|
209
|
-
return undiciDispatcher;
|
|
210
|
-
};
|
|
211
|
-
const closeUndiciDispatcher = async () => {
|
|
212
|
-
const d = undiciDispatcher;
|
|
213
|
-
if (!d)
|
|
214
|
-
return;
|
|
215
|
-
try {
|
|
216
|
-
if (typeof d.close === 'function') {
|
|
217
|
-
await d.close();
|
|
218
|
-
}
|
|
219
|
-
else if (typeof d.destroy === 'function') {
|
|
220
|
-
d.destroy();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
finally {
|
|
224
|
-
undiciDispatcher = null;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
const stop = async () => {
|
|
228
|
-
await this.proxyServer.stop();
|
|
450
|
+
catch (err) {
|
|
229
451
|
this.configManager.stopPolling();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
nodeAgents = null;
|
|
233
|
-
await closeUndiciDispatcher();
|
|
234
|
-
undiciFetchFn = null;
|
|
235
|
-
this.connection = null;
|
|
236
|
-
this.started = false;
|
|
237
|
-
};
|
|
238
|
-
// Build connection object
|
|
239
|
-
const connection = {
|
|
240
|
-
host,
|
|
241
|
-
port,
|
|
242
|
-
url,
|
|
243
|
-
getUrl: () => url,
|
|
244
|
-
asPlaywright: () => (0, adapters_js_1.toPlaywrightProxySettings)(url),
|
|
245
|
-
asPuppeteer: () => (0, adapters_js_1.toPuppeteerArgs)(url),
|
|
246
|
-
asSelenium: () => (0, adapters_js_1.toSeleniumArgs)(url),
|
|
247
|
-
asNodeAgents: () => getNodeAgents(),
|
|
248
|
-
asAxiosConfig: () => (0, adapters_js_1.toAxiosConfig)(getNodeAgents()),
|
|
249
|
-
asGotOptions: () => (0, adapters_js_1.toGotOptions)(getNodeAgents()),
|
|
250
|
-
asUndiciDispatcher: () => getUndiciDispatcher(),
|
|
251
|
-
asUndiciFetch: () => {
|
|
252
|
-
if (!undiciFetchFn) {
|
|
253
|
-
undiciFetchFn = (0, adapters_js_1.createUndiciFetch)(getUndiciDispatcher());
|
|
254
|
-
}
|
|
255
|
-
return undiciFetchFn;
|
|
256
|
-
},
|
|
257
|
-
stop,
|
|
258
|
-
close: stop,
|
|
259
|
-
};
|
|
260
|
-
this.connection = connection;
|
|
261
|
-
this.started = true;
|
|
262
|
-
return connection;
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
263
454
|
})();
|
|
264
455
|
try {
|
|
265
456
|
return await this.startPromise;
|
|
@@ -286,13 +477,16 @@ class AluviaClient {
|
|
|
286
477
|
if (!this.started) {
|
|
287
478
|
return;
|
|
288
479
|
}
|
|
289
|
-
|
|
290
|
-
|
|
480
|
+
if (this.connection) {
|
|
481
|
+
await this.connection.close();
|
|
482
|
+
// connection.close() sets this.connection = null and this.started = false
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
291
485
|
await this.proxyServer.stop();
|
|
486
|
+
this.configManager.stopPolling();
|
|
487
|
+
this.connection = null;
|
|
488
|
+
this.started = false;
|
|
292
489
|
}
|
|
293
|
-
this.configManager.stopPolling();
|
|
294
|
-
this.connection = null;
|
|
295
|
-
this.started = false;
|
|
296
490
|
}
|
|
297
491
|
/**
|
|
298
492
|
* Update the filtering rules used by the proxy.
|
|
@@ -319,7 +513,74 @@ class AluviaClient {
|
|
|
319
513
|
return;
|
|
320
514
|
}
|
|
321
515
|
const trimmed = targetGeo.trim();
|
|
322
|
-
await this.configManager.setConfig({
|
|
516
|
+
await this.configManager.setConfig({
|
|
517
|
+
target_geo: trimmed.length > 0 ? trimmed : null,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get a list of hostnames that have been detected as blocked.
|
|
522
|
+
*
|
|
523
|
+
* This list is maintained in-memory and cleared when the client is stopped.
|
|
524
|
+
* Only available when block detection is enabled.
|
|
525
|
+
*/
|
|
526
|
+
getBlockedHostnames() {
|
|
527
|
+
if (!this.blockDetection) {
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
return Array.from(this.blockDetection.persistentHostnames);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Clear the list of blocked hostnames and retried URLs.
|
|
534
|
+
*
|
|
535
|
+
* Only available when block detection is enabled.
|
|
536
|
+
*/
|
|
537
|
+
clearBlockedHostnames() {
|
|
538
|
+
if (this.blockDetection) {
|
|
539
|
+
this.blockDetection.persistentHostnames.clear();
|
|
540
|
+
this.blockDetection.retriedUrls.clear();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Import Playwright, auto-installing if necessary.
|
|
545
|
+
* Returns the chromium browser type for launching.
|
|
546
|
+
*/
|
|
547
|
+
async _initPlaywright() {
|
|
548
|
+
try {
|
|
549
|
+
const pw = await Promise.resolve().then(() => __importStar(require("playwright")));
|
|
550
|
+
// @ts-ignore
|
|
551
|
+
return pw.chromium;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Playwright not installed — attempt auto-install
|
|
555
|
+
this.logger.info("Playwright not found. Installing playwright...");
|
|
556
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("node:child_process")));
|
|
557
|
+
try {
|
|
558
|
+
execSync("npm install playwright", {
|
|
559
|
+
stdio: "inherit",
|
|
560
|
+
cwd: process.cwd(),
|
|
561
|
+
});
|
|
562
|
+
const pw = await Promise.resolve().then(() => __importStar(require("playwright")));
|
|
563
|
+
// @ts-ignore
|
|
564
|
+
return pw.chromium;
|
|
565
|
+
}
|
|
566
|
+
catch (installError) {
|
|
567
|
+
throw new errors_js_1.ApiError(`Failed to auto-install Playwright. Install it manually: npm install playwright\n${installError.message}`, 500);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Find a free TCP port by briefly binding to port 0.
|
|
573
|
+
*/
|
|
574
|
+
static findFreePort() {
|
|
575
|
+
return new Promise((resolve, reject) => {
|
|
576
|
+
const server = net.createServer();
|
|
577
|
+
server.listen(0, '127.0.0.1', () => {
|
|
578
|
+
const addr = server.address();
|
|
579
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
580
|
+
server.close(() => resolve(port));
|
|
581
|
+
});
|
|
582
|
+
server.on('error', reject);
|
|
583
|
+
});
|
|
323
584
|
}
|
|
324
585
|
}
|
|
325
586
|
exports.AluviaClient = AluviaClient;
|