@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,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGeosApi = createGeosApi;
4
+ const errors_js_1 = require("../errors.js");
5
+ function isRecord(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ function asErrorEnvelope(value) {
9
+ if (!isRecord(value))
10
+ return null;
11
+ if (value['success'] !== false)
12
+ return null;
13
+ const error = value['error'];
14
+ if (!isRecord(error))
15
+ return null;
16
+ const code = error['code'];
17
+ const message = error['message'];
18
+ if (typeof code !== 'string' || typeof message !== 'string')
19
+ return null;
20
+ return { success: false, error: { code, message, details: error['details'] } };
21
+ }
22
+ function formatErrorDetails(details) {
23
+ if (details == null)
24
+ return '';
25
+ try {
26
+ const json = JSON.stringify(details);
27
+ if (!json)
28
+ return '';
29
+ return json.length > 500 ? `${json.slice(0, 500)}…` : json;
30
+ }
31
+ catch {
32
+ return String(details);
33
+ }
34
+ }
35
+ function unwrapSuccessArray(value) {
36
+ if (!isRecord(value))
37
+ return null;
38
+ if (value['success'] === true && Array.isArray(value.data)) {
39
+ return value.data;
40
+ }
41
+ if (Array.isArray(value.data)) {
42
+ return value.data;
43
+ }
44
+ return null;
45
+ }
46
+ function throwForNon2xx(result) {
47
+ const status = result.status;
48
+ if (status === 401 || status === 403) {
49
+ throw new errors_js_1.InvalidApiKeyError(`Authentication failed (HTTP ${status}). Check token validity and that you are using an account API token for account endpoints.`);
50
+ }
51
+ const maybeError = asErrorEnvelope(result.body);
52
+ if (maybeError) {
53
+ const details = formatErrorDetails(maybeError.error.details);
54
+ const detailsSuffix = details ? ` details=${details}` : '';
55
+ throw new errors_js_1.ApiError(`API request failed (HTTP ${status}) code=${maybeError.error.code} message=${maybeError.error.message}${detailsSuffix}`, status);
56
+ }
57
+ throw new errors_js_1.ApiError(`API request failed (HTTP ${status})`, status);
58
+ }
59
+ function createGeosApi(ctx) {
60
+ return {
61
+ list: async () => {
62
+ const result = await ctx.request({
63
+ method: 'GET',
64
+ path: '/geos',
65
+ });
66
+ if (result.status < 200 || result.status >= 300) {
67
+ throwForNon2xx(result);
68
+ }
69
+ const data = unwrapSuccessArray(result.body);
70
+ if (data == null) {
71
+ throw new errors_js_1.ApiError('API response missing expected success envelope data', result.status);
72
+ }
73
+ return data;
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestCore = requestCore;
4
+ const errors_js_1 = require("../errors.js");
5
+ const DEFAULT_TIMEOUT_MS = 30000;
6
+ function joinUrl(baseUrl, path) {
7
+ const base = baseUrl.replace(/\/+$/, '');
8
+ const p = path.startsWith('/') ? path : `/${path}`;
9
+ return `${base}${p}`;
10
+ }
11
+ function buildQueryString(query) {
12
+ if (!query)
13
+ return '';
14
+ const params = new URLSearchParams();
15
+ for (const [key, value] of Object.entries(query)) {
16
+ if (value == null)
17
+ continue;
18
+ if (Array.isArray(value)) {
19
+ for (const v of value)
20
+ params.append(key, String(v));
21
+ continue;
22
+ }
23
+ params.set(key, String(value));
24
+ }
25
+ const qs = params.toString();
26
+ return qs ? `?${qs}` : '';
27
+ }
28
+ function isJsonResponse(contentType) {
29
+ if (!contentType)
30
+ return false;
31
+ return contentType.toLowerCase().includes('application/json');
32
+ }
33
+ async function requestCore(options) {
34
+ const url = `${joinUrl(options.apiBaseUrl, options.path)}${buildQueryString(options.query)}`;
35
+ const fetchImpl = options.fetch ?? globalThis.fetch;
36
+ if (typeof fetchImpl !== 'function') {
37
+ throw new Error('globalThis.fetch is not available; Node 18+ is required');
38
+ }
39
+ const headers = {
40
+ Accept: 'application/json',
41
+ Authorization: `Bearer ${options.apiKey}`,
42
+ ...(options.headers ?? {}),
43
+ };
44
+ if (options.ifNoneMatch)
45
+ headers['If-None-Match'] = options.ifNoneMatch;
46
+ const hasJsonBody = options.body !== undefined && options.body !== null;
47
+ if (hasJsonBody)
48
+ headers['Content-Type'] = 'application/json';
49
+ const controller = new AbortController();
50
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
51
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
52
+ try {
53
+ let response;
54
+ try {
55
+ response = await fetchImpl(url, {
56
+ method: options.method,
57
+ headers,
58
+ body: hasJsonBody ? JSON.stringify(options.body) : undefined,
59
+ signal: controller.signal,
60
+ });
61
+ }
62
+ catch (err) {
63
+ if (controller.signal.aborted) {
64
+ throw new errors_js_1.ApiError(`Request timed out after ${timeoutMs}ms`, 408);
65
+ }
66
+ throw err;
67
+ }
68
+ const etag = response.headers.get('etag');
69
+ if (response.status === 204 || response.status === 304) {
70
+ return { status: response.status, etag, body: null };
71
+ }
72
+ const contentType = response.headers.get('content-type');
73
+ if (!isJsonResponse(contentType)) {
74
+ return { status: response.status, etag, body: null };
75
+ }
76
+ const text = await response.text();
77
+ if (!text)
78
+ return { status: response.status, etag, body: null };
79
+ return { status: response.status, etag, body: JSON.parse(text) };
80
+ }
81
+ finally {
82
+ clearTimeout(timeoutId);
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ // AluviaClient - Main public class for Aluvia Client
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.AluviaClient = void 0;
5
+ const ConfigManager_js_1 = require("./ConfigManager.js");
6
+ const ProxyServer_js_1 = require("./ProxyServer.js");
7
+ const errors_js_1 = require("../errors.js");
8
+ const adapters_js_1 = require("./adapters.js");
9
+ const logger_js_1 = require("./logger.js");
10
+ const AluviaApi_js_1 = require("../api/AluviaApi.js");
11
+ /**
12
+ * AluviaClient is the main entry point for the Aluvia Client.
13
+ *
14
+ * It manages the local proxy server and configuration polling.
15
+ */
16
+ class AluviaClient {
17
+ constructor(options) {
18
+ this.connection = null;
19
+ this.started = false;
20
+ this.startPromise = null;
21
+ const apiKey = String(options.apiKey ?? '').trim();
22
+ if (!apiKey) {
23
+ throw new errors_js_1.MissingApiKeyError('Aluvia apiKey is required');
24
+ }
25
+ const localProxy = options.localProxy ?? true;
26
+ 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;
31
+ 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';
35
+ this.logger = new logger_js_1.Logger(logLevel);
36
+ // Create ConfigManager
37
+ this.configManager = new ConfigManager_js_1.ConfigManager({
38
+ apiKey,
39
+ apiBaseUrl,
40
+ pollIntervalMs,
41
+ gatewayProtocol,
42
+ gatewayPort,
43
+ logLevel,
44
+ connectionId,
45
+ strict,
46
+ });
47
+ // Create ProxyServer
48
+ this.proxyServer = new ProxyServer_js_1.ProxyServer(this.configManager, { logLevel });
49
+ this.api = new AluviaApi_js_1.AluviaApi({
50
+ apiKey,
51
+ apiBaseUrl,
52
+ timeoutMs,
53
+ });
54
+ }
55
+ /**
56
+ * Start the Aluvia Client connection:
57
+ * - Fetch initial account connection config from Aluvia.
58
+ * - 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.
61
+ *
62
+ * Returns the active connection with host/port/url and a stop() method.
63
+ */
64
+ async start() {
65
+ // Return existing connection if already started
66
+ if (this.started && this.connection) {
67
+ return this.connection;
68
+ }
69
+ // If a start is already in-flight, await it.
70
+ if (this.startPromise) {
71
+ return this.startPromise;
72
+ }
73
+ this.startPromise = (async () => {
74
+ const localProxyEnabled = this.options.localProxy === true;
75
+ // Fetch initial configuration (may throw InvalidApiKeyError or ApiError)
76
+ 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');
83
+ let nodeAgents = null;
84
+ let undiciDispatcher = null;
85
+ 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
+ const getNodeAgents = () => {
101
+ if (!nodeAgents) {
102
+ nodeAgents = (0, adapters_js_1.createNodeProxyAgents)(getProxyUrlForHttpClients());
103
+ }
104
+ return nodeAgents;
105
+ };
106
+ const getUndiciDispatcher = () => {
107
+ if (!undiciDispatcher) {
108
+ undiciDispatcher = (0, adapters_js_1.createUndiciDispatcher)(getProxyUrlForHttpClients());
109
+ }
110
+ return undiciDispatcher;
111
+ };
112
+ const closeUndiciDispatcher = async () => {
113
+ const d = undiciDispatcher;
114
+ if (!d)
115
+ return;
116
+ try {
117
+ if (typeof d.close === 'function') {
118
+ await d.close();
119
+ }
120
+ else if (typeof d.destroy === 'function') {
121
+ d.destroy();
122
+ }
123
+ }
124
+ finally {
125
+ undiciDispatcher = null;
126
+ }
127
+ };
128
+ const stop = async () => {
129
+ this.configManager.stopPolling();
130
+ nodeAgents?.http?.destroy?.();
131
+ nodeAgents?.https?.destroy?.();
132
+ nodeAgents = null;
133
+ await closeUndiciDispatcher();
134
+ undiciFetchFn = null;
135
+ this.connection = null;
136
+ this.started = false;
137
+ };
138
+ // Build connection object
139
+ 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
+ },
175
+ asNodeAgents: () => getNodeAgents(),
176
+ asAxiosConfig: () => (0, adapters_js_1.toAxiosConfig)(getNodeAgents()),
177
+ asGotOptions: () => (0, adapters_js_1.toGotOptions)(getNodeAgents()),
178
+ asUndiciDispatcher: () => getUndiciDispatcher(),
179
+ asUndiciFetch: () => {
180
+ if (!undiciFetchFn) {
181
+ undiciFetchFn = (0, adapters_js_1.createUndiciFetch)(getUndiciDispatcher());
182
+ }
183
+ return undiciFetchFn;
184
+ },
185
+ stop,
186
+ close: stop,
187
+ };
188
+ this.connection = connection;
189
+ this.started = true;
190
+ return connection;
191
+ }
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();
229
+ 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;
263
+ })();
264
+ try {
265
+ return await this.startPromise;
266
+ }
267
+ finally {
268
+ this.startPromise = null;
269
+ }
270
+ }
271
+ /**
272
+ * Global cleanup:
273
+ * - Stop the local proxy server (if running).
274
+ * - Stop config polling.
275
+ */
276
+ async stop() {
277
+ // If start is in-flight, wait for it to settle so we don't leave a running proxy behind.
278
+ if (this.startPromise) {
279
+ try {
280
+ await this.startPromise;
281
+ }
282
+ catch {
283
+ // ignore startup errors; if startup failed there is nothing to stop
284
+ }
285
+ }
286
+ if (!this.started) {
287
+ return;
288
+ }
289
+ // Only stop proxy if it was potentially started.
290
+ if (this.options.localProxy) {
291
+ await this.proxyServer.stop();
292
+ }
293
+ this.configManager.stopPolling();
294
+ this.connection = null;
295
+ this.started = false;
296
+ }
297
+ /**
298
+ * Update the filtering rules used by the proxy.
299
+ * @param rules
300
+ */
301
+ async updateRules(rules) {
302
+ await this.configManager.setConfig({ rules: rules });
303
+ }
304
+ /**
305
+ * Update the upstream session_id.
306
+ * @param sessionId
307
+ */
308
+ async updateSessionId(sessionId) {
309
+ await this.configManager.setConfig({ session_id: sessionId });
310
+ }
311
+ /**
312
+ * Update the upstream target_geo (geo targeting).
313
+ *
314
+ * Pass null to clear geo targeting.
315
+ */
316
+ async updateTargetGeo(targetGeo) {
317
+ if (targetGeo === null) {
318
+ await this.configManager.setConfig({ target_geo: null });
319
+ return;
320
+ }
321
+ const trimmed = targetGeo.trim();
322
+ await this.configManager.setConfig({ target_geo: trimmed.length > 0 ? trimmed : null });
323
+ }
324
+ }
325
+ exports.AluviaClient = AluviaClient;