@xbrowser/cli 1.9.1 → 1.9.3

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.
@@ -0,0 +1,9 @@
1
+ import {
2
+ detectAntiBot,
3
+ formatDetectionMessage
4
+ } from "./chunk-DPJNCMLC.js";
5
+ import "./chunk-KFQGP6VL.js";
6
+ export {
7
+ detectAntiBot,
8
+ formatDetectionMessage
9
+ };
@@ -0,0 +1,145 @@
1
+ // src/screencast.ts
2
+ var ScreencastCapturer = class {
3
+ interval;
4
+ quality;
5
+ type;
6
+ maxWidth;
7
+ maxHeight;
8
+ isCapturing = false;
9
+ frameCallback = null;
10
+ // CDP Cast state
11
+ cdpSession = null;
12
+ sessionId = "";
13
+ // Fallback polling state
14
+ fallbackTimer = null;
15
+ constructor(options = {}) {
16
+ this.interval = options.interval || 100;
17
+ this.quality = options.quality || 60;
18
+ this.type = options.type || "jpeg";
19
+ this.maxWidth = options.width || 1920;
20
+ this.maxHeight = options.height || 1080;
21
+ }
22
+ /**
23
+ * Start screencast capture using CDP Page.startScreencast.
24
+ *
25
+ * If CDP session creation fails (non-Chromium, restricted access),
26
+ * automatically falls back to `page.screenshot()` polling.
27
+ */
28
+ async startCapture(page, sessionId, onFrame) {
29
+ if (this.isCapturing) {
30
+ throw new Error("Screencast is already capturing");
31
+ }
32
+ this.isCapturing = true;
33
+ this.frameCallback = onFrame;
34
+ this.sessionId = sessionId;
35
+ try {
36
+ const cdp = await page.context().newCDPSession(page);
37
+ this.cdpSession = cdp;
38
+ cdp.on("Page.screencastFrame", async (params) => {
39
+ if (!this.frameCallback) return;
40
+ try {
41
+ await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
42
+ } catch {
43
+ }
44
+ const viewport = {
45
+ width: params.metadata?.deviceWidth || this.maxWidth,
46
+ height: params.metadata?.deviceHeight || this.maxHeight
47
+ };
48
+ this.frameCallback({
49
+ id: crypto.randomUUID(),
50
+ sessionId: this.sessionId,
51
+ timestamp: Date.now(),
52
+ data: Buffer.from(params.data, "base64"),
53
+ url: page.url(),
54
+ viewport
55
+ });
56
+ });
57
+ await cdp.send("Page.startScreencast", {
58
+ format: this.type === "png" ? "png" : "jpeg",
59
+ quality: this.type === "jpeg" ? this.quality : void 0,
60
+ maxWidth: this.maxWidth,
61
+ maxHeight: this.maxHeight
62
+ });
63
+ } catch {
64
+ this.cdpSession = null;
65
+ this.startFallbackPolling(page, sessionId);
66
+ }
67
+ }
68
+ /**
69
+ * Fallback: periodic page.screenshot() polling when CDP Cast is unavailable.
70
+ */
71
+ startFallbackPolling(page, sessionId) {
72
+ const captureLoop = async () => {
73
+ if (!this.frameCallback) return;
74
+ try {
75
+ const frame = await this.captureFrame(page, sessionId);
76
+ this.frameCallback(frame);
77
+ } catch {
78
+ }
79
+ };
80
+ captureLoop();
81
+ this.fallbackTimer = setInterval(captureLoop, this.interval);
82
+ }
83
+ /**
84
+ * Capture a single screenshot frame from the page (fallback mode).
85
+ */
86
+ async captureFrame(page, sessionId) {
87
+ let viewport = page.viewportSize();
88
+ if (!viewport) {
89
+ try {
90
+ viewport = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
91
+ } catch {
92
+ viewport = { width: 1920, height: 1080 };
93
+ }
94
+ }
95
+ const screenshot = await page.screenshot({
96
+ type: this.type,
97
+ quality: this.type === "jpeg" ? this.quality : void 0
98
+ });
99
+ return {
100
+ id: crypto.randomUUID(),
101
+ sessionId,
102
+ timestamp: Date.now(),
103
+ data: screenshot,
104
+ url: page.url(),
105
+ viewport: viewport || { width: 0, height: 0 }
106
+ };
107
+ }
108
+ /**
109
+ * Stop the current screencast capture.
110
+ */
111
+ async stopCapture() {
112
+ if (this.cdpSession) {
113
+ try {
114
+ this.cdpSession.off("Page.screencastFrame", () => {
115
+ });
116
+ await this.cdpSession.send("Page.stopScreencast");
117
+ } catch {
118
+ }
119
+ try {
120
+ await this.cdpSession.detach();
121
+ } catch {
122
+ }
123
+ this.cdpSession = null;
124
+ }
125
+ if (this.fallbackTimer) {
126
+ clearInterval(this.fallbackTimer);
127
+ this.fallbackTimer = null;
128
+ }
129
+ this.isCapturing = false;
130
+ this.frameCallback = null;
131
+ }
132
+ isActive() {
133
+ return this.isCapturing;
134
+ }
135
+ setInterval(interval) {
136
+ this.interval = interval;
137
+ }
138
+ setQuality(quality) {
139
+ this.quality = quality;
140
+ }
141
+ };
142
+
143
+ export {
144
+ ScreencastCapturer
145
+ };
@@ -0,0 +1,246 @@
1
+ // src/lib/anti-bot.ts
2
+ var CAPTCHA_PATTERNS = [
3
+ { name: "reCAPTCHA", pattern: /recaptcha/i },
4
+ { name: "hCaptcha", pattern: /hcaptcha/i },
5
+ { name: "Turnstile", pattern: /turnstile/i },
6
+ { name: "Cloudflare", pattern: /cloudflare/i },
7
+ { name: "FunCaptcha", pattern: /funcaptcha/i },
8
+ { name: "AWS WAF", pattern: /aws.*challenge/i }
9
+ ];
10
+ var WARNING_TEXTS = [
11
+ { text: "detected as bot", severity: "high" },
12
+ { text: "suspicious activity", severity: "high" },
13
+ { text: "unusual traffic", severity: "high" },
14
+ { text: "please verify you are human", severity: "medium" },
15
+ { text: "access denied", severity: "high" },
16
+ { text: "blocked", severity: "high" },
17
+ { text: "rate limit", severity: "medium" },
18
+ { text: "too many requests", severity: "medium" },
19
+ { text: "\u9A8C\u8BC1", severity: "low" },
20
+ { text: "\u9A8C\u8BC1\u7801", severity: "low" }
21
+ ];
22
+ var BLOCKED_URL_PATTERNS = [
23
+ { name: "Cloudflare Challenge", pattern: /challenge-platform/i },
24
+ { name: "Cloudflare Captcha", pattern: /cf-challenge/i },
25
+ { name: "AWS WAF", pattern: /aws-waf/i },
26
+ { name: "Generic Captcha", pattern: /captcha/i }
27
+ ];
28
+ async function detectAntiBot(page, config = {}) {
29
+ const {
30
+ checkCaptcha = true,
31
+ checkWarning = true,
32
+ checkBlocked = true,
33
+ checkLogin = false,
34
+ checkWebdriver = true
35
+ } = config;
36
+ const result = {
37
+ detected: false
38
+ };
39
+ if (checkCaptcha) {
40
+ const captchaResult = await detectCaptcha(page);
41
+ if (captchaResult.detected) {
42
+ return captchaResult;
43
+ }
44
+ }
45
+ if (checkWarning) {
46
+ const warningResult = await detectWarningText(page);
47
+ if (warningResult.detected) {
48
+ return warningResult;
49
+ }
50
+ }
51
+ if (checkBlocked) {
52
+ const blockedResult = await detectBlockedPage(page);
53
+ if (blockedResult.detected) {
54
+ return blockedResult;
55
+ }
56
+ }
57
+ if (checkWebdriver) {
58
+ const webdriverResult = await detectWebdriverExposure(page);
59
+ if (webdriverResult.detected) {
60
+ return webdriverResult;
61
+ }
62
+ }
63
+ if (checkLogin) {
64
+ const loginResult = await detectLoginRequired(page);
65
+ if (loginResult.detected) {
66
+ return loginResult;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ async function detectCaptcha(page) {
72
+ try {
73
+ const iframes = await page.frames();
74
+ for (const iframe of iframes) {
75
+ const src = iframe.url();
76
+ for (const pattern of CAPTCHA_PATTERNS) {
77
+ if (pattern.pattern.test(src)) {
78
+ return {
79
+ detected: true,
80
+ type: "captcha",
81
+ severity: "high",
82
+ message: `Detected ${pattern.name} CAPTCHA (iframe: ${src})`,
83
+ selector: `iframe[src*="${pattern.name.toLowerCase()}"]`,
84
+ actionRequired: "manual"
85
+ };
86
+ }
87
+ }
88
+ }
89
+ const captchaSelectors = [
90
+ ".g-recaptcha",
91
+ "#captcha",
92
+ '[class*="captcha"]',
93
+ '[id*="captcha"]',
94
+ 'iframe[src*="recaptcha"]',
95
+ 'iframe[src*="hcaptcha"]',
96
+ 'iframe[src*="turnstile"]'
97
+ ];
98
+ for (const selector of captchaSelectors) {
99
+ const element = await page.$(selector).catch(() => null);
100
+ if (element) {
101
+ return {
102
+ detected: true,
103
+ type: "captcha",
104
+ severity: "high",
105
+ message: `CAPTCHA element found: ${selector}`,
106
+ selector,
107
+ actionRequired: "manual"
108
+ };
109
+ }
110
+ }
111
+ return { detected: false };
112
+ } catch {
113
+ return { detected: false };
114
+ }
115
+ }
116
+ async function detectWarningText(page) {
117
+ try {
118
+ const pageText = await page.textContent("body").catch(() => "") || "";
119
+ const lowerText = pageText.toLowerCase();
120
+ for (const { text, severity } of WARNING_TEXTS) {
121
+ if (lowerText.includes(text.toLowerCase())) {
122
+ return {
123
+ detected: true,
124
+ type: "warning",
125
+ severity,
126
+ message: `Anti-bot warning text found: "${text}"`,
127
+ actionRequired: severity === "high" ? "manual" : "retry"
128
+ };
129
+ }
130
+ }
131
+ return { detected: false };
132
+ } catch {
133
+ return { detected: false };
134
+ }
135
+ }
136
+ async function detectBlockedPage(page) {
137
+ try {
138
+ const url = page.url();
139
+ for (const { name, pattern } of BLOCKED_URL_PATTERNS) {
140
+ if (pattern.test(url)) {
141
+ return {
142
+ detected: true,
143
+ type: "blocked",
144
+ severity: "high",
145
+ message: `Blocked page detected: ${name} (${url})`,
146
+ actionRequired: "manual"
147
+ };
148
+ }
149
+ }
150
+ return { detected: false };
151
+ } catch {
152
+ return { detected: false };
153
+ }
154
+ }
155
+ async function detectWebdriverExposure(page) {
156
+ try {
157
+ const webdriver = await page.evaluate(() => {
158
+ return {
159
+ webdriver: navigator.webdriver,
160
+ webdriverScriptFn: !!window.__webdriver_script_fn,
161
+ webdriverEvaluate: !!window.__webdriver_evaluate,
162
+ chrome: !!window.chrome,
163
+ permissions: navigator.permissions
164
+ };
165
+ }).catch(() => null);
166
+ if (!webdriver) {
167
+ return { detected: false };
168
+ }
169
+ const issues = [];
170
+ if (webdriver.webdriver === true) {
171
+ issues.push("navigator.webdriver === true");
172
+ }
173
+ if (webdriver.webdriverScriptFn) {
174
+ issues.push("__webdriver_script_fn present");
175
+ }
176
+ if (webdriver.webdriverEvaluate) {
177
+ issues.push("__webdriver_evaluate present");
178
+ }
179
+ if (!webdriver.chrome) {
180
+ issues.push("window.chrome missing");
181
+ }
182
+ if (!webdriver.permissions) {
183
+ issues.push("navigator.permissions missing");
184
+ }
185
+ if (issues.length > 0) {
186
+ return {
187
+ detected: true,
188
+ type: "warning",
189
+ severity: "high",
190
+ message: `Automation markers exposed: ${issues.join(", ")}`,
191
+ actionRequired: "manual"
192
+ };
193
+ }
194
+ return { detected: false };
195
+ } catch {
196
+ return { detected: false };
197
+ }
198
+ }
199
+ async function detectLoginRequired(page) {
200
+ try {
201
+ const loginSelectors = [
202
+ 'a[href*="login"]',
203
+ 'a[href*="signin"]',
204
+ 'a[href*="sign-in"]',
205
+ 'button:has-text("Log in")',
206
+ 'button:has-text("Sign in")',
207
+ 'button:has-text("Login")',
208
+ 'button:has-text("\u767B\u5F55")'
209
+ ];
210
+ for (const selector of loginSelectors) {
211
+ const element = await page.$(selector).catch(() => null);
212
+ if (element) {
213
+ const rect = await element.boundingBox().catch(() => null);
214
+ if (rect && rect.y < 200) {
215
+ return {
216
+ detected: true,
217
+ type: "login",
218
+ severity: "medium",
219
+ message: "Login button detected in page header",
220
+ selector,
221
+ actionRequired: "manual"
222
+ };
223
+ }
224
+ }
225
+ }
226
+ return { detected: false };
227
+ } catch {
228
+ return { detected: false };
229
+ }
230
+ }
231
+ function formatDetectionMessage(result) {
232
+ if (!result.detected) {
233
+ return "\u2705 No anti-bot detection detected.";
234
+ }
235
+ const emoji = result.severity === "high" ? "\u{1F6A8}" : result.severity === "medium" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
236
+ const action = result.actionRequired === "manual" ? "Please handle manually" : result.actionRequired === "retry" ? "Consider retrying with delay" : "Consider switching to a different session";
237
+ return `${emoji} Detection: ${result.message}
238
+ Type: ${result.type}
239
+ Severity: ${result.severity}
240
+ Action: ${action}`;
241
+ }
242
+
243
+ export {
244
+ detectAntiBot,
245
+ formatDetectionMessage
246
+ };
@@ -0,0 +1,93 @@
1
+ // src/config.ts
2
+ import { homedir, tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { loadConfig as coreLoadConfig, saveConfig as coreSaveConfig } from "@dyyz1993/xcli-core";
5
+ function getConfigSource() {
6
+ return { configDir: join(homedir() || tmpdir(), ".xbrowser") };
7
+ }
8
+ function loadConfig() {
9
+ return coreLoadConfig(getConfigSource());
10
+ }
11
+ function saveConfig(config) {
12
+ coreSaveConfig(getConfigSource(), config);
13
+ }
14
+ function getConfigValue(key) {
15
+ const parts = key.split(".");
16
+ let obj = loadConfig();
17
+ for (const part of parts) {
18
+ if (obj && typeof obj === "object") {
19
+ obj = obj[part];
20
+ } else {
21
+ return void 0;
22
+ }
23
+ }
24
+ return obj;
25
+ }
26
+ function setConfigValue(key, value) {
27
+ const config = loadConfig();
28
+ const parts = key.split(".");
29
+ let obj = config;
30
+ for (let i = 0; i < parts.length - 1; i++) {
31
+ if (!obj[parts[i]] || typeof obj[parts[i]] !== "object") {
32
+ obj[parts[i]] = {};
33
+ }
34
+ obj = obj[parts[i]];
35
+ }
36
+ obj[parts[parts.length - 1]] = value;
37
+ saveConfig(config);
38
+ }
39
+ var DEFAULT_MARKETPLACE_URL = "https://marketplace.xbrowser.dev";
40
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org";
41
+ var NPM_SCOPE = "@xbrowser/";
42
+ function getMarketplaceUrl() {
43
+ return process.env.XBROWSER_MARKETPLACE_URL || getConfigValue("marketplaceUrl") || DEFAULT_MARKETPLACE_URL;
44
+ }
45
+ function resolveNpmPackageName(name) {
46
+ if (name.startsWith("@")) return name;
47
+ return `${NPM_SCOPE}${name}`;
48
+ }
49
+ var NPM_NAME_ALIASES = {
50
+ "1688": "alibaba-1688"
51
+ };
52
+ function generateNpmCandidates(name) {
53
+ if (name.startsWith("@")) return [name];
54
+ const resolved = NPM_NAME_ALIASES[name] ?? name;
55
+ return [
56
+ `${NPM_SCOPE}xbrowser-plugin-${resolved}`,
57
+ `${NPM_SCOPE}${resolved}`,
58
+ `xbrowser-plugin-${resolved}`
59
+ ];
60
+ }
61
+ async function resolveNpmPackageWithFallback(name) {
62
+ const candidates = generateNpmCandidates(name);
63
+ for (const candidate of candidates) {
64
+ try {
65
+ const encoded = encodeURIComponent(candidate);
66
+ const res = await fetch(`${NPM_REGISTRY_URL}/${encoded}`);
67
+ if (res.ok) return candidate;
68
+ } catch {
69
+ continue;
70
+ }
71
+ }
72
+ return resolveNpmPackageName(name);
73
+ }
74
+ function getCaptchaConfig() {
75
+ const config = loadConfig();
76
+ return {
77
+ notifyUrl: process.env.XBROWSER_NOTIFY_URL || config.captcha?.notifyUrl,
78
+ autoOpen: process.env.XBROWSER_AUTO_OPEN === "true" || config.captcha?.autoOpen === true,
79
+ timeout: parseInt(process.env.XBROWSER_CAPTCHA_TIMEOUT || "") || config.captcha?.timeout || 120,
80
+ previewPort: parseInt(process.env.XBROWSER_PREVIEW_PORT || "") || config.preview?.port || 9223
81
+ };
82
+ }
83
+
84
+ export {
85
+ loadConfig,
86
+ getConfigValue,
87
+ setConfigValue,
88
+ NPM_REGISTRY_URL,
89
+ NPM_SCOPE,
90
+ getMarketplaceUrl,
91
+ resolveNpmPackageWithFallback,
92
+ getCaptchaConfig
93
+ };