@aluvia/sdk 1.0.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +423 -0
  3. package/dist/cjs/api/AluviaApi.js +51 -0
  4. package/dist/cjs/api/account.js +155 -0
  5. package/dist/cjs/api/geos.js +76 -0
  6. package/dist/cjs/api/request.js +84 -0
  7. package/dist/cjs/api/types.js +2 -0
  8. package/dist/cjs/client/AluviaClient.js +325 -0
  9. package/dist/cjs/client/ConfigManager.js +303 -0
  10. package/dist/cjs/client/ProxyServer.js +182 -0
  11. package/dist/cjs/client/adapters.js +49 -0
  12. package/dist/cjs/client/logger.js +52 -0
  13. package/dist/cjs/client/rules.js +128 -0
  14. package/dist/cjs/client/types.js +3 -0
  15. package/dist/cjs/errors.js +49 -0
  16. package/dist/cjs/index.js +16 -0
  17. package/dist/cjs/package.json +1 -0
  18. package/dist/esm/api/AluviaApi.js +47 -0
  19. package/dist/esm/api/account.js +152 -0
  20. package/dist/esm/api/geos.js +73 -0
  21. package/dist/esm/api/request.js +81 -0
  22. package/dist/esm/api/types.js +1 -0
  23. package/dist/esm/client/AluviaClient.js +321 -0
  24. package/dist/esm/client/ConfigManager.js +299 -0
  25. package/dist/esm/client/ProxyServer.js +178 -0
  26. package/dist/esm/client/adapters.js +39 -0
  27. package/dist/esm/client/logger.js +48 -0
  28. package/dist/esm/client/rules.js +124 -0
  29. package/dist/esm/client/types.js +2 -0
  30. package/dist/esm/errors.js +42 -0
  31. package/dist/esm/index.js +7 -0
  32. package/dist/types/api/AluviaApi.d.ts +29 -0
  33. package/dist/types/api/account.d.ts +41 -0
  34. package/dist/types/api/geos.d.ts +5 -0
  35. package/dist/types/api/request.d.ts +20 -0
  36. package/dist/types/api/types.d.ts +30 -0
  37. package/dist/types/client/AluviaClient.d.ts +50 -0
  38. package/dist/types/client/ConfigManager.d.ts +100 -0
  39. package/dist/types/client/ProxyServer.d.ts +47 -0
  40. package/dist/types/client/adapters.d.ts +26 -0
  41. package/dist/types/client/logger.d.ts +33 -0
  42. package/dist/types/client/rules.d.ts +34 -0
  43. package/dist/types/client/types.d.ts +194 -0
  44. package/dist/types/errors.d.ts +25 -0
  45. package/dist/types/index.d.ts +5 -0
  46. package/package.json +65 -0
