@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.
Files changed (77) hide show
  1. package/README.md +409 -285
  2. package/dist/cjs/api/account.js +10 -74
  3. package/dist/cjs/api/apiUtils.js +80 -0
  4. package/dist/cjs/api/geos.js +2 -63
  5. package/dist/cjs/api/request.js +8 -2
  6. package/dist/cjs/bin/account.js +31 -0
  7. package/dist/cjs/bin/api-helpers.js +58 -0
  8. package/dist/cjs/bin/cli.js +245 -0
  9. package/dist/cjs/bin/close.js +120 -0
  10. package/dist/cjs/bin/geos.js +10 -0
  11. package/dist/cjs/bin/mcp-helpers.js +57 -0
  12. package/dist/cjs/bin/mcp-server.js +220 -0
  13. package/dist/cjs/bin/mcp-tools.js +90 -0
  14. package/dist/cjs/bin/open.js +293 -0
  15. package/dist/cjs/bin/session.js +259 -0
  16. package/dist/cjs/client/AluviaClient.js +365 -189
  17. package/dist/cjs/client/BlockDetection.js +486 -0
  18. package/dist/cjs/client/ConfigManager.js +26 -23
  19. package/dist/cjs/client/PageLoadDetection.js +175 -0
  20. package/dist/cjs/client/ProxyServer.js +4 -2
  21. package/dist/cjs/client/logger.js +4 -0
  22. package/dist/cjs/client/rules.js +38 -49
  23. package/dist/cjs/connect.js +117 -0
  24. package/dist/cjs/errors.js +12 -1
  25. package/dist/cjs/index.js +5 -1
  26. package/dist/cjs/session/lock.js +186 -0
  27. package/dist/esm/api/account.js +2 -66
  28. package/dist/esm/api/apiUtils.js +71 -0
  29. package/dist/esm/api/geos.js +2 -63
  30. package/dist/esm/api/request.js +8 -2
  31. package/dist/esm/bin/account.js +28 -0
  32. package/dist/esm/bin/api-helpers.js +53 -0
  33. package/dist/esm/bin/cli.js +242 -0
  34. package/dist/esm/bin/close.js +117 -0
  35. package/dist/esm/bin/geos.js +7 -0
  36. package/dist/esm/bin/mcp-helpers.js +51 -0
  37. package/dist/esm/bin/mcp-server.js +185 -0
  38. package/dist/esm/bin/mcp-tools.js +78 -0
  39. package/dist/esm/bin/open.js +256 -0
  40. package/dist/esm/bin/session.js +252 -0
  41. package/dist/esm/client/AluviaClient.js +371 -195
  42. package/dist/esm/client/BlockDetection.js +482 -0
  43. package/dist/esm/client/ConfigManager.js +21 -18
  44. package/dist/esm/client/PageLoadDetection.js +171 -0
  45. package/dist/esm/client/ProxyServer.js +5 -3
  46. package/dist/esm/client/logger.js +4 -0
  47. package/dist/esm/client/rules.js +36 -49
  48. package/dist/esm/connect.js +81 -0
  49. package/dist/esm/errors.js +10 -0
  50. package/dist/esm/index.js +5 -3
  51. package/dist/esm/session/lock.js +142 -0
  52. package/dist/types/api/AluviaApi.d.ts +2 -7
  53. package/dist/types/api/account.d.ts +1 -16
  54. package/dist/types/api/apiUtils.d.ts +28 -0
  55. package/dist/types/api/geos.d.ts +1 -1
  56. package/dist/types/bin/account.d.ts +1 -0
  57. package/dist/types/bin/api-helpers.d.ts +20 -0
  58. package/dist/types/bin/cli.d.ts +2 -0
  59. package/dist/types/bin/close.d.ts +1 -0
  60. package/dist/types/bin/geos.d.ts +1 -0
  61. package/dist/types/bin/mcp-helpers.d.ts +28 -0
  62. package/dist/types/bin/mcp-server.d.ts +2 -0
  63. package/dist/types/bin/mcp-tools.d.ts +46 -0
  64. package/dist/types/bin/open.d.ts +21 -0
  65. package/dist/types/bin/session.d.ts +11 -0
  66. package/dist/types/client/AluviaClient.d.ts +51 -4
  67. package/dist/types/client/BlockDetection.d.ts +96 -0
  68. package/dist/types/client/ConfigManager.d.ts +6 -1
  69. package/dist/types/client/PageLoadDetection.d.ts +93 -0
  70. package/dist/types/client/logger.d.ts +2 -0
  71. package/dist/types/client/rules.d.ts +18 -0
  72. package/dist/types/client/types.d.ts +48 -47
  73. package/dist/types/connect.d.ts +18 -0
  74. package/dist/types/errors.d.ts +6 -0
  75. package/dist/types/index.d.ts +7 -5
  76. package/dist/types/session/lock.d.ts +43 -0
  77. package/package.json +11 -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 = shouldProxy(hostname, config.rules);
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;
@@ -29,6 +29,10 @@ export class Logger {
29
29
  console.debug('[aluvia][debug]', ...args);
30
30
  }
31
31
  }
32
+ /** Check if debug logging is enabled. */
33
+ get isDebug() {
34
+ return this.level === 'debug';
35
+ }
32
36
  /**
33
37
  * Log warning messages.
34
38
  * Logs when level is not 'silent'.
@@ -51,48 +51,26 @@ export function matchPattern(hostname, pattern) {
51
51
  return false;
52
52
  }
53
53
  /**
54
- * Determine if a hostname should be proxied based on rules.
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 shouldProxy(hostname, rules) {
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 normalizedRules = rules
61
+ const trimmed = rules
81
62
  .filter((r) => typeof r === 'string')
82
- .map((r) => r.trim())
83
- .filter((r) => r.length > 0);
84
- // Filter out AUTO placeholder
85
- const effectiveRules = normalizedRules.filter((r) => r.toUpperCase() !== 'AUTO');
86
- // If no effective rules after filtering, no proxy
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 effectiveRules) {
71
+ for (const rule of trimmed) {
94
72
  if (rule.startsWith('-')) {
95
- const neg = rule.slice(1).trim(); // Remove the '-' prefix
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
- // Check if hostname matches any negative rule
104
- for (const negRule of negativeRules) {
105
- if (matchPattern(normalizedHostname, negRule)) {
106
- // Excluded by negative rule
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
- // Check if we have a catch-all '*'
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
- // Without catch-all, check if hostname matches any positive rule
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
+ }
@@ -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 './client/AluviaClient.js';
5
- export { AluviaApi } from './api/AluviaApi.js';
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 './errors.js';
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
- 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
- };
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
+ }>;
@@ -1,5 +1,5 @@
1
1
  import type { Geo } from './types.js';
2
- import type { ApiContext } from './account.js';
2
+ import type { ApiContext } from './apiUtils.js';
3
3
  export declare function createGeosApi(ctx: ApiContext): {
4
4
  list: () => Promise<Array<Geo>>;
5
5
  };
@@ -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;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function output(data: Record<string, unknown>, exitCode?: number): never;