@aluvia/sdk 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -285
- package/dist/cjs/api/account.js +10 -74
- package/dist/cjs/api/apiUtils.js +80 -0
- package/dist/cjs/api/geos.js +2 -63
- package/dist/cjs/api/request.js +8 -2
- package/dist/cjs/bin/account.js +31 -0
- package/dist/cjs/bin/api-helpers.js +58 -0
- package/dist/cjs/bin/cli.js +245 -0
- package/dist/cjs/bin/close.js +120 -0
- package/dist/cjs/bin/geos.js +10 -0
- package/dist/cjs/bin/mcp-helpers.js +57 -0
- package/dist/cjs/bin/mcp-server.js +220 -0
- package/dist/cjs/bin/mcp-tools.js +90 -0
- package/dist/cjs/bin/open.js +293 -0
- package/dist/cjs/bin/session.js +259 -0
- package/dist/cjs/client/AluviaClient.js +365 -189
- package/dist/cjs/client/BlockDetection.js +486 -0
- package/dist/cjs/client/ConfigManager.js +26 -23
- package/dist/cjs/client/PageLoadDetection.js +175 -0
- package/dist/cjs/client/ProxyServer.js +4 -2
- package/dist/cjs/client/logger.js +4 -0
- package/dist/cjs/client/rules.js +38 -49
- package/dist/cjs/connect.js +117 -0
- package/dist/cjs/errors.js +12 -1
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/session/lock.js +186 -0
- package/dist/esm/api/account.js +2 -66
- package/dist/esm/api/apiUtils.js +71 -0
- package/dist/esm/api/geos.js +2 -63
- package/dist/esm/api/request.js +8 -2
- package/dist/esm/bin/account.js +28 -0
- package/dist/esm/bin/api-helpers.js +53 -0
- package/dist/esm/bin/cli.js +242 -0
- package/dist/esm/bin/close.js +117 -0
- package/dist/esm/bin/geos.js +7 -0
- package/dist/esm/bin/mcp-helpers.js +51 -0
- package/dist/esm/bin/mcp-server.js +185 -0
- package/dist/esm/bin/mcp-tools.js +78 -0
- package/dist/esm/bin/open.js +256 -0
- package/dist/esm/bin/session.js +252 -0
- package/dist/esm/client/AluviaClient.js +371 -195
- package/dist/esm/client/BlockDetection.js +482 -0
- package/dist/esm/client/ConfigManager.js +21 -18
- package/dist/esm/client/PageLoadDetection.js +171 -0
- package/dist/esm/client/ProxyServer.js +5 -3
- package/dist/esm/client/logger.js +4 -0
- package/dist/esm/client/rules.js +36 -49
- package/dist/esm/connect.js +81 -0
- package/dist/esm/errors.js +10 -0
- package/dist/esm/index.js +5 -3
- package/dist/esm/session/lock.js +142 -0
- package/dist/types/api/AluviaApi.d.ts +2 -7
- package/dist/types/api/account.d.ts +1 -16
- package/dist/types/api/apiUtils.d.ts +28 -0
- package/dist/types/api/geos.d.ts +1 -1
- package/dist/types/bin/account.d.ts +1 -0
- package/dist/types/bin/api-helpers.d.ts +20 -0
- package/dist/types/bin/cli.d.ts +2 -0
- package/dist/types/bin/close.d.ts +1 -0
- package/dist/types/bin/geos.d.ts +1 -0
- package/dist/types/bin/mcp-helpers.d.ts +28 -0
- package/dist/types/bin/mcp-server.d.ts +2 -0
- package/dist/types/bin/mcp-tools.d.ts +46 -0
- package/dist/types/bin/open.d.ts +21 -0
- package/dist/types/bin/session.d.ts +11 -0
- package/dist/types/client/AluviaClient.d.ts +51 -4
- package/dist/types/client/BlockDetection.d.ts +96 -0
- package/dist/types/client/ConfigManager.d.ts +6 -1
- package/dist/types/client/PageLoadDetection.d.ts +93 -0
- package/dist/types/client/logger.d.ts +2 -0
- package/dist/types/client/rules.d.ts +18 -0
- package/dist/types/client/types.d.ts +48 -47
- package/dist/types/connect.d.ts +18 -0
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/index.d.ts +7 -5
- package/dist/types/session/lock.d.ts +43 -0
- package/package.json +11 -10
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// PageLoadDetection - Enhanced page load and blocking detection
|
|
2
|
+
const DEFAULT_BLOCKING_KEYWORDS = [
|
|
3
|
+
"captcha",
|
|
4
|
+
"blocked",
|
|
5
|
+
"access denied",
|
|
6
|
+
"forbidden",
|
|
7
|
+
"cloudflare",
|
|
8
|
+
"please verify",
|
|
9
|
+
"recaptcha",
|
|
10
|
+
"hcaptcha",
|
|
11
|
+
"bot detection",
|
|
12
|
+
"automated access",
|
|
13
|
+
"unusual activity",
|
|
14
|
+
"verify you are human",
|
|
15
|
+
"security check",
|
|
16
|
+
"access restricted",
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_BLOCKING_STATUS_CODES = [403, 429, 503];
|
|
19
|
+
const DEFAULT_MIN_CONTENT_LENGTH = 100;
|
|
20
|
+
/**
|
|
21
|
+
* PageLoadDetection handles enhanced detection of page load failures and blocking
|
|
22
|
+
*/
|
|
23
|
+
export class PageLoadDetection {
|
|
24
|
+
constructor(config, logger) {
|
|
25
|
+
this.blockedHostnames = new Set();
|
|
26
|
+
this.logger = logger;
|
|
27
|
+
this.config = {
|
|
28
|
+
enabled: config.enabled ?? true,
|
|
29
|
+
blockingKeywords: config.blockingKeywords ?? DEFAULT_BLOCKING_KEYWORDS,
|
|
30
|
+
blockingStatusCodes: config.blockingStatusCodes ?? DEFAULT_BLOCKING_STATUS_CODES,
|
|
31
|
+
minContentLength: config.minContentLength ?? DEFAULT_MIN_CONTENT_LENGTH,
|
|
32
|
+
autoAddRules: config.autoAddRules ?? false,
|
|
33
|
+
onBlockingDetected: config.onBlockingDetected,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Update detection configuration
|
|
38
|
+
*/
|
|
39
|
+
updateConfig(config) {
|
|
40
|
+
if (config.enabled !== undefined) {
|
|
41
|
+
this.config.enabled = config.enabled;
|
|
42
|
+
}
|
|
43
|
+
if (config.blockingKeywords !== undefined) {
|
|
44
|
+
this.config.blockingKeywords = config.blockingKeywords;
|
|
45
|
+
}
|
|
46
|
+
if (config.blockingStatusCodes !== undefined) {
|
|
47
|
+
this.config.blockingStatusCodes = config.blockingStatusCodes;
|
|
48
|
+
}
|
|
49
|
+
if (config.minContentLength !== undefined) {
|
|
50
|
+
this.config.minContentLength = config.minContentLength;
|
|
51
|
+
}
|
|
52
|
+
if (config.autoAddRules !== undefined) {
|
|
53
|
+
this.config.autoAddRules = config.autoAddRules;
|
|
54
|
+
}
|
|
55
|
+
if (config.onBlockingDetected !== undefined) {
|
|
56
|
+
this.config.onBlockingDetected = config.onBlockingDetected;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a hostname is already marked as blocked
|
|
61
|
+
*/
|
|
62
|
+
isHostnameBlocked(hostname) {
|
|
63
|
+
return this.blockedHostnames.has(hostname);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get all blocked hostnames
|
|
67
|
+
*/
|
|
68
|
+
getBlockedHostnames() {
|
|
69
|
+
return Array.from(this.blockedHostnames);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Clear blocked hostnames cache
|
|
73
|
+
*/
|
|
74
|
+
clearBlockedHostnames() {
|
|
75
|
+
this.blockedHostnames.clear();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Analyze a page load and detect if it was blocked
|
|
79
|
+
*/
|
|
80
|
+
async analyzePage(page, response) {
|
|
81
|
+
if (!this.config.enabled) {
|
|
82
|
+
return {
|
|
83
|
+
url: page.url(),
|
|
84
|
+
hostname: this.extractHostname(page.url()),
|
|
85
|
+
success: true,
|
|
86
|
+
blocked: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
this.logger.debug("Analyzing page load for URL: " + page.url());
|
|
90
|
+
const url = page.url();
|
|
91
|
+
const hostname = this.extractHostname(url);
|
|
92
|
+
try {
|
|
93
|
+
// Check HTTP status code
|
|
94
|
+
const statusCode = response?.status?.() ?? 0;
|
|
95
|
+
if (statusCode > 0 &&
|
|
96
|
+
this.config.blockingStatusCodes.includes(statusCode)) {
|
|
97
|
+
const reason = {
|
|
98
|
+
type: "status_code",
|
|
99
|
+
details: `HTTP status code ${statusCode} indicates blocking`,
|
|
100
|
+
statusCode,
|
|
101
|
+
};
|
|
102
|
+
await this.handleBlocking(hostname, reason, page);
|
|
103
|
+
return { url, hostname, success: false, blocked: true, reason };
|
|
104
|
+
}
|
|
105
|
+
// Get page content
|
|
106
|
+
const content = await page.content().catch(() => "");
|
|
107
|
+
// Check for blocking keywords first (more specific than content length)
|
|
108
|
+
const contentLower = content.toLowerCase();
|
|
109
|
+
const title = (await page.title().catch(() => "")).toLowerCase();
|
|
110
|
+
const combinedText = `${contentLower} ${title}`;
|
|
111
|
+
for (const keyword of this.config.blockingKeywords) {
|
|
112
|
+
if (combinedText.includes(keyword.toLowerCase())) {
|
|
113
|
+
const reason = {
|
|
114
|
+
type: "keyword",
|
|
115
|
+
details: `Blocking keyword detected: "${keyword}"`,
|
|
116
|
+
keyword,
|
|
117
|
+
};
|
|
118
|
+
await this.handleBlocking(hostname, reason, page);
|
|
119
|
+
return { url, hostname, success: false, blocked: true, reason };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Check content length (less critical than keywords)
|
|
123
|
+
if (!content || content.length < this.config.minContentLength) {
|
|
124
|
+
const reason = {
|
|
125
|
+
type: "content_length",
|
|
126
|
+
details: `Page content too short (${content.length} < ${this.config.minContentLength})`,
|
|
127
|
+
};
|
|
128
|
+
this.logger.warn(`Page may have failed to load: ${url} (${reason.details})`);
|
|
129
|
+
return { url, hostname, success: false, blocked: false, reason };
|
|
130
|
+
}
|
|
131
|
+
// Page loaded successfully
|
|
132
|
+
return { url, hostname, success: true, blocked: false };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const reason = {
|
|
136
|
+
type: "error",
|
|
137
|
+
details: `Error analyzing page: ${error.message}`,
|
|
138
|
+
};
|
|
139
|
+
this.logger.warn(`Error checking page load status for ${url}: ${error.message}`);
|
|
140
|
+
return { url, hostname, success: false, blocked: false, reason };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Handle detected blocking
|
|
145
|
+
*/
|
|
146
|
+
async handleBlocking(hostname, reason, page) {
|
|
147
|
+
this.blockedHostnames.add(hostname);
|
|
148
|
+
this.logger.warn(`Blocking detected for ${hostname}: ${reason.details}`);
|
|
149
|
+
// Trigger callback if provided
|
|
150
|
+
if (this.config.onBlockingDetected) {
|
|
151
|
+
try {
|
|
152
|
+
await this.config.onBlockingDetected(hostname, reason, page);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger.warn(`Error in onBlockingDetected callback: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Extract hostname from URL
|
|
161
|
+
*/
|
|
162
|
+
extractHostname(url) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(url);
|
|
165
|
+
return parsed.hostname;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Server as ProxyChainServer } from 'proxy-chain';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
import { ProxyStartError } from '../errors.js';
|
|
5
|
-
import { shouldProxy } from './rules.js';
|
|
5
|
+
import { shouldProxy, shouldProxyNormalized } from './rules.js';
|
|
6
6
|
/**
|
|
7
7
|
* ProxyServer manages the local HTTP(S) proxy that routes traffic
|
|
8
8
|
* through Aluvia or directly based on rules.
|
|
@@ -94,8 +94,10 @@ export class ProxyServer {
|
|
|
94
94
|
this.logger.debug('Could not extract hostname, going direct');
|
|
95
95
|
return undefined;
|
|
96
96
|
}
|
|
97
|
-
// Check if we should proxy this hostname
|
|
98
|
-
const useProxy =
|
|
97
|
+
// Check if we should proxy this hostname (use pre-normalized rules if available)
|
|
98
|
+
const useProxy = config.normalizedRules
|
|
99
|
+
? shouldProxyNormalized(hostname, config.normalizedRules)
|
|
100
|
+
: shouldProxy(hostname, config.rules);
|
|
99
101
|
if (!useProxy) {
|
|
100
102
|
this.logger.debug(`Hostname ${hostname} bypassing proxy (direct)`);
|
|
101
103
|
return undefined;
|
package/dist/esm/client/rules.js
CHANGED
|
@@ -51,48 +51,26 @@ export function matchPattern(hostname, pattern) {
|
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Rules semantics:
|
|
57
|
-
* - [] (empty) → no proxy (return false)
|
|
58
|
-
* - ['*'] → proxy everything
|
|
59
|
-
* - ['example.com'] → proxy only example.com
|
|
60
|
-
* - ['*.google.com'] → proxy subdomains of google.com
|
|
61
|
-
* - ['*', '-example.com'] → proxy everything except example.com
|
|
62
|
-
* - ['AUTO', 'example.com'] → AUTO is placeholder (ignored), proxy example.com
|
|
63
|
-
*
|
|
64
|
-
* Negative patterns (prefixed with '-') exclude hosts from proxying.
|
|
65
|
-
* If '*' is in rules, default is to proxy unless excluded.
|
|
66
|
-
* Without '*', only explicitly matched patterns are proxied.
|
|
67
|
-
*
|
|
68
|
-
* @param hostname - The hostname to check
|
|
69
|
-
* @param rules - Array of rule patterns
|
|
70
|
-
* @returns true if the hostname should be proxied
|
|
54
|
+
* Pre-process raw rule strings into a NormalizedRules structure.
|
|
55
|
+
* Call once when config is loaded, then use shouldProxyNormalized() per request.
|
|
71
56
|
*/
|
|
72
|
-
export function
|
|
73
|
-
const normalizedHostname = hostname.trim();
|
|
74
|
-
if (!normalizedHostname)
|
|
75
|
-
return false;
|
|
76
|
-
// Empty rules means no proxy
|
|
57
|
+
export function normalizeRules(rules) {
|
|
77
58
|
if (!rules || rules.length === 0) {
|
|
78
|
-
return false;
|
|
59
|
+
return { positiveRules: [], negativeRules: [], hasCatchAll: false, empty: true };
|
|
79
60
|
}
|
|
80
|
-
const
|
|
61
|
+
const trimmed = rules
|
|
81
62
|
.filter((r) => typeof r === 'string')
|
|
82
|
-
.map((r) => r.trim())
|
|
83
|
-
.filter((r) => r.length > 0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (effectiveRules.length === 0) {
|
|
88
|
-
return false;
|
|
63
|
+
.map((r) => r.trim().toLowerCase())
|
|
64
|
+
.filter((r) => r.length > 0)
|
|
65
|
+
.filter((r) => r !== 'auto');
|
|
66
|
+
if (trimmed.length === 0) {
|
|
67
|
+
return { positiveRules: [], negativeRules: [], hasCatchAll: false, empty: true };
|
|
89
68
|
}
|
|
90
|
-
// Separate positive and negative rules
|
|
91
69
|
const negativeRules = [];
|
|
92
70
|
const positiveRules = [];
|
|
93
|
-
for (const rule of
|
|
71
|
+
for (const rule of trimmed) {
|
|
94
72
|
if (rule.startsWith('-')) {
|
|
95
|
-
const neg = rule.slice(1).trim();
|
|
73
|
+
const neg = rule.slice(1).trim();
|
|
96
74
|
if (neg.length > 0)
|
|
97
75
|
negativeRules.push(neg);
|
|
98
76
|
}
|
|
@@ -100,25 +78,34 @@ export function shouldProxy(hostname, rules) {
|
|
|
100
78
|
positiveRules.push(rule);
|
|
101
79
|
}
|
|
102
80
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
81
|
+
return {
|
|
82
|
+
positiveRules,
|
|
83
|
+
negativeRules,
|
|
84
|
+
hasCatchAll: positiveRules.includes('*'),
|
|
85
|
+
empty: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Fast proxy decision using pre-normalized rules.
|
|
90
|
+
*/
|
|
91
|
+
export function shouldProxyNormalized(hostname, rules) {
|
|
92
|
+
const normalizedHostname = hostname.trim().toLowerCase();
|
|
93
|
+
if (!normalizedHostname)
|
|
94
|
+
return false;
|
|
95
|
+
if (rules.empty)
|
|
96
|
+
return false;
|
|
97
|
+
for (const negRule of rules.negativeRules) {
|
|
98
|
+
if (matchPattern(normalizedHostname, negRule))
|
|
107
99
|
return false;
|
|
108
|
-
}
|
|
109
100
|
}
|
|
110
|
-
|
|
111
|
-
const hasCatchAll = positiveRules.includes('*');
|
|
112
|
-
if (hasCatchAll) {
|
|
113
|
-
// With catch-all, proxy everything not excluded by negative rules
|
|
101
|
+
if (rules.hasCatchAll)
|
|
114
102
|
return true;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const posRule of positiveRules) {
|
|
118
|
-
if (matchPattern(normalizedHostname, posRule)) {
|
|
103
|
+
for (const posRule of rules.positiveRules) {
|
|
104
|
+
if (matchPattern(normalizedHostname, posRule))
|
|
119
105
|
return true;
|
|
120
|
-
}
|
|
121
106
|
}
|
|
122
|
-
// No match found
|
|
123
107
|
return false;
|
|
124
108
|
}
|
|
109
|
+
export function shouldProxy(hostname, rules) {
|
|
110
|
+
return shouldProxyNormalized(hostname, normalizeRules(rules));
|
|
111
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readLock, listSessions, isProcessAlive, removeLock } from './session/lock.js';
|
|
2
|
+
import { ConnectError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Connect to a running Aluvia browser session via CDP.
|
|
5
|
+
*
|
|
6
|
+
* - No args: auto-discovers a single running session.
|
|
7
|
+
* - With session name: connects to that specific session.
|
|
8
|
+
*
|
|
9
|
+
* Requires `playwright` as a peer dependency.
|
|
10
|
+
*/
|
|
11
|
+
export async function connect(sessionName) {
|
|
12
|
+
// 1. Import Playwright
|
|
13
|
+
let pw;
|
|
14
|
+
try {
|
|
15
|
+
pw = await import('playwright');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new ConnectError('Playwright is required for connect(). Install it: npm install playwright');
|
|
19
|
+
}
|
|
20
|
+
// 2. Resolve session
|
|
21
|
+
let resolvedName;
|
|
22
|
+
if (sessionName) {
|
|
23
|
+
resolvedName = sessionName;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const sessions = listSessions();
|
|
27
|
+
if (sessions.length === 0) {
|
|
28
|
+
throw new ConnectError('No running Aluvia sessions found. Start one with: npx aluvia-sdk session start <url>');
|
|
29
|
+
}
|
|
30
|
+
if (sessions.length > 1) {
|
|
31
|
+
const names = sessions.map((s) => s.session).join(', ');
|
|
32
|
+
throw new ConnectError(`Multiple Aluvia sessions running (${names}). Specify which one: connect('${sessions[0].session}')`);
|
|
33
|
+
}
|
|
34
|
+
resolvedName = sessions[0].session;
|
|
35
|
+
}
|
|
36
|
+
// 3. Validate session state
|
|
37
|
+
const lock = readLock(resolvedName);
|
|
38
|
+
if (!lock) {
|
|
39
|
+
throw new ConnectError(`No Aluvia session found named '${resolvedName}'. Run 'npx aluvia-sdk session list' to list sessions.`);
|
|
40
|
+
}
|
|
41
|
+
if (!isProcessAlive(lock.pid)) {
|
|
42
|
+
removeLock(resolvedName);
|
|
43
|
+
throw new ConnectError(`Session '${resolvedName}' is no longer running. Stale lock file removed.`);
|
|
44
|
+
}
|
|
45
|
+
if (!lock.ready) {
|
|
46
|
+
throw new ConnectError(`Session '${resolvedName}' is still starting up. Try again shortly.`);
|
|
47
|
+
}
|
|
48
|
+
if (!lock.cdpUrl) {
|
|
49
|
+
throw new ConnectError(`Session '${resolvedName}' has no CDP URL.`);
|
|
50
|
+
}
|
|
51
|
+
// 4. Connect over CDP
|
|
52
|
+
let browser;
|
|
53
|
+
try {
|
|
54
|
+
browser = await pw.chromium.connectOverCDP(lock.cdpUrl);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new ConnectError(`Failed to connect to session '${resolvedName}' at ${lock.cdpUrl}: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
// 5. Get context and page
|
|
60
|
+
let context;
|
|
61
|
+
let page;
|
|
62
|
+
try {
|
|
63
|
+
context = browser.contexts()[0] ?? await browser.newContext();
|
|
64
|
+
page = context.pages()[0] ?? await context.newPage();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
await browser.close().catch(() => { });
|
|
68
|
+
throw new ConnectError(`Connected but failed to get page: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
browser,
|
|
72
|
+
context,
|
|
73
|
+
page,
|
|
74
|
+
sessionName: resolvedName,
|
|
75
|
+
cdpUrl: lock.cdpUrl,
|
|
76
|
+
connectionId: lock.connectionId,
|
|
77
|
+
disconnect: async () => {
|
|
78
|
+
await browser.close();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/esm/errors.js
CHANGED
|
@@ -40,3 +40,13 @@ export class ProxyStartError extends Error {
|
|
|
40
40
|
Object.setPrototypeOf(this, ProxyStartError.prototype);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Thrown by connect() when it cannot establish a CDP connection to a running session.
|
|
45
|
+
*/
|
|
46
|
+
export class ConnectError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'ConnectError';
|
|
50
|
+
Object.setPrototypeOf(this, ConnectError.prototype);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Aluvia Client Node
|
|
2
2
|
// Main entry point
|
|
3
3
|
// Public class
|
|
4
|
-
export { AluviaClient } from
|
|
5
|
-
export { AluviaApi } from
|
|
4
|
+
export { AluviaClient } from "./client/AluviaClient.js";
|
|
5
|
+
export { AluviaApi } from "./api/AluviaApi.js";
|
|
6
|
+
// Connect helper
|
|
7
|
+
export { connect } from "./connect.js";
|
|
6
8
|
// Public error classes
|
|
7
|
-
export { MissingApiKeyError, InvalidApiKeyError, ApiError, ProxyStartError, } from
|
|
9
|
+
export { MissingApiKeyError, InvalidApiKeyError, ApiError, ProxyStartError, ConnectError, } from "./errors.js";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
const LOCK_DIR = path.join(os.tmpdir(), 'aluvia-sdk');
|
|
5
|
+
const ADJECTIVES = [
|
|
6
|
+
'swift', 'bold', 'calm', 'keen', 'warm', 'bright', 'silent', 'rapid', 'steady', 'clever',
|
|
7
|
+
'vivid', 'agile', 'noble', 'lucid', 'crisp', 'gentle', 'fierce', 'nimble', 'sturdy', 'witty',
|
|
8
|
+
];
|
|
9
|
+
const NOUNS = [
|
|
10
|
+
'falcon', 'tiger', 'river', 'maple', 'coral', 'cedar', 'orbit', 'prism', 'flint', 'spark',
|
|
11
|
+
'ridge', 'ember', 'crane', 'grove', 'stone', 'brook', 'drift', 'crest', 'sage', 'lynx',
|
|
12
|
+
];
|
|
13
|
+
function lockFileName(sessionName) {
|
|
14
|
+
return `cli-${sessionName ?? 'default'}.lock`;
|
|
15
|
+
}
|
|
16
|
+
function logFileName(sessionName) {
|
|
17
|
+
return `cli-${sessionName ?? 'default'}.log`;
|
|
18
|
+
}
|
|
19
|
+
export function writeLock(data, sessionName) {
|
|
20
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
21
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
22
|
+
const tmpPath = filePath + '.tmp';
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data), 'utf-8');
|
|
25
|
+
try {
|
|
26
|
+
// On Windows, rename may fail to overwrite an existing file; remove it first.
|
|
27
|
+
fs.rmSync(filePath, { force: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Ignore errors removing the existing lock file.
|
|
31
|
+
}
|
|
32
|
+
fs.renameSync(tmpPath, filePath);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
try {
|
|
36
|
+
// Ensure no leftover temp file remains if rename fails.
|
|
37
|
+
fs.rmSync(tmpPath, { force: true });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Ignore cleanup errors.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function readLock(sessionName) {
|
|
45
|
+
try {
|
|
46
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
47
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if (typeof parsed.pid === 'number' && Number.isFinite(parsed.pid)) {
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function removeLock(sessionName) {
|
|
59
|
+
try {
|
|
60
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
61
|
+
fs.unlinkSync(filePath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function isProcessAlive(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function getLogFilePath(sessionName) {
|
|
77
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
78
|
+
return path.join(LOCK_DIR, logFileName(sessionName));
|
|
79
|
+
}
|
|
80
|
+
export function validateSessionName(name) {
|
|
81
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
82
|
+
}
|
|
83
|
+
export function generateSessionName() {
|
|
84
|
+
const maxAttempts = 10;
|
|
85
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
86
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
87
|
+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
88
|
+
const name = attempt === 0 ? `${adj}-${noun}` : `${adj}-${noun}-${attempt}`;
|
|
89
|
+
const filePath = path.join(LOCK_DIR, lockFileName(name));
|
|
90
|
+
if (!fs.existsSync(filePath)) {
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
// Lock file exists — check if the process is still alive
|
|
94
|
+
const lock = readLock(name);
|
|
95
|
+
if (!lock || !isProcessAlive(lock.pid)) {
|
|
96
|
+
// Stale lock, we can reuse this name
|
|
97
|
+
removeLock(name);
|
|
98
|
+
return name;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Fallback: use timestamp
|
|
102
|
+
return `session-${Date.now()}`;
|
|
103
|
+
}
|
|
104
|
+
export function toLockData(info) {
|
|
105
|
+
const { session: _session, ...lock } = info;
|
|
106
|
+
return lock;
|
|
107
|
+
}
|
|
108
|
+
export function listSessions() {
|
|
109
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
110
|
+
const files = fs.readdirSync(LOCK_DIR);
|
|
111
|
+
const sessions = [];
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const match = file.match(/^cli-(.+)\.lock$/);
|
|
114
|
+
if (!match)
|
|
115
|
+
continue;
|
|
116
|
+
const sessionName = match[1];
|
|
117
|
+
const lock = readLock(sessionName);
|
|
118
|
+
if (!lock) {
|
|
119
|
+
// Corrupt lock file, clean up
|
|
120
|
+
removeLock(sessionName);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!isProcessAlive(lock.pid)) {
|
|
124
|
+
// Stale lock, clean up
|
|
125
|
+
removeLock(sessionName);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
sessions.push({
|
|
129
|
+
session: sessionName,
|
|
130
|
+
pid: lock.pid,
|
|
131
|
+
connectionId: lock.connectionId,
|
|
132
|
+
cdpUrl: lock.cdpUrl,
|
|
133
|
+
proxyUrl: lock.proxyUrl,
|
|
134
|
+
url: lock.url,
|
|
135
|
+
ready: lock.ready,
|
|
136
|
+
blockDetection: lock.blockDetection,
|
|
137
|
+
autoUnblock: lock.autoUnblock,
|
|
138
|
+
lastDetection: lock.lastDetection,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return sessions;
|
|
142
|
+
}
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { createAccountApi } from './account.js';
|
|
2
2
|
import { createGeosApi } from './geos.js';
|
|
3
|
+
import type { AluviaApiRequestArgs } from './apiUtils.js';
|
|
4
|
+
export type { AluviaApiRequestArgs };
|
|
3
5
|
export type AluviaApiOptions = {
|
|
4
6
|
apiKey: string;
|
|
5
7
|
apiBaseUrl?: string;
|
|
6
8
|
timeoutMs?: number;
|
|
7
9
|
fetch?: typeof fetch;
|
|
8
10
|
};
|
|
9
|
-
export type AluviaApiRequestArgs = {
|
|
10
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
11
|
-
path: string;
|
|
12
|
-
query?: Record<string, string | number | boolean | null | undefined>;
|
|
13
|
-
body?: unknown;
|
|
14
|
-
headers?: Record<string, string>;
|
|
15
|
-
};
|
|
16
11
|
export declare class AluviaApi {
|
|
17
12
|
private readonly apiKey;
|
|
18
13
|
private readonly apiBaseUrl;
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
import type { Account, AccountConnection, AccountConnectionDeleteResult, AccountPayment, AccountUsage } from './types.js';
|
|
2
|
-
|
|
3
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
4
|
-
path: string;
|
|
5
|
-
query?: Record<string, string | number | boolean | null | undefined>;
|
|
6
|
-
body?: unknown;
|
|
7
|
-
headers?: Record<string, string>;
|
|
8
|
-
etag?: string | null;
|
|
9
|
-
};
|
|
10
|
-
export type ApiRequestResult = {
|
|
11
|
-
status: number;
|
|
12
|
-
etag: string | null;
|
|
13
|
-
body: unknown | null;
|
|
14
|
-
};
|
|
15
|
-
export type ApiContext = {
|
|
16
|
-
request: (args: ApiRequestArgs) => Promise<ApiRequestResult>;
|
|
17
|
-
};
|
|
2
|
+
import type { ApiContext } from './apiUtils.js';
|
|
18
3
|
export declare function createAccountApi(ctx: ApiContext): {
|
|
19
4
|
get: () => Promise<Account>;
|
|
20
5
|
usage: {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ErrorEnvelope } from './types.js';
|
|
2
|
+
export type ApiRequestArgs = {
|
|
3
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
4
|
+
path: string;
|
|
5
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
6
|
+
body?: unknown;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
etag?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type ApiRequestResult = {
|
|
11
|
+
status: number;
|
|
12
|
+
etag: string | null;
|
|
13
|
+
body: unknown | null;
|
|
14
|
+
};
|
|
15
|
+
export type ApiContext = {
|
|
16
|
+
request: (args: ApiRequestArgs) => Promise<ApiRequestResult>;
|
|
17
|
+
};
|
|
18
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
19
|
+
export declare function asErrorEnvelope(value: unknown): ErrorEnvelope | null;
|
|
20
|
+
export declare function formatErrorDetails(details: unknown): string;
|
|
21
|
+
export declare function throwForNon2xx(result: ApiRequestResult): never;
|
|
22
|
+
export type AluviaApiRequestArgs = Omit<ApiRequestArgs, 'etag'>;
|
|
23
|
+
export declare function throwIfAuthError(status: number): void;
|
|
24
|
+
export declare function unwrapSuccess<T>(value: unknown): T | null;
|
|
25
|
+
export declare function requestAndUnwrap<T>(ctx: ApiContext, args: ApiRequestArgs): Promise<{
|
|
26
|
+
data: T;
|
|
27
|
+
etag: string | null;
|
|
28
|
+
}>;
|
package/dist/types/api/geos.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleAccount(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AluviaApi } from '../api/AluviaApi.js';
|
|
2
|
+
import type { LockData } from '../session/lock.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create an AluviaApi instance from ALUVIA_API_KEY env var.
|
|
5
|
+
* Calls output() and exits if the key is missing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function requireApi(): AluviaApi;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a session by name or auto-select when only one is running.
|
|
10
|
+
* Calls output() and exits on error (no sessions, ambiguous sessions, stale lock).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveSession(sessionName?: string): {
|
|
13
|
+
session: string;
|
|
14
|
+
lock: LockData;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Require a connection ID from lock data.
|
|
18
|
+
* Calls output() and exits if connectionId is missing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function requireConnectionId(lock: LockData, session: string): number;
|