@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.
Files changed (77) hide show
  1. package/README.md +409 -285
  2. package/dist/cjs/api/account.js +10 -74
  3. package/dist/cjs/api/apiUtils.js +80 -0
  4. package/dist/cjs/api/geos.js +2 -63
  5. package/dist/cjs/api/request.js +8 -2
  6. package/dist/cjs/bin/account.js +31 -0
  7. package/dist/cjs/bin/api-helpers.js +58 -0
  8. package/dist/cjs/bin/cli.js +245 -0
  9. package/dist/cjs/bin/close.js +120 -0
  10. package/dist/cjs/bin/geos.js +10 -0
  11. package/dist/cjs/bin/mcp-helpers.js +57 -0
  12. package/dist/cjs/bin/mcp-server.js +220 -0
  13. package/dist/cjs/bin/mcp-tools.js +90 -0
  14. package/dist/cjs/bin/open.js +293 -0
  15. package/dist/cjs/bin/session.js +259 -0
  16. package/dist/cjs/client/AluviaClient.js +365 -189
  17. package/dist/cjs/client/BlockDetection.js +486 -0
  18. package/dist/cjs/client/ConfigManager.js +26 -23
  19. package/dist/cjs/client/PageLoadDetection.js +175 -0
  20. package/dist/cjs/client/ProxyServer.js +4 -2
  21. package/dist/cjs/client/logger.js +4 -0
  22. package/dist/cjs/client/rules.js +38 -49
  23. package/dist/cjs/connect.js +117 -0
  24. package/dist/cjs/errors.js +12 -1
  25. package/dist/cjs/index.js +5 -1
  26. package/dist/cjs/session/lock.js +186 -0
  27. package/dist/esm/api/account.js +2 -66
  28. package/dist/esm/api/apiUtils.js +71 -0
  29. package/dist/esm/api/geos.js +2 -63
  30. package/dist/esm/api/request.js +8 -2
  31. package/dist/esm/bin/account.js +28 -0
  32. package/dist/esm/bin/api-helpers.js +53 -0
  33. package/dist/esm/bin/cli.js +242 -0
  34. package/dist/esm/bin/close.js +117 -0
  35. package/dist/esm/bin/geos.js +7 -0
  36. package/dist/esm/bin/mcp-helpers.js +51 -0
  37. package/dist/esm/bin/mcp-server.js +185 -0
  38. package/dist/esm/bin/mcp-tools.js +78 -0
  39. package/dist/esm/bin/open.js +256 -0
  40. package/dist/esm/bin/session.js +252 -0
  41. package/dist/esm/client/AluviaClient.js +371 -195
  42. package/dist/esm/client/BlockDetection.js +482 -0
  43. package/dist/esm/client/ConfigManager.js +21 -18
  44. package/dist/esm/client/PageLoadDetection.js +171 -0
  45. package/dist/esm/client/ProxyServer.js +5 -3
  46. package/dist/esm/client/logger.js +4 -0
  47. package/dist/esm/client/rules.js +36 -49
  48. package/dist/esm/connect.js +81 -0
  49. package/dist/esm/errors.js +10 -0
  50. package/dist/esm/index.js +5 -3
  51. package/dist/esm/session/lock.js +142 -0
  52. package/dist/types/api/AluviaApi.d.ts +2 -7
  53. package/dist/types/api/account.d.ts +1 -16
  54. package/dist/types/api/apiUtils.d.ts +28 -0
  55. package/dist/types/api/geos.d.ts +1 -1
  56. package/dist/types/bin/account.d.ts +1 -0
  57. package/dist/types/bin/api-helpers.d.ts +20 -0
  58. package/dist/types/bin/cli.d.ts +2 -0
  59. package/dist/types/bin/close.d.ts +1 -0
  60. package/dist/types/bin/geos.d.ts +1 -0
  61. package/dist/types/bin/mcp-helpers.d.ts +28 -0
  62. package/dist/types/bin/mcp-server.d.ts +2 -0
  63. package/dist/types/bin/mcp-tools.d.ts +46 -0
  64. package/dist/types/bin/open.d.ts +21 -0
  65. package/dist/types/bin/session.d.ts +11 -0
  66. package/dist/types/client/AluviaClient.d.ts +51 -4
  67. package/dist/types/client/BlockDetection.d.ts +96 -0
  68. package/dist/types/client/ConfigManager.d.ts +6 -1
  69. package/dist/types/client/PageLoadDetection.d.ts +93 -0
  70. package/dist/types/client/logger.d.ts +2 -0
  71. package/dist/types/client/rules.d.ts +18 -0
  72. package/dist/types/client/types.d.ts +48 -47
  73. package/dist/types/connect.d.ts +18 -0
  74. package/dist/types/errors.d.ts +6 -0
  75. package/dist/types/index.d.ts +7 -5
  76. package/dist/types/session/lock.d.ts +43 -0
  77. 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
- const apiKey = String(options.apiKey ?? '').trim();
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('Aluvia apiKey is required');
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, localProxy, strict };
61
- const connectionId = Number(options.connectionId) ?? null;
62
- const apiBaseUrl = options.apiBaseUrl ?? 'https://api.aluvia.io/v1';
63
- const pollIntervalMs = options.pollIntervalMs ?? 5000;
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 ?? 'http';
66
- const gatewayPort = options.gatewayPort ?? (gatewayProtocol === 'https' ? 8443 : 8080);
67
- const logLevel = options.logLevel ?? 'info';
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
- * - If localProxy is enabled (default): start a local HTTP proxy on 127.0.0.1:<localPort or free port>.
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
- // Initialize Playwright if requested
111
- let browserInstance = undefined;
112
- if (this.options.startPlaywright) {
113
- try {
114
- // @ts-expect-error - playwright is an optional peer dependency
115
- const pw = await Promise.resolve().then(() => __importStar(require('playwright')));
116
- // We need to launch the browser after we have proxy configuration
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)(getProxyUrlForHttpClients());
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)(getProxyUrlForHttpClients());
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 === 'function') {
368
+ if (typeof d.close === "function") {
165
369
  await d.close();
166
370
  }
167
- else if (typeof d.destroy === 'function') {
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 cfg = this.configManager.getConfig();
189
- if (cfg) {
190
- const { protocol, host, port, username, password } = cfg.rawProxy;
191
- const proxySettings = {
192
- ...(0, adapters_js_1.toPlaywrightProxySettings)(`${protocol}://${host}:${port}`),
193
- username,
194
- password,
195
- };
196
- launchedBrowser = await browserInstance.launch({
197
- proxy: proxySettings,
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: cfgAtStart?.rawProxy.host ?? '127.0.0.1',
210
- port: cfgAtStart?.rawProxy.port ?? 0,
211
- url: serverUrlAtStart,
212
- getUrl: () => {
213
- const cfg = this.configManager.getConfig();
214
- if (!cfg)
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
- // In client proxy mode, keep config fresh so routing decisions update without restarting.
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
- nodeAgents?.http?.destroy?.();
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
- // Only stop proxy if it was potentially started.
375
- if (this.options.localProxy) {
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({ target_geo: trimmed.length > 0 ? trimmed : null });
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;