@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.
Files changed (77) hide show
  1. package/README.md +425 -256
  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 +411 -150
  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 +384 -156
  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 +69 -46
  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 -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
- 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();
22
65
  if (!apiKey) {
23
- throw new errors_js_1.MissingApiKeyError('Aluvia apiKey is required');
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, localProxy, strict };
28
- const connectionId = Number(options.connectionId) ?? null;
29
- const apiBaseUrl = options.apiBaseUrl ?? 'https://api.aluvia.io/v1';
30
- 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);
31
76
  const timeoutMs = options.timeoutMs;
32
- const gatewayProtocol = options.gatewayProtocol ?? 'http';
33
- const gatewayPort = options.gatewayPort ?? (gatewayProtocol === 'https' ? 8443 : 8080);
34
- 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";
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
- * - If localProxy is enabled (default): start a local HTTP proxy on 127.0.0.1:<localPort or free port>.
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
- // Gateway mode cannot function without proxy credentials/config, so fail fast.
78
- if (!localProxyEnabled && !this.configManager.getConfig()) {
79
- throw new errors_js_1.ApiError('Failed to load account connection config; cannot start in gateway mode without proxy credentials', 500);
80
- }
81
- if (!localProxyEnabled) {
82
- 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);
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)(getProxyUrlForHttpClients());
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)(getProxyUrlForHttpClients());
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 === 'function') {
368
+ if (typeof d.close === "function") {
118
369
  await d.close();
119
370
  }
120
- else if (typeof d.destroy === 'function') {
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: cfgAtStart?.rawProxy.host ?? '127.0.0.1',
141
- port: cfgAtStart?.rawProxy.port ?? 0,
142
- url: serverUrlAtStart,
143
- getUrl: () => {
144
- const cfg = this.configManager.getConfig();
145
- if (!cfg)
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
- stop,
186
- close: stop,
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
- // In client proxy mode, keep config fresh so routing decisions update without restarting.
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
- nodeAgents?.http?.destroy?.();
231
- nodeAgents?.https?.destroy?.();
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
- // Only stop proxy if it was potentially started.
290
- 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 {
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({ 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
+ });
323
584
  }
324
585
  }
325
586
  exports.AluviaClient = AluviaClient;