@myrialabs/clopen 0.1.10 → 0.2.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 (71) hide show
  1. package/README.md +23 -1
  2. package/backend/index.ts +20 -0
  3. package/backend/lib/auth/auth-service.ts +484 -0
  4. package/backend/lib/auth/index.ts +4 -0
  5. package/backend/lib/auth/permissions.ts +63 -0
  6. package/backend/lib/auth/rate-limiter.ts +145 -0
  7. package/backend/lib/auth/tokens.ts +53 -0
  8. package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
  9. package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
  10. package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
  11. package/backend/lib/database/migrations/index.ts +21 -0
  12. package/backend/lib/database/queries/auth-queries.ts +201 -0
  13. package/backend/lib/database/queries/index.ts +2 -1
  14. package/backend/lib/engine/adapters/opencode/server.ts +1 -1
  15. package/backend/lib/mcp/config.ts +13 -18
  16. package/backend/lib/mcp/index.ts +9 -0
  17. package/backend/lib/mcp/remote-server.ts +132 -0
  18. package/backend/lib/mcp/servers/helper.ts +49 -3
  19. package/backend/lib/mcp/servers/index.ts +3 -2
  20. package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
  21. package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
  22. package/backend/lib/preview/browser/browser-pool.ts +73 -176
  23. package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
  24. package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
  25. package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
  26. package/backend/lib/utils/ws.ts +65 -1
  27. package/backend/ws/auth/index.ts +17 -0
  28. package/backend/ws/auth/invites.ts +84 -0
  29. package/backend/ws/auth/login.ts +269 -0
  30. package/backend/ws/auth/status.ts +41 -0
  31. package/backend/ws/auth/users.ts +32 -0
  32. package/backend/ws/engine/claude/accounts.ts +3 -1
  33. package/backend/ws/engine/utils.ts +38 -6
  34. package/backend/ws/index.ts +4 -4
  35. package/backend/ws/preview/browser/interact.ts +27 -5
  36. package/bin/clopen.ts +39 -0
  37. package/bun.lock +113 -51
  38. package/frontend/App.svelte +47 -29
  39. package/frontend/lib/components/auth/InvitePage.svelte +215 -0
  40. package/frontend/lib/components/auth/LoginPage.svelte +129 -0
  41. package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
  42. package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
  43. package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
  44. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
  45. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
  46. package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
  47. package/frontend/lib/components/settings/SettingsView.svelte +21 -7
  48. package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
  49. package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
  50. package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
  51. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
  52. package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
  53. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  54. package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
  55. package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
  56. package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
  57. package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
  58. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
  59. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
  60. package/frontend/lib/stores/features/auth.svelte.ts +296 -0
  61. package/frontend/lib/stores/features/settings.svelte.ts +53 -9
  62. package/frontend/lib/stores/features/user.svelte.ts +26 -68
  63. package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
  64. package/frontend/lib/stores/ui/update.svelte.ts +2 -2
  65. package/package.json +8 -6
  66. package/shared/types/stores/settings.ts +16 -2
  67. package/shared/utils/logger.ts +1 -0
  68. package/shared/utils/ws-client.ts +30 -13
  69. package/shared/utils/ws-server.ts +42 -4
  70. package/backend/lib/mcp/stdio-server.ts +0 -103
  71. package/backend/ws/mcp/index.ts +0 -61
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Remote MCP HTTP Server for Open Code
3
+ *
4
+ * Serves custom MCP tools over HTTP (Streamable HTTP transport) so Open Code
5
+ * can connect via `type: 'remote'` config instead of spawning a stdio subprocess.
6
+ *
7
+ * Tool handlers execute directly in the main Clopen process — no subprocess,
8
+ * no WebSocket bridge. This is architecturally identical to how Claude Code
9
+ * uses in-process MCP servers via createSdkMcpServer().
10
+ *
11
+ * Transport: WebStandardStreamableHTTPServerTransport (works natively with Bun)
12
+ */
13
+
14
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
15
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
16
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
+ import { createRemoteMcpServer } from './servers/helper';
18
+ import { debug } from '$shared/utils/logger';
19
+
20
+ // Lazy imports to avoid circular dependencies at module load time
21
+ let _allServers: Parameters<typeof createRemoteMcpServer>[0] | null = null;
22
+ let _enabledConfig: Parameters<typeof createRemoteMcpServer>[1] | null = null;
23
+
24
+ async function getServerDeps() {
25
+ if (!_allServers || !_enabledConfig) {
26
+ const { allServers } = await import('./servers/index');
27
+ const { mcpServersConfig } = await import('./config');
28
+ _allServers = allServers;
29
+ _enabledConfig = mcpServersConfig;
30
+ }
31
+ return { allServers: _allServers, enabledConfig: _enabledConfig };
32
+ }
33
+
34
+ // ============================================================================
35
+ // Session Management
36
+ // ============================================================================
37
+
38
+ /** Active transports keyed by MCP session ID */
39
+ const transports = new Map<string, WebStandardStreamableHTTPServerTransport>();
40
+
41
+ /** Active MCP servers keyed by MCP session ID */
42
+ const servers = new Map<string, McpServer>();
43
+
44
+ /**
45
+ * Handle an incoming MCP HTTP request (GET/POST/DELETE).
46
+ *
47
+ * Mounted at /mcp on the main Elysia server.
48
+ * Follows the Streamable HTTP transport protocol:
49
+ * - POST without session: initialization → create new transport + server
50
+ * - POST with session: route to existing transport
51
+ * - GET with session: SSE stream for server notifications
52
+ * - DELETE with session: close session
53
+ */
54
+ export async function handleMcpRequest(request: Request): Promise<Response> {
55
+ const sessionId = request.headers.get('mcp-session-id');
56
+
57
+ // Existing session — route to its transport
58
+ if (sessionId && transports.has(sessionId)) {
59
+ const transport = transports.get(sessionId)!;
60
+ return transport.handleRequest(request);
61
+ }
62
+
63
+ // New initialization request — create transport + MCP server
64
+ if (request.method === 'POST') {
65
+ // Parse body to check if it's an init request
66
+ const body = await request.json();
67
+
68
+ if (isInitializeRequest(body)) {
69
+ const { allServers, enabledConfig } = await getServerDeps();
70
+
71
+ const transport = new WebStandardStreamableHTTPServerTransport({
72
+ sessionIdGenerator: () => crypto.randomUUID(),
73
+ onsessioninitialized: (sid) => {
74
+ transports.set(sid, transport);
75
+ debug.log('mcp', `🌐 Remote MCP session initialized: ${sid}`);
76
+ },
77
+ onsessionclosed: (sid) => {
78
+ transports.delete(sid);
79
+ servers.delete(sid);
80
+ debug.log('mcp', `🌐 Remote MCP session closed: ${sid}`);
81
+ },
82
+ });
83
+
84
+ transport.onclose = () => {
85
+ if (transport.sessionId) {
86
+ transports.delete(transport.sessionId);
87
+ servers.delete(transport.sessionId);
88
+ }
89
+ };
90
+
91
+ // Create a fresh MCP server with all enabled tools (in-process handlers)
92
+ const mcpServer = createRemoteMcpServer(allServers, enabledConfig);
93
+ await mcpServer.connect(transport);
94
+
95
+ // Store server reference for cleanup
96
+ if (transport.sessionId) {
97
+ servers.set(transport.sessionId, mcpServer);
98
+ }
99
+
100
+ // Handle the initialization request with pre-parsed body
101
+ return transport.handleRequest(request, { parsedBody: body });
102
+ }
103
+ }
104
+
105
+ // Invalid request
106
+ return new Response(JSON.stringify({
107
+ jsonrpc: '2.0',
108
+ error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
109
+ id: null,
110
+ }), {
111
+ status: 400,
112
+ headers: { 'Content-Type': 'application/json' },
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Close all active MCP sessions and transports.
118
+ * Called during graceful server shutdown.
119
+ */
120
+ export async function closeMcpServer(): Promise<void> {
121
+ for (const [sessionId, transport] of transports) {
122
+ try {
123
+ await transport.close();
124
+ debug.log('mcp', `🌐 Remote MCP transport closed: ${sessionId}`);
125
+ } catch (error) {
126
+ debug.error('mcp', `Error closing MCP transport ${sessionId}:`, error);
127
+ }
128
+ }
129
+ transports.clear();
130
+ servers.clear();
131
+ debug.log('mcp', '🌐 All remote MCP sessions closed');
132
+ }
@@ -3,10 +3,14 @@
3
3
  *
4
4
  * Stores both the Claude SDK server instance AND raw tool definitions
5
5
  * so the same source can be used for Claude Code (in-process) and
6
- * Open Code (stdio subprocess via @modelcontextprotocol/sdk).
6
+ * Open Code (remote HTTP MCP via @modelcontextprotocol/sdk).
7
+ *
8
+ * Claude Code: createSdkMcpServer() → in-process MCP server
9
+ * Open Code: createRemoteMcpServer() → HTTP MCP server (same process, same handlers)
7
10
  */
8
11
 
9
12
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
14
  import type { z } from "zod";
11
15
 
12
16
  /**
@@ -35,8 +39,7 @@ type ToolHandler<TSchema extends Record<string, z.ZodType<any>> | undefined> =
35
39
  * Raw tool definition — schema, description, and handler.
36
40
  * Single source of truth used by:
37
41
  * - Claude Code: in-process via createSdkMcpServer
38
- * - Open Code stdio: schema/description for registration, handler via bridge
39
- * - MCP bridge: handler for in-process execution
42
+ * - Open Code: remote HTTP MCP via createRemoteMcpServer (in-process handlers)
40
43
  */
41
44
  export interface RawToolDef {
42
45
  description: string;
@@ -152,3 +155,46 @@ export function buildServerRegistries<
152
155
  }
153
156
  };
154
157
  }
158
+
159
+ // ============================================================================
160
+ // Remote MCP Server for Open Code (HTTP transport, in-process execution)
161
+ // ============================================================================
162
+
163
+ /**
164
+ * Create a McpServer instance (from @modelcontextprotocol/sdk) with tools registered
165
+ * from the same RawToolDef definitions used by Claude Code.
166
+ *
167
+ * This is the Open Code equivalent of createSdkMcpServer() for Claude Code.
168
+ * Handlers execute directly in-process — no subprocess, no bridge.
169
+ *
170
+ * @param servers - Server definitions from defineServer()
171
+ * @param enabledConfig - Which servers/tools are enabled (from mcpServersConfig)
172
+ */
173
+ export function createRemoteMcpServer(
174
+ servers: readonly ServerWithMeta<string, readonly string[]>[],
175
+ enabledConfig: Record<string, { enabled: boolean; tools: readonly string[] }>
176
+ ): McpServer {
177
+ const mcpServer = new McpServer({
178
+ name: 'clopen-mcp',
179
+ version: '1.0.0',
180
+ });
181
+
182
+ for (const srv of servers) {
183
+ const config = enabledConfig[srv.meta.name];
184
+ if (!config?.enabled) continue;
185
+
186
+ for (const toolName of config.tools) {
187
+ const def = srv.meta.toolDefs[toolName as string];
188
+ if (!def) continue;
189
+
190
+ mcpServer.registerTool(toolName as string, {
191
+ description: def.description,
192
+ inputSchema: def.schema,
193
+ }, async (args: Record<string, unknown>) => {
194
+ return await def.handler(args) as any;
195
+ });
196
+ }
197
+ }
198
+
199
+ return mcpServer;
200
+ }
@@ -14,8 +14,9 @@ import weather from './weather/index';
14
14
  import browserAutomation from './browser-automation/index';
15
15
  import { buildServerRegistries } from './helper';
16
16
 
17
- // Re-export types for stdio server
17
+ // Re-export types and remote server factory
18
18
  export type { RawToolDef } from './helper';
19
+ export { createRemoteMcpServer } from './helper';
19
20
 
20
21
  /**
21
22
  * All MCP Servers
@@ -23,7 +24,7 @@ export type { RawToolDef } from './helper';
23
24
  * Simply import and add new servers to this array.
24
25
  * Metadata and registry will be automatically built.
25
26
  */
26
- const allServers = [
27
+ export const allServers = [
27
28
  weather,
28
29
  browserAutomation,
29
30
  // Add more servers here...
@@ -11,14 +11,31 @@ import { audioCaptureScript } from './scripts/audio-stream';
11
11
 
12
12
  export class BrowserAudioCapture {
13
13
  /**
14
- * Setup audio capture for a page
15
- * Injects audio capture script that intercepts AudioContext before page loads
14
+ * Setup audio capture for a page (pre-navigation).
15
+ * WARNING: Uses evaluateOnNewDocument which patches AudioContext BEFORE page
16
+ * scripts run. This is detected by Cloudflare's fingerprinting.
17
+ * Prefer injectAudioCapture() for post-navigation injection.
16
18
  */
17
19
  async setupAudioCapture(page: Page, config: StreamingConfig['audio']): Promise<void> {
18
- // Inject audio capture script BEFORE page loads to intercept AudioContext
19
20
  await page.evaluateOnNewDocument(audioCaptureScript, config);
20
21
  }
21
22
 
23
+ /**
24
+ * Inject audio capture into the current page context (post-navigation).
25
+ * Uses page.evaluate() instead of evaluateOnNewDocument() to avoid
26
+ * Cloudflare detection — AudioContext constructor patching before page
27
+ * load is heavily flagged by CF's fingerprinting algorithms.
28
+ * Call this AFTER navigation completes and CF challenges pass.
29
+ */
30
+ async injectAudioCapture(page: Page, config: StreamingConfig['audio']): Promise<boolean> {
31
+ try {
32
+ await page.evaluate(audioCaptureScript, config);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
22
39
  /**
23
40
  * Check if audio encoder is supported in the page
24
41
  */
@@ -62,6 +62,8 @@ export class BrowserNavigationTracker extends EventEmitter {
62
62
  });
63
63
 
64
64
  // Track hash changes (fragment identifier changes like #contact-us)
65
+ // Temporarily disabled URL tracking injection to test CloudFlare evasion
66
+ /*
65
67
  await page.evaluateOnNewDocument(() => {
66
68
  let lastUrl = window.location.href;
67
69
 
@@ -86,6 +88,7 @@ export class BrowserNavigationTracker extends EventEmitter {
86
88
  // Periodically check for URL changes (for SPA navigation)
87
89
  setInterval(checkUrlChange, 500);
88
90
  });
91
+ */
89
92
  }
90
93
 
91
94
  async navigateSession(sessionId: string, session: BrowserTab, url: string): Promise<string> {
@@ -1,35 +1,32 @@
1
1
  /**
2
- * Browser Pool Module using puppeteer-cluster
2
+ * Browser Pool Module
3
3
  *
4
- * Uses puppeteer-cluster for efficient browser management with isolated contexts.
4
+ * Uses puppeteer-extra with StealthPlugin for Cloudflare bypass.
5
+ * Architecture mirrors the working test-cf.ts approach:
6
+ * - Single shared browser launched directly via puppeteer.launch()
7
+ * - Isolated BrowserContext per session (separate cookies, storage, cache)
8
+ * - StealthPlugin applied at launch time (via puppeteer-extra hooks)
5
9
  *
6
- * Architecture:
7
- * - puppeteer-cluster manages browser lifecycle and crash recovery
8
- * - CONCURRENCY_CONTEXT mode: shared browser, isolated contexts per worker
9
- * - Each user session gets its own isolated BrowserContext
10
- *
11
- * Isolation per session:
12
- * - Separate cookies
13
- * - Separate localStorage/sessionStorage
14
- * - Separate cache
15
- * - Separate service workers
16
- * - No data leakage between users
17
- *
18
- * Memory Usage:
19
- * - 1 user: ~300MB (browser) + ~20MB (context) = ~320MB
20
- * - 10 users: ~300MB (browser) + ~200MB (contexts) = ~500MB
21
- * - vs. old: 10 users = 10 browsers = 2-5GB
10
+ * Why not puppeteer-cluster?
11
+ * - Cluster's CONCURRENCY_CONTEXT mode accesses the browser via the raw
12
+ * underlying reference, bypassing puppeteer-extra's page creation hooks.
13
+ * - This causes a race condition where stealth evasions (evaluateOnNewDocument)
14
+ * may not be registered before the first navigation, breaking Cloudflare bypass.
15
+ * - Direct launch() ensures puppeteer-extra wraps ALL page creation correctly.
22
16
  */
23
17
 
24
- import { Cluster } from 'puppeteer-cluster';
25
18
  import type { Browser, BrowserContext, Page } from 'puppeteer';
26
19
  import { debug } from '$shared/utils/logger';
20
+ import puppeteer from 'puppeteer-extra';
21
+ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
22
+
23
+ puppeteer.use(StealthPlugin());
27
24
 
28
25
  export interface PoolConfig {
29
- maxConcurrency: number; // Maximum concurrent isolated contexts
30
- timeout: number; // Task timeout
31
- retryLimit: number; // Number of retries on failure
32
- retryDelay: number; // Delay between retries
26
+ maxConcurrency: number;
27
+ timeout: number;
28
+ retryLimit: number;
29
+ retryDelay: number;
33
30
  }
34
31
 
35
32
  export interface PooledSession {
@@ -40,150 +37,89 @@ export interface PooledSession {
40
37
  }
41
38
 
42
39
  const DEFAULT_CONFIG: PoolConfig = {
43
- maxConcurrency: 50, // Support up to 50 concurrent users
44
- timeout: 60000, // 60 second timeout
45
- retryLimit: 3, // Retry 3 times on failure
46
- retryDelay: 1000 // 1 second delay between retries
40
+ maxConcurrency: 50,
41
+ timeout: 60000,
42
+ retryLimit: 3,
43
+ retryDelay: 1000
47
44
  };
48
45
 
49
46
  /**
50
- * Optimized Chromium launch arguments for low-resource usage
47
+ * Chromium launch arguments for stealth (matches test-cf.ts exactly)
51
48
  */
52
49
  const CHROMIUM_ARGS = [
53
- // === CORE STABILITY (Windows compatible) ===
54
50
  '--no-sandbox',
55
- '--disable-dev-shm-usage',
56
- '--disable-gpu',
57
-
58
- // === PREVENT THROTTLING ===
59
- '--disable-background-timer-throttling',
60
- '--disable-backgrounding-occluded-windows',
61
- '--disable-renderer-backgrounding',
62
-
63
- // === DISABLE UNNECESSARY FEATURES ===
64
- '--no-first-run',
65
- '--no-default-browser-check',
66
- '--disable-extensions',
67
- '--disable-popup-blocking',
68
-
69
- // === LOW-END DEVICE OPTIMIZATIONS ===
70
- '--memory-pressure-off',
71
- '--disable-features=TranslateUI',
72
- '--disable-sync',
73
- '--disable-domain-reliability',
74
- '--disable-client-side-phishing-detection',
75
- '--disable-software-rasterizer',
76
- '--disable-smooth-scrolling',
77
- '--disable-threaded-animation',
78
- '--disable-threaded-scrolling',
79
- '--disable-composited-antialiasing',
80
- '--disable-webgl',
81
- '--disable-webgl2',
82
- '--disable-accelerated-2d-canvas',
83
- '--disable-gpu-vsync',
84
- '--disable-ipc-flooding-protection',
85
-
86
- // === AUDIO SUPPORT ===
87
- '--autoplay-policy=no-user-gesture-required',
88
- '--use-fake-ui-for-media-stream'
51
+ '--disable-blink-features=AutomationControlled',
52
+ '--window-size=1366,768'
89
53
  ];
90
54
 
91
55
  class BrowserPool {
92
- private cluster: Cluster | null = null;
56
+ private browser: Browser | null = null;
93
57
  private sessions = new Map<string, PooledSession>();
94
58
  private config: PoolConfig;
95
- private isInitializing = false;
96
- private initPromise: Promise<Cluster> | null = null;
59
+ private isLaunching = false;
60
+ private launchPromise: Promise<Browser> | null = null;
97
61
 
98
62
  constructor(config: Partial<PoolConfig> = {}) {
99
63
  this.config = { ...DEFAULT_CONFIG, ...config };
100
64
  }
101
65
 
102
66
  /**
103
- * Get or create the puppeteer-cluster instance
104
- * Thread-safe: multiple calls during initialization will wait for the same promise
67
+ * Get or create the shared browser instance.
68
+ * Uses puppeteer-extra directly (same as test-cf.ts) to ensure
69
+ * StealthPlugin hooks fire for every page created.
105
70
  */
106
- async getCluster(): Promise<Cluster> {
107
- if (this.cluster) {
108
- return this.cluster;
71
+ async getBrowser(): Promise<Browser> {
72
+ if (this.browser?.connected) {
73
+ return this.browser;
109
74
  }
110
75
 
111
- if (this.isInitializing && this.initPromise) {
112
- return this.initPromise;
76
+ if (this.isLaunching && this.launchPromise) {
77
+ return this.launchPromise;
113
78
  }
114
79
 
115
- this.isInitializing = true;
116
- this.initPromise = this.launchCluster();
80
+ this.isLaunching = true;
81
+ this.launchPromise = this.launchBrowser();
117
82
 
118
83
  try {
119
- this.cluster = await this.initPromise;
120
- return this.cluster;
84
+ this.browser = await this.launchPromise;
85
+ return this.browser;
121
86
  } finally {
122
- this.isInitializing = false;
123
- this.initPromise = null;
87
+ this.isLaunching = false;
88
+ this.launchPromise = null;
124
89
  }
125
90
  }
126
91
 
127
92
  /**
128
- * Launch puppeteer-cluster with CONCURRENCY_CONTEXT for isolation
93
+ * Launch browser via puppeteer-extra (with StealthPlugin already registered).
94
+ * This matches test-cf.ts which successfully bypasses Cloudflare.
129
95
  */
130
- private async launchCluster(): Promise<Cluster> {
131
- debug.log('preview', '🚀 Launching puppeteer-cluster...');
132
-
133
- const cluster = await Cluster.launch({
134
- // CONCURRENCY_CONTEXT: shared browser, isolated context per worker
135
- // Each context has its own cookies, localStorage, sessionStorage, cache
136
- concurrency: Cluster.CONCURRENCY_CONTEXT,
137
- maxConcurrency: this.config.maxConcurrency,
138
- timeout: this.config.timeout,
139
- retryLimit: this.config.retryLimit,
140
- retryDelay: this.config.retryDelay,
141
-
142
- puppeteerOptions: {
143
- headless: true,
144
- args: CHROMIUM_ARGS
145
- },
146
-
147
- // Monitor events
148
- monitor: false // Disable built-in monitoring, we use our own logging
96
+ private async launchBrowser(): Promise<Browser> {
97
+ debug.log('preview', '🚀 Launching browser with puppeteer-extra + StealthPlugin...');
98
+
99
+ const browser = await puppeteer.launch({
100
+ headless: true,
101
+ channel: 'chrome',
102
+ args: CHROMIUM_ARGS
103
+ }) as unknown as Browser;
104
+
105
+ debug.log('preview', '✅ Browser launched successfully');
106
+
107
+ // Handle browser disconnection
108
+ browser.on('disconnected', () => {
109
+ debug.warn('preview', '⚠️ Browser disconnected');
110
+ this.browser = null;
111
+ // Close all sessions since browser is gone
112
+ this.sessions.clear();
149
113
  });
150
114
 
151
- // Handle cluster errors
152
- cluster.on('taskerror', (err, data) => {
153
- debug.error('preview', `Task error for session ${data?.sessionId}:`, err.message);
154
- });
155
-
156
- debug.log('preview', '✅ puppeteer-cluster launched successfully');
157
- debug.log('preview', `📊 Max concurrency: ${this.config.maxConcurrency}`);
158
-
159
- return cluster;
160
- }
161
-
162
- /**
163
- * Get the shared browser instance from the cluster
164
- * Note: This accesses the internal browser - use with caution
165
- */
166
- async getBrowser(): Promise<Browser> {
167
- const cluster = await this.getCluster();
168
-
169
- // Access the browser through a dummy task
170
- // This is a workaround since cluster doesn't expose browser directly
171
- return new Promise((resolve, reject) => {
172
- cluster
173
- .execute(async ({ page }: { page: Page }) => {
174
- const browser = page.browser();
175
- resolve(browser);
176
- })
177
- .catch(reject);
178
- });
115
+ return browser;
179
116
  }
180
117
 
181
118
  /**
182
- * Create an isolated session with its own BrowserContext
183
- * Uses puppeteer-cluster's task execution for proper resource management
119
+ * Create an isolated session with its own BrowserContext.
120
+ * Each context has separate cookies, localStorage, sessionStorage, and cache.
184
121
  */
185
122
  async createSession(sessionId: string): Promise<PooledSession> {
186
- // Check if session already exists
187
123
  const existing = this.sessions.get(sessionId);
188
124
  if (existing) {
189
125
  debug.log('preview', `♻️ Reusing existing session: ${sessionId}`);
@@ -192,13 +128,10 @@ class BrowserPool {
192
128
 
193
129
  debug.log('preview', `🔒 Creating isolated session: ${sessionId}`);
194
130
 
195
- const cluster = await this.getCluster();
196
131
  const browser = await this.getBrowser();
197
132
 
198
- // Create isolated BrowserContext for this session
199
- // This provides FULL ISOLATION: cookies, localStorage, sessionStorage, cache
200
- // When sessionId is prefixed with projectId (e.g., "project123:tab-1"),
201
- // this ensures complete isolation between projects
133
+ // Create isolated context puppeteer-extra wraps this correctly
134
+ // so StealthPlugin's onPageCreated fires for every page in this context
202
135
  const context = await browser.createBrowserContext();
203
136
  const page = await context.newPage();
204
137
 
@@ -242,14 +175,12 @@ class BrowserPool {
242
175
  debug.log('preview', `🗑️ Destroying session: ${sessionId}`);
243
176
 
244
177
  try {
245
- // Close the page first
246
178
  if (session.page && !session.page.isClosed()) {
247
179
  await session.page.close().catch((err: Error) => {
248
180
  debug.warn('preview', `Error closing page: ${err.message}`);
249
181
  });
250
182
  }
251
183
 
252
- // Close the context (this clears all cookies, storage, cache)
253
184
  await session.context.close().catch((err: Error) => {
254
185
  debug.warn('preview', `Error closing context: ${err.message}`);
255
186
  });
@@ -261,35 +192,13 @@ class BrowserPool {
261
192
  debug.log('preview', `✅ Session destroyed (remaining: ${this.sessions.size})`);
262
193
  }
263
194
 
264
- /**
265
- * Execute a task in the cluster (for one-off operations)
266
- */
267
- async execute<T>(
268
- taskFunction: (opts: { page: Page; data: any }) => Promise<T>,
269
- data?: any
270
- ): Promise<T> {
271
- const cluster = await this.getCluster();
272
- return cluster.execute(data, taskFunction);
273
- }
274
-
275
- /**
276
- * Queue a task in the cluster
277
- */
278
- async queue(taskFunction: (opts: { page: Page; data: any }) => Promise<void>, data?: any): Promise<void> {
279
- const cluster = await this.getCluster();
280
- cluster.queue(data, taskFunction);
281
- }
282
-
283
195
  /**
284
196
  * Check if a session is valid
285
197
  */
286
198
  isSessionValid(sessionId: string): boolean {
287
199
  const session = this.sessions.get(sessionId);
288
200
  if (!session) return false;
289
-
290
- // Check if page is still open
291
201
  if (session.page.isClosed()) return false;
292
-
293
202
  return true;
294
203
  }
295
204
 
@@ -298,7 +207,7 @@ class BrowserPool {
298
207
  */
299
208
  getStats() {
300
209
  return {
301
- clusterActive: this.cluster !== null,
210
+ browserConnected: this.browser?.connected ?? false,
302
211
  activeSessions: this.sessions.size,
303
212
  maxConcurrency: this.config.maxConcurrency,
304
213
  sessions: Array.from(this.sessions.entries()).map(([id, session]) => ({
@@ -310,34 +219,22 @@ class BrowserPool {
310
219
  };
311
220
  }
312
221
 
313
- /**
314
- * Wait for all queued tasks to complete
315
- */
316
- async idle(): Promise<void> {
317
- if (this.cluster) {
318
- await this.cluster.idle();
319
- }
320
- }
321
-
322
222
  /**
323
223
  * Clean up all resources
324
224
  */
325
225
  async cleanup(): Promise<void> {
326
226
  debug.log('preview', '🧹 Cleaning up browser pool...');
327
227
 
328
- // Destroy all sessions
329
228
  const sessionIds = Array.from(this.sessions.keys());
330
229
  await Promise.all(sessionIds.map((id) => this.destroySession(id)));
331
230
 
332
- // Close the cluster
333
- if (this.cluster) {
231
+ if (this.browser) {
334
232
  try {
335
- await this.cluster.idle();
336
- await this.cluster.close();
233
+ await this.browser.close();
337
234
  } catch (error) {
338
- debug.warn('preview', `⚠️ Error closing cluster: ${error}`);
235
+ debug.warn('preview', `⚠️ Error closing browser: ${error}`);
339
236
  }
340
- this.cluster = null;
237
+ this.browser = null;
341
238
  }
342
239
 
343
240
  debug.log('preview', '✅ Browser pool cleaned up');
@@ -219,8 +219,9 @@ export class BrowserPreviewService extends EventEmitter {
219
219
  await this.navigationTracker.setupNavigationTracking(tab.id, tab.page, tab);
220
220
 
221
221
  // Setup dialog bindings and handling
222
- await this.dialogHandler.setupDialogBindings(tab.id, tab.page);
223
- await this.dialogHandler.setupDialogHandling(tab.id, tab.page, tab);
222
+ // Temporarily disable dialog injection to test CloudFlare evasion
223
+ // await this.dialogHandler.setupDialogBindings(tab.id, tab.page);
224
+ // await this.dialogHandler.setupDialogHandling(tab.id, tab.page, tab);
224
225
 
225
226
  return tab;
226
227
  }