@@ -0,0 +1,321 @@
1
+ // AluviaClient - Main public class for Aluvia Client
2
+ import { ConfigManager } from './ConfigManager.js';
3
+ import { ProxyServer } from './ProxyServer.js';
4
+ import { ApiError, MissingApiKeyError } from '../errors.js';
5
+ import { createNodeProxyAgents, createUndiciDispatcher, createUndiciFetch, toAxiosConfig, toGotOptions, toPlaywrightProxySettings, toPuppeteerArgs, toSeleniumArgs, } from './adapters.js';
6
+ import { Logger } from './logger.js';
7
+ import { AluviaApi } from '../api/AluviaApi.js';
8
+ /**
9
+ * AluviaClient is the main entry point for the Aluvia Client.
10
+ *
11
+ * It manages the local proxy server and configuration polling.
12
+ */
13
+ export class AluviaClient {
14
+ constructor(options) {
15
+ this.connection = null;
16
+ this.started = false;
17
+ this.startPromise = null;
18
+ const apiKey = String(options.apiKey ?? '').trim();
19
+ if (!apiKey) {
20
+ throw new MissingApiKeyError('Aluvia apiKey is required');
21
+ }
22
+ const localProxy = options.localProxy ?? true;
23
+ const strict = options.strict ?? true;
24
+ this.options = { ...options, apiKey, localProxy, strict };
25
+ const connectionId = Number(options.connectionId) ?? null;
26
+ const apiBaseUrl = options.apiBaseUrl ?? 'https://api.aluvia.io/v1';
27
+ const pollIntervalMs = options.pollIntervalMs ?? 5000;
28
+ const timeoutMs = options.timeoutMs;
29
+ const gatewayProtocol = options.gatewayProtocol ?? 'http';
30
+ const gatewayPort = options.gatewayPort ?? (gatewayProtocol === 'https' ? 8443 : 8080);
31
+ const logLevel = options.logLevel ?? 'info';
32
+ this.logger = new Logger(logLevel);
33
+ // Create ConfigManager
34
+ this.configManager = new ConfigManager({
35
+ apiKey,
36
+ apiBaseUrl,
37
+ pollIntervalMs,
38
+ gatewayProtocol,
39
+ gatewayPort,
40
+ logLevel,
41
+ connectionId,
42
+ strict,
43
+ });
44
+ // Create ProxyServer
45
+ this.proxyServer = new ProxyServer(this.configManager, { logLevel });
46
+ this.api = new AluviaApi({
47
+ apiKey,
48
+ apiBaseUrl,
49
+ timeoutMs,
50
+ });
51
+ }
52
+ /**
53
+ * Start the Aluvia Client connection:
54
+ * - Fetch initial account connection config from Aluvia.
55
+ * - Start polling for config updates.
56
+ * - If localProxy is enabled (default): start a local HTTP proxy on 127.0.0.1:<localPort or free port>.
57
+ * - If localProxy is disabled: do NOT start a local proxy; adapters use gateway proxy settings.
58
+ *
59
+ * Returns the active connection with host/port/url and a stop() method.
60
+ */
61
+ async start() {
62
+ // Return existing connection if already started
63
+ if (this.started && this.connection) {
64
+ return this.connection;
65
+ }
66
+ // If a start is already in-flight, await it.
67
+ if (this.startPromise) {
68
+ return this.startPromise;
69
+ }
70
+ this.startPromise = (async () => {
71
+ const localProxyEnabled = this.options.localProxy === true;
72
+ // Fetch initial configuration (may throw InvalidApiKeyError or ApiError)
73
+ await this.configManager.init();
74
+ // Gateway mode cannot function without proxy credentials/config, so fail fast.
75
+ if (!localProxyEnabled && !this.configManager.getConfig()) {
76
+ throw new ApiError('Failed to load account connection config; cannot start in gateway mode without proxy credentials', 500);
77
+ }
78
+ if (!localProxyEnabled) {
79
+ this.logger.debug('localProxy disabled — local proxy will not start');
80
+ let nodeAgents = null;
81
+ let undiciDispatcher = null;
82
+ let undiciFetchFn = null;
83
+ const cfgAtStart = this.configManager.getConfig();
84
+ const serverUrlAtStart = (() => {
85
+ if (!cfgAtStart)
86
+ return '';
87
+ const { protocol, host, port } = cfgAtStart.rawProxy;
88
+ return `${protocol}://${host}:${port}`;
89
+ })();
90
+ const getProxyUrlForHttpClients = () => {
91
+ const cfg = this.configManager.getConfig();
92
+ if (!cfg)
93
+ return 'http://127.0.0.1';
94
+ const { protocol, host, port, username, password } = cfg.rawProxy;
95
+ return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
96
+ };
97
+ const getNodeAgents = () => {
98
+ if (!nodeAgents) {
99
+ nodeAgents = createNodeProxyAgents(getProxyUrlForHttpClients());
100
+ }
101
+ return nodeAgents;
102
+ };
103
+ const getUndiciDispatcher = () => {
104
+ if (!undiciDispatcher) {
105
+ undiciDispatcher = createUndiciDispatcher(getProxyUrlForHttpClients());
106
+ }
107
+ return undiciDispatcher;
108
+ };
109
+ const closeUndiciDispatcher = async () => {
110
+ const d = undiciDispatcher;
111
+ if (!d)
112
+ return;
113
+ try {
114
+ if (typeof d.close === 'function') {
115
+ await d.close();
116
+ }
117
+ else if (typeof d.destroy === 'function') {
118
+ d.destroy();
119
+ }
120
+ }
121
+ finally {
122
+ undiciDispatcher = null;
123
+ }
124
+ };
125
+ const stop = async () => {
126
+ this.configManager.stopPolling();
127
+ nodeAgents?.http?.destroy?.();
128
+ nodeAgents?.https?.destroy?.();
129
+ nodeAgents = null;
130
+ await closeUndiciDispatcher();
131
+ undiciFetchFn = null;
132
+ this.connection = null;
133
+ this.started = false;
134
+ };
135
+ // Build connection object
136
+ const connection = {
137
+ host: cfgAtStart?.rawProxy.host ?? '127.0.0.1',
138
+ port: cfgAtStart?.rawProxy.port ?? 0,
139
+ url: serverUrlAtStart,
140
+ getUrl: () => {
141
+ const cfg = this.configManager.getConfig();
142
+ if (!cfg)
143
+ return '';
144
+ const { protocol, host, port, username, password } = cfg.rawProxy;
145
+ return `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
146
+ },
147
+ asPlaywright: () => {
148
+ const cfg = this.configManager.getConfig();
149
+ if (!cfg)
150
+ return { server: '' };
151
+ const { protocol, host, port, username, password } = cfg.rawProxy;
152
+ return {
153
+ ...toPlaywrightProxySettings(`${protocol}://${host}:${port}`),
154
+ username,
155
+ password,
156
+ };
157
+ },
158
+ asPuppeteer: () => {
159
+ const cfg = this.configManager.getConfig();
160
+ if (!cfg)
161
+ return [];
162
+ const { protocol, host, port } = cfg.rawProxy;
163
+ return toPuppeteerArgs(`${protocol}://${host}:${port}`);
164
+ },
165
+ asSelenium: () => {
166
+ const cfg = this.configManager.getConfig();
167
+ if (!cfg)
168
+ return '';
169
+ const { protocol, host, port } = cfg.rawProxy;
170
+ return toSeleniumArgs(`${protocol}://${host}:${port}`);
171
+ },
172
+ asNodeAgents: () => getNodeAgents(),
173
+ asAxiosConfig: () => toAxiosConfig(getNodeAgents()),
174
+ asGotOptions: () => toGotOptions(getNodeAgents()),
175
+ asUndiciDispatcher: () => getUndiciDispatcher(),
176
+ asUndiciFetch: () => {
177
+ if (!undiciFetchFn) {
178
+ undiciFetchFn = createUndiciFetch(getUndiciDispatcher());
179
+ }
180
+ return undiciFetchFn;
181
+ },
182
+ stop,
183
+ close: stop,
184
+ };
185
+ this.connection = connection;
186
+ this.started = true;
187
+ return connection;
188
+ }
189
+ // In client proxy mode, keep config fresh so routing decisions update without restarting.
190
+ this.configManager.startPolling();
191
+ // localProxy === true
192
+ const { host, port, url } = await this.proxyServer.start(this.options.localPort);
193
+ let nodeAgents = null;
194
+ let undiciDispatcher = null;
195
+ let undiciFetchFn = null;
196
+ const getNodeAgents = () => {
197
+ if (!nodeAgents) {
198
+ nodeAgents = createNodeProxyAgents(url);
199
+ }
200
+ return nodeAgents;
201
+ };
202
+ const getUndiciDispatcher = () => {
203
+ if (!undiciDispatcher) {
204
+ undiciDispatcher = createUndiciDispatcher(url);
205
+ }
206
+ return undiciDispatcher;
207
+ };
208
+ const closeUndiciDispatcher = async () => {
209
+ const d = undiciDispatcher;
210
+ if (!d)
211
+ return;
212
+ try {
213
+ if (typeof d.close === 'function') {
214
+ await d.close();
215
+ }
216
+ else if (typeof d.destroy === 'function') {
217
+ d.destroy();
218
+ }
219
+ }
220
+ finally {
221
+ undiciDispatcher = null;
222
+ }
223
+ };
224
+ const stop = async () => {
225
+ await this.proxyServer.stop();
226
+ this.configManager.stopPolling();
227
+ nodeAgents?.http?.destroy?.();
228
+ nodeAgents?.https?.destroy?.();
229
+ nodeAgents = null;
230
+ await closeUndiciDispatcher();
231
+ undiciFetchFn = null;
232
+ this.connection = null;
233
+ this.started = false;
234
+ };
235
+ // Build connection object
236
+ const connection = {
237
+ host,
238
+ port,
239
+ url,
240
+ getUrl: () => url,
241
+ asPlaywright: () => toPlaywrightProxySettings(url),
242
+ asPuppeteer: () => toPuppeteerArgs(url),
243
+ asSelenium: () => toSeleniumArgs(url),
244
+ asNodeAgents: () => getNodeAgents(),
245
+ asAxiosConfig: () => toAxiosConfig(getNodeAgents()),
246
+ asGotOptions: () => toGotOptions(getNodeAgents()),
247
+ asUndiciDispatcher: () => getUndiciDispatcher(),
248
+ asUndiciFetch: () => {
249
+ if (!undiciFetchFn) {
250
+ undiciFetchFn = createUndiciFetch(getUndiciDispatcher());
251
+ }
252
+ return undiciFetchFn;
253
+ },
254
+ stop,
255
+ close: stop,
256
+ };
257
+ this.connection = connection;
258
+ this.started = true;
259
+ return connection;
260
+ })();
261
+ try {
262
+ return await this.startPromise;
263
+ }
264
+ finally {
265
+ this.startPromise = null;
266
+ }
267
+ }
268
+ /**
269
+ * Global cleanup:
270
+ * - Stop the local proxy server (if running).
271
+ * - Stop config polling.
272
+ */
273
+ async stop() {
274
+ // If start is in-flight, wait for it to settle so we don't leave a running proxy behind.
275
+ if (this.startPromise) {
276
+ try {
277
+ await this.startPromise;
278
+ }
279
+ catch {
280
+ // ignore startup errors; if startup failed there is nothing to stop
281
+ }
282
+ }
283
+ if (!this.started) {
284
+ return;
285
+ }
286
+ // Only stop proxy if it was potentially started.
287
+ if (this.options.localProxy) {
288
+ await this.proxyServer.stop();
289
+ }
290
+ this.configManager.stopPolling();
291
+ this.connection = null;
292
+ this.started = false;
293
+ }
294
+ /**
295
+ * Update the filtering rules used by the proxy.
296
+ * @param rules
297
+ */
298
+ async updateRules(rules) {
299
+ await this.configManager.setConfig({ rules: rules });
300
+ }
301
+ /**
302
+ * Update the upstream session_id.
303
+ * @param sessionId
304
+ */
305
+ async updateSessionId(sessionId) {
306
+ await this.configManager.setConfig({ session_id: sessionId });
307
+ }
308
+ /**
309
+ * Update the upstream target_geo (geo targeting).
310
+ *
311
+ * Pass null to clear geo targeting.
312
+ */
313
+ async updateTargetGeo(targetGeo) {
314
+ if (targetGeo === null) {
315
+ await this.configManager.setConfig({ target_geo: null });
316
+ return;
317
+ }
318
+ const trimmed = targetGeo.trim();
319
+ await this.configManager.setConfig({ target_geo: trimmed.length > 0 ? trimmed : null });
320
+ }
321
+ }
@@ -0,0 +1,299 @@
1
+ // ConfigManager - Control plane for connection configuration
2
+ import { Logger } from './logger.js';
3
+ import { InvalidApiKeyError, ApiError } from '../errors.js';
4
+ import { requestCore } from '../api/request.js';
5
+ function isRecord(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ function toAccountConnectionApiResponse(value) {
9
+ if (!isRecord(value))
10
+ return {};
11
+ const data = value['data'];
12
+ if (!isRecord(data))
13
+ return {};
14
+ return { data: data };
15
+ }
16
+ function toValidationErrors(value) {
17
+ if (!isRecord(value))
18
+ return null;
19
+ const apiError = value['error'];
20
+ if (!isRecord(apiError))
21
+ return null;
22
+ if (apiError['code'] !== 'validation_error')
23
+ return null;
24
+ const details = apiError['details'];
25
+ const errors = [];
26
+ if (isRecord(details)) {
27
+ for (const fieldMessages of Object.values(details)) {
28
+ if (Array.isArray(fieldMessages)) {
29
+ for (const message of fieldMessages) {
30
+ if (typeof message === 'string') {
31
+ errors.push(message);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return errors.length ? errors : null;
38
+ }
39
+ /**
40
+ * ConfigManager handles fetching and maintaining connection configuration from the Aluvia API.
41
+ *
42
+ * Responsibilities:
43
+ * - Initial fetch of account connection config
44
+ * - Polling for updates using ETag
45
+ * - Providing current config to ProxyServer
46
+ */
47
+ export class ConfigManager {
48
+ constructor(options) {
49
+ this.config = null;
50
+ this.timer = null;
51
+ this.pollInFlight = false;
52
+ this.options = options;
53
+ this.logger = new Logger(options.logLevel);
54
+ this.strict = options.strict ?? true;
55
+ }
56
+ /**
57
+ * Fetch initial configuration from the account connections API.
58
+ * Must be called before starting the proxy.
59
+ *
60
+ * @throws InvalidApiKeyError if apiKey is invalid (401/403)
61
+ * @throws ApiError for other API errors
62
+ */
63
+ async init() {
64
+ if (this.options.connectionId) {
65
+ this.accountConnectionId = this.options.connectionId ?? null;
66
+ this.logger.info(`Using account connection API (connection id: ${this.accountConnectionId})`);
67
+ let result;
68
+ try {
69
+ result = await requestCore({
70
+ apiBaseUrl: this.options.apiBaseUrl,
71
+ apiKey: this.options.apiKey,
72
+ method: 'GET',
73
+ path: `/account/connections/${this.accountConnectionId}`,
74
+ });
75
+ }
76
+ catch (err) {
77
+ if (err instanceof ApiError)
78
+ throw err;
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ throw new ApiError(`Failed to fetch account connection config: ${msg}`);
81
+ }
82
+ if (result.status === 401 || result.status === 403) {
83
+ throw new InvalidApiKeyError(`Authentication failed with status ${result.status}`);
84
+ }
85
+ if (result.status === 200 && result.body) {
86
+ this.config = this.buildConfigFromAny(result.body, result.etag);
87
+ this.logger.info('Configuration loaded successfully');
88
+ this.logger.debug('Config summary:', this.redactConfig(this.config));
89
+ return;
90
+ }
91
+ throw new ApiError(`Failed to fetch account connection config: HTTP ${result.status}`, result.status);
92
+ }
93
+ // No connectionId: create an account connection (preferred)
94
+ this.logger.info('No connectionId provided; creating account connection...');
95
+ try {
96
+ const created = await requestCore({
97
+ apiBaseUrl: this.options.apiBaseUrl,
98
+ apiKey: this.options.apiKey,
99
+ method: 'POST',
100
+ path: '/account/connections',
101
+ body: {},
102
+ });
103
+ if (created.status === 401 || created.status === 403) {
104
+ throw new InvalidApiKeyError(`Authentication failed with status ${created.status}`);
105
+ }
106
+ if ((created.status === 200 || created.status === 201) && created.body) {
107
+ const createdResponse = toAccountConnectionApiResponse(created.body);
108
+ this.accountConnectionId = Number(createdResponse.data?.connection_id);
109
+ if (this.accountConnectionId != null) {
110
+ this.logger.info(`Account connection created (connection id: ${this.accountConnectionId})`);
111
+ }
112
+ else {
113
+ this.logger.info('Account connection created (connection id unavailable in response)');
114
+ }
115
+ this.config = this.buildConfigFromAny(created.body, created.etag);
116
+ this.logger.info('Configuration loaded successfully');
117
+ this.logger.debug('Config summary:', this.redactConfig(this.config));
118
+ return;
119
+ }
120
+ const msg = `Failed to create account connection config: HTTP ${created.status}`;
121
+ if (this.strict) {
122
+ throw new ApiError(msg, created.status);
123
+ }
124
+ this.logger.warn(`${msg}; continuing without config (strict=false)`);
125
+ return;
126
+ }
127
+ catch (err) {
128
+ if (err instanceof InvalidApiKeyError)
129
+ throw err;
130
+ if (err instanceof ApiError) {
131
+ if (this.strict)
132
+ throw err;
133
+ this.logger.warn('Create account connection failed; continuing without config (strict=false)', err);
134
+ return;
135
+ }
136
+ const msg = err instanceof Error ? err.message : String(err);
137
+ if (this.strict) {
138
+ throw new ApiError(`Failed to create account connection config: ${msg}`);
139
+ }
140
+ this.logger.warn('Create account connection failed; continuing without config (strict=false)', err);
141
+ return;
142
+ }
143
+ }
144
+ /**
145
+ * Start polling for configuration updates.
146
+ * Uses ETag for efficient conditional requests.
147
+ */
148
+ startPolling() {
149
+ // Don't start if already polling
150
+ if (this.timer) {
151
+ this.logger.debug('Polling already active, skipping startPolling()');
152
+ return;
153
+ }
154
+ this.logger.info(`Starting config polling every ${this.options.pollIntervalMs}ms`);
155
+ this.timer = setInterval(async () => {
156
+ if (this.pollInFlight) {
157
+ this.logger.debug('Previous poll still running, skipping this poll tick');
158
+ return;
159
+ }
160
+ this.pollInFlight = true;
161
+ try {
162
+ await this.pollOnce();
163
+ }
164
+ finally {
165
+ this.pollInFlight = false;
166
+ }
167
+ }, this.options.pollIntervalMs);
168
+ }
169
+ /**
170
+ * Stop polling for configuration updates.
171
+ */
172
+ stopPolling() {
173
+ if (this.timer) {
174
+ clearInterval(this.timer);
175
+ this.timer = null;
176
+ this.logger.info('Config polling stopped');
177
+ }
178
+ }
179
+ /**
180
+ * Get the current configuration.
181
+ * Returns null if init() hasn't been called or failed.
182
+ */
183
+ getConfig() {
184
+ return this.config;
185
+ }
186
+ async setConfig(body) {
187
+ this.logger.debug(`Setting config: ${JSON.stringify(body)}`);
188
+ let result;
189
+ try {
190
+ result = await requestCore({
191
+ apiBaseUrl: this.options.apiBaseUrl,
192
+ apiKey: this.options.apiKey,
193
+ method: 'PATCH',
194
+ path: `/account/connections/${this.accountConnectionId}`,
195
+ body,
196
+ });
197
+ }
198
+ catch (err) {
199
+ if (err instanceof ApiError)
200
+ throw err;
201
+ const msg = err instanceof Error ? err.message : String(err);
202
+ throw new ApiError(`Failed to update account connection config: ${msg}`);
203
+ }
204
+ if (result.status === 401 || result.status === 403) {
205
+ throw new InvalidApiKeyError(`Authentication failed with status ${result.status}`);
206
+ }
207
+ if (result.status === 200 && result.body) {
208
+ this.config = this.buildConfigFromAny(result.body, result.etag);
209
+ this.logger.debug('Configuration updated from API');
210
+ this.logger.debug('New config summary:', this.redactConfig(this.config));
211
+ return this.config;
212
+ }
213
+ if (result.status === 422 && result.body) {
214
+ const validationErrors = toValidationErrors(result.body);
215
+ if (validationErrors) {
216
+ throw new ApiError(`Failed to update account connection config: ${validationErrors[0]}`, result.status);
217
+ }
218
+ }
219
+ throw new ApiError(`Failed to update account connection config: HTTP ${result.status}`, result.status);
220
+ }
221
+ /**
222
+ * Perform a single poll iteration.
223
+ * Called by the polling timer.
224
+ */
225
+ async pollOnce() {
226
+ if (!this.config) {
227
+ this.logger.warn('No config available, skipping poll');
228
+ return;
229
+ }
230
+ try {
231
+ const result = await requestCore({
232
+ apiBaseUrl: this.options.apiBaseUrl,
233
+ apiKey: this.options.apiKey,
234
+ method: 'GET',
235
+ path: `/account/connections/${this.accountConnectionId}`,
236
+ ifNoneMatch: this.config.etag,
237
+ });
238
+ if (result.status === 304) {
239
+ this.logger.debug('Config unchanged (304 Not Modified)');
240
+ return;
241
+ }
242
+ if (result.status === 200 && result.body) {
243
+ this.config = this.buildConfigFromAny(result.body, result.etag);
244
+ this.logger.debug('Configuration updated from API');
245
+ this.logger.debug('New config summary:', this.redactConfig(this.config));
246
+ return;
247
+ }
248
+ this.logger.warn(`Poll returned unexpected status ${result.status}`);
249
+ }
250
+ catch (error) {
251
+ this.logger.warn('Poll failed, keeping existing config:', error);
252
+ }
253
+ }
254
+ /**
255
+ * Build ConnectionNetworkConfig from API response.
256
+ */
257
+ buildConfigFromAny(body, etag) {
258
+ const response = toAccountConnectionApiResponse(body);
259
+ const data = response.data;
260
+ const rules = Array.isArray(data?.rules) ? data.rules : [];
261
+ const sessionId = data?.session_id ?? null;
262
+ const targetGeo = data?.target_geo ?? null;
263
+ const username = data?.proxy_username ?? null;
264
+ const password = data?.proxy_password ?? null;
265
+ if (!username || !password) {
266
+ throw new ApiError('Account connection response missing proxy credentials (data.proxy_username and data.proxy_password are required)', 500);
267
+ }
268
+ return {
269
+ rawProxy: {
270
+ protocol: this.options.gatewayProtocol,
271
+ host: 'gateway.aluvia.io',
272
+ port: this.options.gatewayPort,
273
+ username,
274
+ password,
275
+ },
276
+ rules,
277
+ sessionId,
278
+ targetGeo,
279
+ etag,
280
+ };
281
+ }
282
+ redactConfig(config) {
283
+ if (!config)
284
+ return null;
285
+ return {
286
+ rulesCount: config.rules?.length ?? 0,
287
+ sessionId: config.sessionId,
288
+ targetGeo: config.targetGeo,
289
+ etag: config.etag,
290
+ rawProxy: {
291
+ protocol: config.rawProxy.protocol,
292
+ host: config.rawProxy.host,
293
+ port: config.rawProxy.port,
294
+ username: config.rawProxy.username ? '[set]' : '[missing]',
295
+ password: config.rawProxy.password ? '[set]' : '[missing]',
296
+ },
297
+ };
298
+ }
299
+ }