@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.
- package/README.md +425 -256
- 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 +411 -150
- 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 +384 -156
- 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 +69 -46
- 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 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// PageLoadDetection - Enhanced page load and blocking detection
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PageLoadDetection = void 0;
|
|
5
|
+
const DEFAULT_BLOCKING_KEYWORDS = [
|
|
6
|
+
"captcha",
|
|
7
|
+
"blocked",
|
|
8
|
+
"access denied",
|
|
9
|
+
"forbidden",
|
|
10
|
+
"cloudflare",
|
|
11
|
+
"please verify",
|
|
12
|
+
"recaptcha",
|
|
13
|
+
"hcaptcha",
|
|
14
|
+
"bot detection",
|
|
15
|
+
"automated access",
|
|
16
|
+
"unusual activity",
|
|
17
|
+
"verify you are human",
|
|
18
|
+
"security check",
|
|
19
|
+
"access restricted",
|
|
20
|
+
];
|
|
21
|
+
const DEFAULT_BLOCKING_STATUS_CODES = [403, 429, 503];
|
|
22
|
+
const DEFAULT_MIN_CONTENT_LENGTH = 100;
|
|
23
|
+
/**
|
|
24
|
+
* PageLoadDetection handles enhanced detection of page load failures and blocking
|
|
25
|
+
*/
|
|
26
|
+
class PageLoadDetection {
|
|
27
|
+
constructor(config, logger) {
|
|
28
|
+
this.blockedHostnames = new Set();
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
this.config = {
|
|
31
|
+
enabled: config.enabled ?? true,
|
|
32
|
+
blockingKeywords: config.blockingKeywords ?? DEFAULT_BLOCKING_KEYWORDS,
|
|
33
|
+
blockingStatusCodes: config.blockingStatusCodes ?? DEFAULT_BLOCKING_STATUS_CODES,
|
|
34
|
+
minContentLength: config.minContentLength ?? DEFAULT_MIN_CONTENT_LENGTH,
|
|
35
|
+
autoAddRules: config.autoAddRules ?? false,
|
|
36
|
+
onBlockingDetected: config.onBlockingDetected,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Update detection configuration
|
|
41
|
+
*/
|
|
42
|
+
updateConfig(config) {
|
|
43
|
+
if (config.enabled !== undefined) {
|
|
44
|
+
this.config.enabled = config.enabled;
|
|
45
|
+
}
|
|
46
|
+
if (config.blockingKeywords !== undefined) {
|
|
47
|
+
this.config.blockingKeywords = config.blockingKeywords;
|
|
48
|
+
}
|
|
49
|
+
if (config.blockingStatusCodes !== undefined) {
|
|
50
|
+
this.config.blockingStatusCodes = config.blockingStatusCodes;
|
|
51
|
+
}
|
|
52
|
+
if (config.minContentLength !== undefined) {
|
|
53
|
+
this.config.minContentLength = config.minContentLength;
|
|
54
|
+
}
|
|
55
|
+
if (config.autoAddRules !== undefined) {
|
|
56
|
+
this.config.autoAddRules = config.autoAddRules;
|
|
57
|
+
}
|
|
58
|
+
if (config.onBlockingDetected !== undefined) {
|
|
59
|
+
this.config.onBlockingDetected = config.onBlockingDetected;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if a hostname is already marked as blocked
|
|
64
|
+
*/
|
|
65
|
+
isHostnameBlocked(hostname) {
|
|
66
|
+
return this.blockedHostnames.has(hostname);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get all blocked hostnames
|
|
70
|
+
*/
|
|
71
|
+
getBlockedHostnames() {
|
|
72
|
+
return Array.from(this.blockedHostnames);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Clear blocked hostnames cache
|
|
76
|
+
*/
|
|
77
|
+
clearBlockedHostnames() {
|
|
78
|
+
this.blockedHostnames.clear();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Analyze a page load and detect if it was blocked
|
|
82
|
+
*/
|
|
83
|
+
async analyzePage(page, response) {
|
|
84
|
+
if (!this.config.enabled) {
|
|
85
|
+
return {
|
|
86
|
+
url: page.url(),
|
|
87
|
+
hostname: this.extractHostname(page.url()),
|
|
88
|
+
success: true,
|
|
89
|
+
blocked: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
this.logger.debug("Analyzing page load for URL: " + page.url());
|
|
93
|
+
const url = page.url();
|
|
94
|
+
const hostname = this.extractHostname(url);
|
|
95
|
+
try {
|
|
96
|
+
// Check HTTP status code
|
|
97
|
+
const statusCode = response?.status?.() ?? 0;
|
|
98
|
+
if (statusCode > 0 &&
|
|
99
|
+
this.config.blockingStatusCodes.includes(statusCode)) {
|
|
100
|
+
const reason = {
|
|
101
|
+
type: "status_code",
|
|
102
|
+
details: `HTTP status code ${statusCode} indicates blocking`,
|
|
103
|
+
statusCode,
|
|
104
|
+
};
|
|
105
|
+
await this.handleBlocking(hostname, reason, page);
|
|
106
|
+
return { url, hostname, success: false, blocked: true, reason };
|
|
107
|
+
}
|
|
108
|
+
// Get page content
|
|
109
|
+
const content = await page.content().catch(() => "");
|
|
110
|
+
// Check for blocking keywords first (more specific than content length)
|
|
111
|
+
const contentLower = content.toLowerCase();
|
|
112
|
+
const title = (await page.title().catch(() => "")).toLowerCase();
|
|
113
|
+
const combinedText = `${contentLower} ${title}`;
|
|
114
|
+
for (const keyword of this.config.blockingKeywords) {
|
|
115
|
+
if (combinedText.includes(keyword.toLowerCase())) {
|
|
116
|
+
const reason = {
|
|
117
|
+
type: "keyword",
|
|
118
|
+
details: `Blocking keyword detected: "${keyword}"`,
|
|
119
|
+
keyword,
|
|
120
|
+
};
|
|
121
|
+
await this.handleBlocking(hostname, reason, page);
|
|
122
|
+
return { url, hostname, success: false, blocked: true, reason };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Check content length (less critical than keywords)
|
|
126
|
+
if (!content || content.length < this.config.minContentLength) {
|
|
127
|
+
const reason = {
|
|
128
|
+
type: "content_length",
|
|
129
|
+
details: `Page content too short (${content.length} < ${this.config.minContentLength})`,
|
|
130
|
+
};
|
|
131
|
+
this.logger.warn(`Page may have failed to load: ${url} (${reason.details})`);
|
|
132
|
+
return { url, hostname, success: false, blocked: false, reason };
|
|
133
|
+
}
|
|
134
|
+
// Page loaded successfully
|
|
135
|
+
return { url, hostname, success: true, blocked: false };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const reason = {
|
|
139
|
+
type: "error",
|
|
140
|
+
details: `Error analyzing page: ${error.message}`,
|
|
141
|
+
};
|
|
142
|
+
this.logger.warn(`Error checking page load status for ${url}: ${error.message}`);
|
|
143
|
+
return { url, hostname, success: false, blocked: false, reason };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handle detected blocking
|
|
148
|
+
*/
|
|
149
|
+
async handleBlocking(hostname, reason, page) {
|
|
150
|
+
this.blockedHostnames.add(hostname);
|
|
151
|
+
this.logger.warn(`Blocking detected for ${hostname}: ${reason.details}`);
|
|
152
|
+
// Trigger callback if provided
|
|
153
|
+
if (this.config.onBlockingDetected) {
|
|
154
|
+
try {
|
|
155
|
+
await this.config.onBlockingDetected(hostname, reason, page);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.logger.warn(`Error in onBlockingDetected callback: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract hostname from URL
|
|
164
|
+
*/
|
|
165
|
+
extractHostname(url) {
|
|
166
|
+
try {
|
|
167
|
+
const parsed = new URL(url);
|
|
168
|
+
return parsed.hostname;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return url;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.PageLoadDetection = PageLoadDetection;
|
|
@@ -97,8 +97,10 @@ class ProxyServer {
|
|
|
97
97
|
this.logger.debug('Could not extract hostname, going direct');
|
|
98
98
|
return undefined;
|
|
99
99
|
}
|
|
100
|
-
// Check if we should proxy this hostname
|
|
101
|
-
const useProxy =
|
|
100
|
+
// Check if we should proxy this hostname (use pre-normalized rules if available)
|
|
101
|
+
const useProxy = config.normalizedRules
|
|
102
|
+
? (0, rules_js_1.shouldProxyNormalized)(hostname, config.normalizedRules)
|
|
103
|
+
: (0, rules_js_1.shouldProxy)(hostname, config.rules);
|
|
102
104
|
if (!useProxy) {
|
|
103
105
|
this.logger.debug(`Hostname ${hostname} bypassing proxy (direct)`);
|
|
104
106
|
return undefined;
|
package/dist/cjs/client/rules.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Rule engine for hostname matching and proxy decision
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.matchPattern = matchPattern;
|
|
5
|
+
exports.normalizeRules = normalizeRules;
|
|
6
|
+
exports.shouldProxyNormalized = shouldProxyNormalized;
|
|
5
7
|
exports.shouldProxy = shouldProxy;
|
|
6
8
|
/**
|
|
7
9
|
* Match a hostname against a pattern.
|
|
@@ -55,48 +57,26 @@ function matchPattern(hostname, pattern) {
|
|
|
55
57
|
return false;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Rules semantics:
|
|
61
|
-
* - [] (empty) → no proxy (return false)
|
|
62
|
-
* - ['*'] → proxy everything
|
|
63
|
-
* - ['example.com'] → proxy only example.com
|
|
64
|
-
* - ['*.google.com'] → proxy subdomains of google.com
|
|
65
|
-
* - ['*', '-example.com'] → proxy everything except example.com
|
|
66
|
-
* - ['AUTO', 'example.com'] → AUTO is placeholder (ignored), proxy example.com
|
|
67
|
-
*
|
|
68
|
-
* Negative patterns (prefixed with '-') exclude hosts from proxying.
|
|
69
|
-
* If '*' is in rules, default is to proxy unless excluded.
|
|
70
|
-
* Without '*', only explicitly matched patterns are proxied.
|
|
71
|
-
*
|
|
72
|
-
* @param hostname - The hostname to check
|
|
73
|
-
* @param rules - Array of rule patterns
|
|
74
|
-
* @returns true if the hostname should be proxied
|
|
60
|
+
* Pre-process raw rule strings into a NormalizedRules structure.
|
|
61
|
+
* Call once when config is loaded, then use shouldProxyNormalized() per request.
|
|
75
62
|
*/
|
|
76
|
-
function
|
|
77
|
-
const normalizedHostname = hostname.trim();
|
|
78
|
-
if (!normalizedHostname)
|
|
79
|
-
return false;
|
|
80
|
-
// Empty rules means no proxy
|
|
63
|
+
function normalizeRules(rules) {
|
|
81
64
|
if (!rules || rules.length === 0) {
|
|
82
|
-
return false;
|
|
65
|
+
return { positiveRules: [], negativeRules: [], hasCatchAll: false, empty: true };
|
|
83
66
|
}
|
|
84
|
-
const
|
|
67
|
+
const trimmed = rules
|
|
85
68
|
.filter((r) => typeof r === 'string')
|
|
86
|
-
.map((r) => r.trim())
|
|
87
|
-
.filter((r) => r.length > 0)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (effectiveRules.length === 0) {
|
|
92
|
-
return false;
|
|
69
|
+
.map((r) => r.trim().toLowerCase())
|
|
70
|
+
.filter((r) => r.length > 0)
|
|
71
|
+
.filter((r) => r !== 'auto');
|
|
72
|
+
if (trimmed.length === 0) {
|
|
73
|
+
return { positiveRules: [], negativeRules: [], hasCatchAll: false, empty: true };
|
|
93
74
|
}
|
|
94
|
-
// Separate positive and negative rules
|
|
95
75
|
const negativeRules = [];
|
|
96
76
|
const positiveRules = [];
|
|
97
|
-
for (const rule of
|
|
77
|
+
for (const rule of trimmed) {
|
|
98
78
|
if (rule.startsWith('-')) {
|
|
99
|
-
const neg = rule.slice(1).trim();
|
|
79
|
+
const neg = rule.slice(1).trim();
|
|
100
80
|
if (neg.length > 0)
|
|
101
81
|
negativeRules.push(neg);
|
|
102
82
|
}
|
|
@@ -104,25 +84,34 @@ function shouldProxy(hostname, rules) {
|
|
|
104
84
|
positiveRules.push(rule);
|
|
105
85
|
}
|
|
106
86
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
return {
|
|
88
|
+
positiveRules,
|
|
89
|
+
negativeRules,
|
|
90
|
+
hasCatchAll: positiveRules.includes('*'),
|
|
91
|
+
empty: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fast proxy decision using pre-normalized rules.
|
|
96
|
+
*/
|
|
97
|
+
function shouldProxyNormalized(hostname, rules) {
|
|
98
|
+
const normalizedHostname = hostname.trim().toLowerCase();
|
|
99
|
+
if (!normalizedHostname)
|
|
100
|
+
return false;
|
|
101
|
+
if (rules.empty)
|
|
102
|
+
return false;
|
|
103
|
+
for (const negRule of rules.negativeRules) {
|
|
104
|
+
if (matchPattern(normalizedHostname, negRule))
|
|
111
105
|
return false;
|
|
112
|
-
}
|
|
113
106
|
}
|
|
114
|
-
|
|
115
|
-
const hasCatchAll = positiveRules.includes('*');
|
|
116
|
-
if (hasCatchAll) {
|
|
117
|
-
// With catch-all, proxy everything not excluded by negative rules
|
|
107
|
+
if (rules.hasCatchAll)
|
|
118
108
|
return true;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for (const posRule of positiveRules) {
|
|
122
|
-
if (matchPattern(normalizedHostname, posRule)) {
|
|
109
|
+
for (const posRule of rules.positiveRules) {
|
|
110
|
+
if (matchPattern(normalizedHostname, posRule))
|
|
123
111
|
return true;
|
|
124
|
-
}
|
|
125
112
|
}
|
|
126
|
-
// No match found
|
|
127
113
|
return false;
|
|
128
114
|
}
|
|
115
|
+
function shouldProxy(hostname, rules) {
|
|
116
|
+
return shouldProxyNormalized(hostname, normalizeRules(rules));
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.connect = connect;
|
|
37
|
+
const lock_js_1 = require("./session/lock.js");
|
|
38
|
+
const errors_js_1 = require("./errors.js");
|
|
39
|
+
/**
|
|
40
|
+
* Connect to a running Aluvia browser session via CDP.
|
|
41
|
+
*
|
|
42
|
+
* - No args: auto-discovers a single running session.
|
|
43
|
+
* - With session name: connects to that specific session.
|
|
44
|
+
*
|
|
45
|
+
* Requires `playwright` as a peer dependency.
|
|
46
|
+
*/
|
|
47
|
+
async function connect(sessionName) {
|
|
48
|
+
// 1. Import Playwright
|
|
49
|
+
let pw;
|
|
50
|
+
try {
|
|
51
|
+
pw = await Promise.resolve().then(() => __importStar(require('playwright')));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new errors_js_1.ConnectError('Playwright is required for connect(). Install it: npm install playwright');
|
|
55
|
+
}
|
|
56
|
+
// 2. Resolve session
|
|
57
|
+
let resolvedName;
|
|
58
|
+
if (sessionName) {
|
|
59
|
+
resolvedName = sessionName;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const sessions = (0, lock_js_1.listSessions)();
|
|
63
|
+
if (sessions.length === 0) {
|
|
64
|
+
throw new errors_js_1.ConnectError('No running Aluvia sessions found. Start one with: npx aluvia-sdk session start <url>');
|
|
65
|
+
}
|
|
66
|
+
if (sessions.length > 1) {
|
|
67
|
+
const names = sessions.map((s) => s.session).join(', ');
|
|
68
|
+
throw new errors_js_1.ConnectError(`Multiple Aluvia sessions running (${names}). Specify which one: connect('${sessions[0].session}')`);
|
|
69
|
+
}
|
|
70
|
+
resolvedName = sessions[0].session;
|
|
71
|
+
}
|
|
72
|
+
// 3. Validate session state
|
|
73
|
+
const lock = (0, lock_js_1.readLock)(resolvedName);
|
|
74
|
+
if (!lock) {
|
|
75
|
+
throw new errors_js_1.ConnectError(`No Aluvia session found named '${resolvedName}'. Run 'npx aluvia-sdk session list' to list sessions.`);
|
|
76
|
+
}
|
|
77
|
+
if (!(0, lock_js_1.isProcessAlive)(lock.pid)) {
|
|
78
|
+
(0, lock_js_1.removeLock)(resolvedName);
|
|
79
|
+
throw new errors_js_1.ConnectError(`Session '${resolvedName}' is no longer running. Stale lock file removed.`);
|
|
80
|
+
}
|
|
81
|
+
if (!lock.ready) {
|
|
82
|
+
throw new errors_js_1.ConnectError(`Session '${resolvedName}' is still starting up. Try again shortly.`);
|
|
83
|
+
}
|
|
84
|
+
if (!lock.cdpUrl) {
|
|
85
|
+
throw new errors_js_1.ConnectError(`Session '${resolvedName}' has no CDP URL.`);
|
|
86
|
+
}
|
|
87
|
+
// 4. Connect over CDP
|
|
88
|
+
let browser;
|
|
89
|
+
try {
|
|
90
|
+
browser = await pw.chromium.connectOverCDP(lock.cdpUrl);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw new errors_js_1.ConnectError(`Failed to connect to session '${resolvedName}' at ${lock.cdpUrl}: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
// 5. Get context and page
|
|
96
|
+
let context;
|
|
97
|
+
let page;
|
|
98
|
+
try {
|
|
99
|
+
context = browser.contexts()[0] ?? await browser.newContext();
|
|
100
|
+
page = context.pages()[0] ?? await context.newPage();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
await browser.close().catch(() => { });
|
|
104
|
+
throw new errors_js_1.ConnectError(`Connected but failed to get page: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
browser,
|
|
108
|
+
context,
|
|
109
|
+
page,
|
|
110
|
+
sessionName: resolvedName,
|
|
111
|
+
cdpUrl: lock.cdpUrl,
|
|
112
|
+
connectionId: lock.connectionId,
|
|
113
|
+
disconnect: async () => {
|
|
114
|
+
await browser.close();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Error classes for Aluvia Client
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = void 0;
|
|
4
|
+
exports.ConnectError = exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* Thrown when the apiKey is not provided to AluviaClient.
|
|
7
7
|
*/
|
|
@@ -47,3 +47,14 @@ class ProxyStartError extends Error {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
exports.ProxyStartError = ProxyStartError;
|
|
50
|
+
/**
|
|
51
|
+
* Thrown by connect() when it cannot establish a CDP connection to a running session.
|
|
52
|
+
*/
|
|
53
|
+
class ConnectError extends Error {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'ConnectError';
|
|
57
|
+
Object.setPrototypeOf(this, ConnectError.prototype);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.ConnectError = ConnectError;
|
package/dist/cjs/index.js
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
// Aluvia Client Node
|
|
3
3
|
// Main entry point
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = exports.AluviaApi = exports.AluviaClient = void 0;
|
|
5
|
+
exports.ConnectError = exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = exports.connect = exports.AluviaApi = exports.AluviaClient = void 0;
|
|
6
6
|
// Public class
|
|
7
7
|
var AluviaClient_js_1 = require("./client/AluviaClient.js");
|
|
8
8
|
Object.defineProperty(exports, "AluviaClient", { enumerable: true, get: function () { return AluviaClient_js_1.AluviaClient; } });
|
|
9
9
|
var AluviaApi_js_1 = require("./api/AluviaApi.js");
|
|
10
10
|
Object.defineProperty(exports, "AluviaApi", { enumerable: true, get: function () { return AluviaApi_js_1.AluviaApi; } });
|
|
11
|
+
// Connect helper
|
|
12
|
+
var connect_js_1 = require("./connect.js");
|
|
13
|
+
Object.defineProperty(exports, "connect", { enumerable: true, get: function () { return connect_js_1.connect; } });
|
|
11
14
|
// Public error classes
|
|
12
15
|
var errors_js_1 = require("./errors.js");
|
|
13
16
|
Object.defineProperty(exports, "MissingApiKeyError", { enumerable: true, get: function () { return errors_js_1.MissingApiKeyError; } });
|
|
14
17
|
Object.defineProperty(exports, "InvalidApiKeyError", { enumerable: true, get: function () { return errors_js_1.InvalidApiKeyError; } });
|
|
15
18
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return errors_js_1.ApiError; } });
|
|
16
19
|
Object.defineProperty(exports, "ProxyStartError", { enumerable: true, get: function () { return errors_js_1.ProxyStartError; } });
|
|
20
|
+
Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.writeLock = writeLock;
|
|
37
|
+
exports.readLock = readLock;
|
|
38
|
+
exports.removeLock = removeLock;
|
|
39
|
+
exports.isProcessAlive = isProcessAlive;
|
|
40
|
+
exports.getLogFilePath = getLogFilePath;
|
|
41
|
+
exports.validateSessionName = validateSessionName;
|
|
42
|
+
exports.generateSessionName = generateSessionName;
|
|
43
|
+
exports.toLockData = toLockData;
|
|
44
|
+
exports.listSessions = listSessions;
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const os = __importStar(require("node:os"));
|
|
48
|
+
const LOCK_DIR = path.join(os.tmpdir(), 'aluvia-sdk');
|
|
49
|
+
const ADJECTIVES = [
|
|
50
|
+
'swift', 'bold', 'calm', 'keen', 'warm', 'bright', 'silent', 'rapid', 'steady', 'clever',
|
|
51
|
+
'vivid', 'agile', 'noble', 'lucid', 'crisp', 'gentle', 'fierce', 'nimble', 'sturdy', 'witty',
|
|
52
|
+
];
|
|
53
|
+
const NOUNS = [
|
|
54
|
+
'falcon', 'tiger', 'river', 'maple', 'coral', 'cedar', 'orbit', 'prism', 'flint', 'spark',
|
|
55
|
+
'ridge', 'ember', 'crane', 'grove', 'stone', 'brook', 'drift', 'crest', 'sage', 'lynx',
|
|
56
|
+
];
|
|
57
|
+
function lockFileName(sessionName) {
|
|
58
|
+
return `cli-${sessionName ?? 'default'}.lock`;
|
|
59
|
+
}
|
|
60
|
+
function logFileName(sessionName) {
|
|
61
|
+
return `cli-${sessionName ?? 'default'}.log`;
|
|
62
|
+
}
|
|
63
|
+
function writeLock(data, sessionName) {
|
|
64
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
65
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
66
|
+
const tmpPath = filePath + '.tmp';
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data), 'utf-8');
|
|
69
|
+
try {
|
|
70
|
+
// On Windows, rename may fail to overwrite an existing file; remove it first.
|
|
71
|
+
fs.rmSync(filePath, { force: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore errors removing the existing lock file.
|
|
75
|
+
}
|
|
76
|
+
fs.renameSync(tmpPath, filePath);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
try {
|
|
80
|
+
// Ensure no leftover temp file remains if rename fails.
|
|
81
|
+
fs.rmSync(tmpPath, { force: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore cleanup errors.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function readLock(sessionName) {
|
|
89
|
+
try {
|
|
90
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
91
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (typeof parsed.pid === 'number' && Number.isFinite(parsed.pid)) {
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function removeLock(sessionName) {
|
|
103
|
+
try {
|
|
104
|
+
const filePath = path.join(LOCK_DIR, lockFileName(sessionName));
|
|
105
|
+
fs.unlinkSync(filePath);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isProcessAlive(pid) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function getLogFilePath(sessionName) {
|
|
121
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
122
|
+
return path.join(LOCK_DIR, logFileName(sessionName));
|
|
123
|
+
}
|
|
124
|
+
function validateSessionName(name) {
|
|
125
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
126
|
+
}
|
|
127
|
+
function generateSessionName() {
|
|
128
|
+
const maxAttempts = 10;
|
|
129
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
130
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
131
|
+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
132
|
+
const name = attempt === 0 ? `${adj}-${noun}` : `${adj}-${noun}-${attempt}`;
|
|
133
|
+
const filePath = path.join(LOCK_DIR, lockFileName(name));
|
|
134
|
+
if (!fs.existsSync(filePath)) {
|
|
135
|
+
return name;
|
|
136
|
+
}
|
|
137
|
+
// Lock file exists — check if the process is still alive
|
|
138
|
+
const lock = readLock(name);
|
|
139
|
+
if (!lock || !isProcessAlive(lock.pid)) {
|
|
140
|
+
// Stale lock, we can reuse this name
|
|
141
|
+
removeLock(name);
|
|
142
|
+
return name;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Fallback: use timestamp
|
|
146
|
+
return `session-${Date.now()}`;
|
|
147
|
+
}
|
|
148
|
+
function toLockData(info) {
|
|
149
|
+
const { session: _session, ...lock } = info;
|
|
150
|
+
return lock;
|
|
151
|
+
}
|
|
152
|
+
function listSessions() {
|
|
153
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
154
|
+
const files = fs.readdirSync(LOCK_DIR);
|
|
155
|
+
const sessions = [];
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const match = file.match(/^cli-(.+)\.lock$/);
|
|
158
|
+
if (!match)
|
|
159
|
+
continue;
|
|
160
|
+
const sessionName = match[1];
|
|
161
|
+
const lock = readLock(sessionName);
|
|
162
|
+
if (!lock) {
|
|
163
|
+
// Corrupt lock file, clean up
|
|
164
|
+
removeLock(sessionName);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!isProcessAlive(lock.pid)) {
|
|
168
|
+
// Stale lock, clean up
|
|
169
|
+
removeLock(sessionName);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
sessions.push({
|
|
173
|
+
session: sessionName,
|
|
174
|
+
pid: lock.pid,
|
|
175
|
+
connectionId: lock.connectionId,
|
|
176
|
+
cdpUrl: lock.cdpUrl,
|
|
177
|
+
proxyUrl: lock.proxyUrl,
|
|
178
|
+
url: lock.url,
|
|
179
|
+
ready: lock.ready,
|
|
180
|
+
blockDetection: lock.blockDetection,
|
|
181
|
+
autoUnblock: lock.autoUnblock,
|
|
182
|
+
lastDetection: lock.lastDetection,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return sessions;
|
|
186
|
+
}
|