@aluvia/sdk 1.1.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 +409 -285
- 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 +365 -189
- 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 +371 -195
- 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 +48 -47
- 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 -10
|
@@ -41,30 +41,42 @@ const errors_js_1 = require("../errors.js");
|
|
|
41
41
|
const adapters_js_1 = require("./adapters.js");
|
|
42
42
|
const logger_js_1 = require("./logger.js");
|
|
43
43
|
const AluviaApi_js_1 = require("../api/AluviaApi.js");
|
|
44
|
+
const BlockDetection_js_1 = require("./BlockDetection.js");
|
|
45
|
+
const net = __importStar(require("node:net"));
|
|
44
46
|
/**
|
|
45
47
|
* AluviaClient is the main entry point for the Aluvia Client.
|
|
46
48
|
*
|
|
47
49
|
* It manages the local proxy server and configuration polling.
|
|
48
50
|
*/
|
|
49
51
|
class AluviaClient {
|
|
52
|
+
/** Read-only access to the connection ID from ConfigManager. */
|
|
53
|
+
get connectionId() {
|
|
54
|
+
return this.configManager.connectionId;
|
|
55
|
+
}
|
|
50
56
|
constructor(options) {
|
|
51
57
|
this.connection = null;
|
|
52
58
|
this.started = false;
|
|
53
59
|
this.startPromise = null;
|
|
54
|
-
|
|
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();
|
|
55
65
|
if (!apiKey) {
|
|
56
|
-
throw new errors_js_1.MissingApiKeyError(
|
|
66
|
+
throw new errors_js_1.MissingApiKeyError("Aluvia apiKey is required");
|
|
57
67
|
}
|
|
58
|
-
const localProxy = options.localProxy ?? true;
|
|
59
68
|
const strict = options.strict ?? true;
|
|
60
|
-
this.options = { ...options, apiKey,
|
|
61
|
-
const connectionId = Number(options.connectionId)
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
64
76
|
const timeoutMs = options.timeoutMs;
|
|
65
|
-
const gatewayProtocol = options.gatewayProtocol ??
|
|
66
|
-
const gatewayPort = options.gatewayPort ?? (gatewayProtocol ===
|
|
67
|
-
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";
|
|
68
80
|
this.logger = new logger_js_1.Logger(logLevel);
|
|
69
81
|
// Create ConfigManager
|
|
70
82
|
this.configManager = new ConfigManager_js_1.ConfigManager({
|
|
@@ -84,13 +96,233 @@ class AluviaClient {
|
|
|
84
96
|
apiBaseUrl,
|
|
85
97
|
timeoutMs,
|
|
86
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
|
+
}
|
|
87
320
|
}
|
|
88
321
|
/**
|
|
89
322
|
* Start the Aluvia Client connection:
|
|
90
323
|
* - Fetch initial account connection config from Aluvia.
|
|
91
324
|
* - Start polling for config updates.
|
|
92
|
-
* -
|
|
93
|
-
* - 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>.
|
|
94
326
|
*
|
|
95
327
|
* Returns the active connection with host/port/url and a stop() method.
|
|
96
328
|
*/
|
|
@@ -104,55 +336,27 @@ class AluviaClient {
|
|
|
104
336
|
return this.startPromise;
|
|
105
337
|
}
|
|
106
338
|
this.startPromise = (async () => {
|
|
107
|
-
const localProxyEnabled = this.options.localProxy === true;
|
|
108
339
|
// Fetch initial configuration (may throw InvalidApiKeyError or ApiError)
|
|
109
340
|
await this.configManager.init();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Store the chromium module for now, will launch after proxy is ready
|
|
118
|
-
browserInstance = pw.chromium;
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
throw new errors_js_1.ApiError(`Failed to load Playwright. Make sure 'playwright' is installed: ${error.message}`, 500);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// Gateway mode cannot function without proxy credentials/config, so fail fast.
|
|
125
|
-
if (!localProxyEnabled && !this.configManager.getConfig()) {
|
|
126
|
-
throw new errors_js_1.ApiError('Failed to load account connection config; cannot start in gateway mode without proxy credentials', 500);
|
|
127
|
-
}
|
|
128
|
-
if (!localProxyEnabled) {
|
|
129
|
-
this.logger.debug('localProxy disabled — local proxy will not start');
|
|
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);
|
|
130
348
|
let nodeAgents = null;
|
|
131
349
|
let undiciDispatcher = null;
|
|
132
350
|
let undiciFetchFn = null;
|
|
133
|
-
const cfgAtStart = this.configManager.getConfig();
|
|
134
|
-
const serverUrlAtStart = (() => {
|
|
135
|
-
if (!cfgAtStart)
|
|
136
|
-
return '';
|
|
137
|
-
const { protocol, host, port } = cfgAtStart.rawProxy;
|
|
138
|
-
return `${protocol}://${host}:${port}`;
|
|
139
|
-
})();
|
|
140
|
-
const getProxyUrlForHttpClients = () => {
|
|
141
|
-
const cfg = this.configManager.getConfig();
|
|
142
|
-
if (!cfg)
|
|
143
|
-
return 'http://127.0.0.1';
|
|
144
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
145
|
-
return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
146
|
-
};
|
|
147
351
|
const getNodeAgents = () => {
|
|
148
352
|
if (!nodeAgents) {
|
|
149
|
-
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(
|
|
353
|
+
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(url);
|
|
150
354
|
}
|
|
151
355
|
return nodeAgents;
|
|
152
356
|
};
|
|
153
357
|
const getUndiciDispatcher = () => {
|
|
154
358
|
if (!undiciDispatcher) {
|
|
155
|
-
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(
|
|
359
|
+
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(url);
|
|
156
360
|
}
|
|
157
361
|
return undiciDispatcher;
|
|
158
362
|
};
|
|
@@ -161,10 +365,10 @@ class AluviaClient {
|
|
|
161
365
|
if (!d)
|
|
162
366
|
return;
|
|
163
367
|
try {
|
|
164
|
-
if (typeof d.close ===
|
|
368
|
+
if (typeof d.close === "function") {
|
|
165
369
|
await d.close();
|
|
166
370
|
}
|
|
167
|
-
else if (typeof d.destroy ===
|
|
371
|
+
else if (typeof d.destroy === "function") {
|
|
168
372
|
d.destroy();
|
|
169
373
|
}
|
|
170
374
|
}
|
|
@@ -173,6 +377,7 @@ class AluviaClient {
|
|
|
173
377
|
}
|
|
174
378
|
};
|
|
175
379
|
const stop = async () => {
|
|
380
|
+
await this.proxyServer.stop();
|
|
176
381
|
this.configManager.stopPolling();
|
|
177
382
|
nodeAgents?.http?.destroy?.();
|
|
178
383
|
nodeAgents?.https?.destroy?.();
|
|
@@ -184,63 +389,44 @@ class AluviaClient {
|
|
|
184
389
|
};
|
|
185
390
|
// Launch browser if Playwright was requested
|
|
186
391
|
let launchedBrowser = undefined;
|
|
392
|
+
let launchedBrowserContext = undefined;
|
|
393
|
+
let browserCdpUrl;
|
|
187
394
|
if (browserInstance) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
412
|
}
|
|
413
|
+
launchedBrowserContext = await launchedBrowser.newContext();
|
|
414
|
+
// Attach block detection
|
|
415
|
+
this.attachBlockDetectionListener(launchedBrowserContext);
|
|
200
416
|
}
|
|
201
417
|
const stopWithBrowser = async () => {
|
|
202
|
-
if (launchedBrowser)
|
|
418
|
+
if (launchedBrowser)
|
|
203
419
|
await launchedBrowser.close();
|
|
204
|
-
}
|
|
205
420
|
await stop();
|
|
206
421
|
};
|
|
207
422
|
// Build connection object
|
|
208
423
|
const connection = {
|
|
209
|
-
host
|
|
210
|
-
port
|
|
211
|
-
url
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return '';
|
|
216
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
217
|
-
return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
218
|
-
},
|
|
219
|
-
asPlaywright: () => {
|
|
220
|
-
const cfg = this.configManager.getConfig();
|
|
221
|
-
if (!cfg)
|
|
222
|
-
return { server: '' };
|
|
223
|
-
const { protocol, host, port, username, password } = cfg.rawProxy;
|
|
224
|
-
return {
|
|
225
|
-
...(0, adapters_js_1.toPlaywrightProxySettings)(`${protocol}://${host}:${port}`),
|
|
226
|
-
username,
|
|
227
|
-
password,
|
|
228
|
-
};
|
|
229
|
-
},
|
|
230
|
-
asPuppeteer: () => {
|
|
231
|
-
const cfg = this.configManager.getConfig();
|
|
232
|
-
if (!cfg)
|
|
233
|
-
return [];
|
|
234
|
-
const { protocol, host, port } = cfg.rawProxy;
|
|
235
|
-
return (0, adapters_js_1.toPuppeteerArgs)(`${protocol}://${host}:${port}`);
|
|
236
|
-
},
|
|
237
|
-
asSelenium: () => {
|
|
238
|
-
const cfg = this.configManager.getConfig();
|
|
239
|
-
if (!cfg)
|
|
240
|
-
return "";
|
|
241
|
-
const { protocol, host, port } = cfg.rawProxy;
|
|
242
|
-
return (0, adapters_js_1.toSeleniumArgs)(`${protocol}://${host}:${port}`);
|
|
243
|
-
},
|
|
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),
|
|
244
430
|
asNodeAgents: () => getNodeAgents(),
|
|
245
431
|
asAxiosConfig: () => (0, adapters_js_1.toAxiosConfig)(getNodeAgents()),
|
|
246
432
|
asGotOptions: () => (0, adapters_js_1.toGotOptions)(getNodeAgents()),
|
|
@@ -252,6 +438,8 @@ class AluviaClient {
|
|
|
252
438
|
return undiciFetchFn;
|
|
253
439
|
},
|
|
254
440
|
browser: launchedBrowser,
|
|
441
|
+
browserContext: launchedBrowserContext,
|
|
442
|
+
cdpUrl: browserCdpUrl,
|
|
255
443
|
stop: stopWithBrowser,
|
|
256
444
|
close: stopWithBrowser,
|
|
257
445
|
};
|
|
@@ -259,92 +447,10 @@ class AluviaClient {
|
|
|
259
447
|
this.started = true;
|
|
260
448
|
return connection;
|
|
261
449
|
}
|
|
262
|
-
|
|
263
|
-
this.configManager.startPolling();
|
|
264
|
-
// localProxy === true
|
|
265
|
-
const { host, port, url } = await this.proxyServer.start(this.options.localPort);
|
|
266
|
-
let nodeAgents = null;
|
|
267
|
-
let undiciDispatcher = null;
|
|
268
|
-
let undiciFetchFn = null;
|
|
269
|
-
const getNodeAgents = () => {
|
|
270
|
-
if (!nodeAgents) {
|
|
271
|
-
nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(url);
|
|
272
|
-
}
|
|
273
|
-
return nodeAgents;
|
|
274
|
-
};
|
|
275
|
-
const getUndiciDispatcher = () => {
|
|
276
|
-
if (!undiciDispatcher) {
|
|
277
|
-
undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(url);
|
|
278
|
-
}
|
|
279
|
-
return undiciDispatcher;
|
|
280
|
-
};
|
|
281
|
-
const closeUndiciDispatcher = async () => {
|
|
282
|
-
const d = undiciDispatcher;
|
|
283
|
-
if (!d)
|
|
284
|
-
return;
|
|
285
|
-
try {
|
|
286
|
-
if (typeof d.close === 'function') {
|
|
287
|
-
await d.close();
|
|
288
|
-
}
|
|
289
|
-
else if (typeof d.destroy === 'function') {
|
|
290
|
-
d.destroy();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
finally {
|
|
294
|
-
undiciDispatcher = null;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
const stop = async () => {
|
|
298
|
-
await this.proxyServer.stop();
|
|
450
|
+
catch (err) {
|
|
299
451
|
this.configManager.stopPolling();
|
|
300
|
-
|
|
301
|
-
nodeAgents?.https?.destroy?.();
|
|
302
|
-
nodeAgents = null;
|
|
303
|
-
await closeUndiciDispatcher();
|
|
304
|
-
undiciFetchFn = null;
|
|
305
|
-
this.connection = null;
|
|
306
|
-
this.started = false;
|
|
307
|
-
};
|
|
308
|
-
// Launch browser if Playwright was requested
|
|
309
|
-
let launchedBrowser = undefined;
|
|
310
|
-
if (browserInstance) {
|
|
311
|
-
const proxySettings = (0, adapters_js_1.toPlaywrightProxySettings)(url);
|
|
312
|
-
launchedBrowser = await browserInstance.launch({
|
|
313
|
-
proxy: proxySettings,
|
|
314
|
-
});
|
|
452
|
+
throw err;
|
|
315
453
|
}
|
|
316
|
-
const stopWithBrowser = async () => {
|
|
317
|
-
if (launchedBrowser) {
|
|
318
|
-
await launchedBrowser.close();
|
|
319
|
-
}
|
|
320
|
-
await stop();
|
|
321
|
-
};
|
|
322
|
-
// Build connection object
|
|
323
|
-
const connection = {
|
|
324
|
-
host,
|
|
325
|
-
port,
|
|
326
|
-
url,
|
|
327
|
-
getUrl: () => url,
|
|
328
|
-
asPlaywright: () => (0, adapters_js_1.toPlaywrightProxySettings)(url),
|
|
329
|
-
asPuppeteer: () => (0, adapters_js_1.toPuppeteerArgs)(url),
|
|
330
|
-
asSelenium: () => (0, adapters_js_1.toSeleniumArgs)(url),
|
|
331
|
-
asNodeAgents: () => getNodeAgents(),
|
|
332
|
-
asAxiosConfig: () => (0, adapters_js_1.toAxiosConfig)(getNodeAgents()),
|
|
333
|
-
asGotOptions: () => (0, adapters_js_1.toGotOptions)(getNodeAgents()),
|
|
334
|
-
asUndiciDispatcher: () => getUndiciDispatcher(),
|
|
335
|
-
asUndiciFetch: () => {
|
|
336
|
-
if (!undiciFetchFn) {
|
|
337
|
-
undiciFetchFn = (0, adapters_js_1.createUndiciFetch)(getUndiciDispatcher());
|
|
338
|
-
}
|
|
339
|
-
return undiciFetchFn;
|
|
340
|
-
},
|
|
341
|
-
browser: launchedBrowser,
|
|
342
|
-
stop: stopWithBrowser,
|
|
343
|
-
close: stopWithBrowser,
|
|
344
|
-
};
|
|
345
|
-
this.connection = connection;
|
|
346
|
-
this.started = true;
|
|
347
|
-
return connection;
|
|
348
454
|
})();
|
|
349
455
|
try {
|
|
350
456
|
return await this.startPromise;
|
|
@@ -371,13 +477,16 @@ class AluviaClient {
|
|
|
371
477
|
if (!this.started) {
|
|
372
478
|
return;
|
|
373
479
|
}
|
|
374
|
-
|
|
375
|
-
|
|
480
|
+
if (this.connection) {
|
|
481
|
+
await this.connection.close();
|
|
482
|
+
// connection.close() sets this.connection = null and this.started = false
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
376
485
|
await this.proxyServer.stop();
|
|
486
|
+
this.configManager.stopPolling();
|
|
487
|
+
this.connection = null;
|
|
488
|
+
this.started = false;
|
|
377
489
|
}
|
|
378
|
-
this.configManager.stopPolling();
|
|
379
|
-
this.connection = null;
|
|
380
|
-
this.started = false;
|
|
381
490
|
}
|
|
382
491
|
/**
|
|
383
492
|
* Update the filtering rules used by the proxy.
|
|
@@ -404,7 +513,74 @@ class AluviaClient {
|
|
|
404
513
|
return;
|
|
405
514
|
}
|
|
406
515
|
const trimmed = targetGeo.trim();
|
|
407
|
-
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
|
+
});
|
|
408
584
|
}
|
|
409
585
|
}
|
|
410
586
|
exports.AluviaClient = AluviaClient;
|