@myrialabs/clopen 0.1.9 → 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 (94) hide show
  1. package/README.md +23 -1
  2. package/backend/index.ts +25 -1
  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/chat/stream-manager.ts +4 -1
  9. package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
  10. package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
  11. package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
  12. package/backend/lib/database/migrations/index.ts +21 -0
  13. package/backend/lib/database/queries/auth-queries.ts +201 -0
  14. package/backend/lib/database/queries/index.ts +2 -1
  15. package/backend/lib/database/queries/session-queries.ts +13 -0
  16. package/backend/lib/database/queries/snapshot-queries.ts +1 -1
  17. package/backend/lib/engine/adapters/opencode/server.ts +9 -1
  18. package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
  19. package/backend/lib/mcp/config.ts +13 -18
  20. package/backend/lib/mcp/index.ts +9 -0
  21. package/backend/lib/mcp/remote-server.ts +132 -0
  22. package/backend/lib/mcp/servers/helper.ts +49 -3
  23. package/backend/lib/mcp/servers/index.ts +3 -2
  24. package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
  25. package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
  26. package/backend/lib/preview/browser/browser-pool.ts +73 -176
  27. package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
  28. package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
  29. package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
  30. package/backend/lib/snapshot/helpers.ts +22 -49
  31. package/backend/lib/snapshot/snapshot-service.ts +148 -83
  32. package/backend/lib/utils/ws.ts +65 -1
  33. package/backend/ws/auth/index.ts +17 -0
  34. package/backend/ws/auth/invites.ts +84 -0
  35. package/backend/ws/auth/login.ts +269 -0
  36. package/backend/ws/auth/status.ts +41 -0
  37. package/backend/ws/auth/users.ts +32 -0
  38. package/backend/ws/chat/stream.ts +13 -0
  39. package/backend/ws/engine/claude/accounts.ts +3 -1
  40. package/backend/ws/engine/utils.ts +38 -6
  41. package/backend/ws/index.ts +4 -4
  42. package/backend/ws/preview/browser/interact.ts +27 -5
  43. package/backend/ws/snapshot/restore.ts +111 -12
  44. package/backend/ws/snapshot/timeline.ts +56 -29
  45. package/bin/clopen.ts +56 -1
  46. package/bun.lock +113 -51
  47. package/frontend/App.svelte +47 -29
  48. package/frontend/lib/components/auth/InvitePage.svelte +215 -0
  49. package/frontend/lib/components/auth/LoginPage.svelte +129 -0
  50. package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
  51. package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
  52. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
  53. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
  54. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
  55. package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
  56. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
  57. package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
  58. package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
  59. package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
  60. package/frontend/lib/components/git/CommitForm.svelte +6 -4
  61. package/frontend/lib/components/history/HistoryModal.svelte +1 -1
  62. package/frontend/lib/components/history/HistoryView.svelte +1 -1
  63. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
  64. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
  65. package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
  66. package/frontend/lib/components/settings/SettingsView.svelte +21 -7
  67. package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
  68. package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
  69. package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
  70. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
  71. package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
  72. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  73. package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
  74. package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
  75. package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
  76. package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
  77. package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
  78. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
  79. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  80. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
  81. package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
  82. package/frontend/lib/stores/features/auth.svelte.ts +296 -0
  83. package/frontend/lib/stores/features/settings.svelte.ts +53 -9
  84. package/frontend/lib/stores/features/user.svelte.ts +26 -68
  85. package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
  86. package/frontend/lib/stores/ui/update.svelte.ts +2 -14
  87. package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
  88. package/package.json +8 -6
  89. package/shared/types/stores/settings.ts +16 -2
  90. package/shared/utils/logger.ts +1 -0
  91. package/shared/utils/ws-client.ts +30 -13
  92. package/shared/utils/ws-server.ts +42 -4
  93. package/backend/lib/mcp/stdio-server.ts +0 -103
  94. package/backend/ws/mcp/index.ts +0 -61
@@ -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
  }