chrome-ai-bridge 2.3.9 → 2.3.10

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,148 @@
1
+ /**
2
+ * DebugLogger - Chrome拡張機能デバッグ用ログ管理クラス
3
+ *
4
+ * カテゴリ:
5
+ * - ws: WebSocket接続関連
6
+ * - cdp: Chrome DevTools Protocol関連
7
+ * - tab: タブ操作関連
8
+ * - relay: リレーサーバー関連
9
+ * - error: エラー
10
+ */
11
+
12
+ class DebugLogger {
13
+ constructor() {
14
+ this.logs = [];
15
+ this.maxLogs = 500;
16
+ this.enabled = true;
17
+ }
18
+
19
+ /**
20
+ * ログエントリを追加
21
+ * @param {string} category - ログカテゴリ ('ws', 'cdp', 'tab', 'relay', 'error')
22
+ * @param {string} message - ログメッセージ
23
+ * @param {any} data - 追加データ(オプション)
24
+ */
25
+ log(category, message, data = null) {
26
+ if (!this.enabled) return;
27
+
28
+ const entry = {
29
+ ts: new Date().toISOString(),
30
+ category,
31
+ message,
32
+ data: data !== null ? this._safeStringify(data) : null
33
+ };
34
+
35
+ this.logs.push(entry);
36
+
37
+ // 最大件数を超えたら古いログを削除
38
+ if (this.logs.length > this.maxLogs) {
39
+ this.logs.shift();
40
+ }
41
+
42
+ // コンソールにも出力
43
+ const prefix = `[${category.toUpperCase()}]`;
44
+ if (data !== null) {
45
+ console.log(prefix, message, data);
46
+ } else {
47
+ console.log(prefix, message);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * エラーログを追加(ショートカット)
53
+ * @param {string} message - エラーメッセージ
54
+ * @param {any} error - エラーオブジェクト
55
+ */
56
+ error(message, error = null) {
57
+ const errorData = error instanceof Error
58
+ ? { name: error.name, message: error.message, stack: error.stack }
59
+ : error;
60
+ this.log('error', message, errorData);
61
+ }
62
+
63
+ /**
64
+ * ログを取得
65
+ * @param {string|null} filter - カテゴリでフィルタ(nullで全件)
66
+ * @param {number} limit - 取得件数(デフォルト: 100)
67
+ * @returns {Array} ログエントリの配列
68
+ */
69
+ getLogs(filter = null, limit = 100) {
70
+ let result = filter
71
+ ? this.logs.filter(l => l.category === filter)
72
+ : this.logs;
73
+
74
+ // 最新のログから返す
75
+ return result.slice(-limit);
76
+ }
77
+
78
+ /**
79
+ * ログをクリア
80
+ */
81
+ clear() {
82
+ this.logs = [];
83
+ console.log('[DEBUG] Logs cleared');
84
+ }
85
+
86
+ /**
87
+ * ログを有効/無効にする
88
+ * @param {boolean} enabled
89
+ */
90
+ setEnabled(enabled) {
91
+ this.enabled = enabled;
92
+ console.log('[DEBUG] Logger', enabled ? 'enabled' : 'disabled');
93
+ }
94
+
95
+ /**
96
+ * 安全なJSON変換(循環参照対策)
97
+ * @param {any} obj
98
+ * @returns {any}
99
+ */
100
+ _safeStringify(obj) {
101
+ if (obj === null || obj === undefined) return obj;
102
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
103
+ return obj;
104
+ }
105
+
106
+ try {
107
+ const seen = new WeakSet();
108
+ return JSON.parse(JSON.stringify(obj, (key, value) => {
109
+ if (typeof value === 'object' && value !== null) {
110
+ if (seen.has(value)) {
111
+ return '[Circular]';
112
+ }
113
+ seen.add(value);
114
+ }
115
+ // WebSocketなど大きなオブジェクトは省略
116
+ if (value instanceof WebSocket) {
117
+ return `[WebSocket: ${value.readyState}]`;
118
+ }
119
+ return value;
120
+ }));
121
+ } catch (e) {
122
+ return String(obj);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * 統計情報を取得
128
+ * @returns {Object}
129
+ */
130
+ getStats() {
131
+ const stats = {
132
+ total: this.logs.length,
133
+ byCategory: {}
134
+ };
135
+
136
+ for (const log of this.logs) {
137
+ stats.byCategory[log.category] = (stats.byCategory[log.category] || 0) + 1;
138
+ }
139
+
140
+ return stats;
141
+ }
142
+ }
143
+
144
+ // シングルトンインスタンスをエクスポート
145
+ export const debugLogger = new DebugLogger();
146
+
147
+ // デフォルトエクスポート
148
+ export default debugLogger;
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
+ <defs>
3
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#4285F4"/>
5
+ <stop offset="100%" style="stop-color:#7C3AED"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <!-- Background circle -->
9
+ <circle cx="64" cy="64" r="60" fill="url(#grad)"/>
10
+ <!-- Bridge shape - two nodes connected -->
11
+ <circle cx="36" cy="64" r="16" fill="white" opacity="0.95"/>
12
+ <circle cx="92" cy="64" r="16" fill="white" opacity="0.95"/>
13
+ <!-- Connection line -->
14
+ <rect x="36" y="58" width="56" height="12" fill="white" opacity="0.95" rx="6"/>
15
+ <!-- AI sparkle on left -->
16
+ <path d="M36 52 L38 56 L42 56 L39 59 L40 64 L36 61 L32 64 L33 59 L30 56 L34 56 Z" fill="#4285F4"/>
17
+ <!-- Chrome dot on right -->
18
+ <circle cx="92" cy="64" r="6" fill="#7C3AED"/>
19
+ </svg>
@@ -0,0 +1,28 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "chrome-ai-bridge Extension",
4
+ "version": "2.0.23",
5
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqDLiSB+b/gbnQ4zWRP65jnd27KmzpjJyR1JAQIjCD/dNORzTgk6+G0TjYEDZLIDceKHzGJudqnwq4q9g3T1eJ0SECZNXnaoE00WkgXfAUSQn6cmmXR3aQGFky/zbCmxnkRa0vYupxszlhw0yrlSZrIJd/weWF75Byh0zJfZ84kqDDhaj7TlB5laHICnoSLmPTif4mQcUW9oOKmAJPriPw4CWATKZsrQ4X46djxefSmfbqYfb9rttAqJVst40gO0Gsl6GOGxMHMds5Cl9GELc0dI3Gpobw07hQldZb8TeyilI/SnOaeS3HPtrp+KyEgRu8SgRdlrvuq6DeEZsP+kK7wIDAQAB",
6
+ "description": "Bridge between Chrome tabs and chrome-ai-bridge MCP server",
7
+ "permissions": ["debugger", "activeTab", "tabs", "storage", "alarms"],
8
+ "host_permissions": ["<all_urls>"],
9
+ "background": {
10
+ "service_worker": "background.mjs",
11
+ "type": "module"
12
+ },
13
+ "icons": {
14
+ "16": "icons/icon-16.png",
15
+ "32": "icons/icon-32.png",
16
+ "48": "icons/icon-48.png",
17
+ "128": "icons/icon-128.png"
18
+ },
19
+ "action": {
20
+ "default_title": "chrome-ai-bridge",
21
+ "default_icon": {
22
+ "16": "icons/icon-16.png",
23
+ "32": "icons/icon-32.png",
24
+ "48": "icons/icon-48.png"
25
+ }
26
+ },
27
+ "options_page": "ui/connect.html"
28
+ }
@@ -0,0 +1,505 @@
1
+ /**
2
+ * RelayServer - WebSocket server for Extension communication
3
+ */
4
+
5
+ import WebSocket, { WebSocketServer } from 'ws';
6
+ import { EventEmitter } from 'events';
7
+ import crypto from 'crypto';
8
+ import http from 'http';
9
+ import fs from 'fs';
10
+
11
+ // デバッグログをファイルに出力
12
+ const DEBUG_LOG_PATH = '/tmp/relay-server-debug.log';
13
+ function debugLog(...args: any[]) {
14
+ const timestamp = new Date().toISOString();
15
+ const message = `[${timestamp}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}\n`;
16
+ fs.appendFileSync(DEBUG_LOG_PATH, message);
17
+ }
18
+
19
+ export interface RelayServerOptions {
20
+ port?: number; // 0 for auto-assign
21
+ host?: string;
22
+ token?: string; // Authentication token
23
+ sessionId?: string;
24
+ }
25
+
26
+ export interface CDPCommand {
27
+ id: number;
28
+ method: string;
29
+ params?: any;
30
+ }
31
+
32
+ export interface CDPEvent {
33
+ method: string;
34
+ params?: any;
35
+ }
36
+
37
+ export class RelayServer extends EventEmitter {
38
+ private wss: WebSocketServer | null = null;
39
+ private ws: WebSocket | null = null; // Single connection (1 tab per server)
40
+ private port: number = 0;
41
+ private host: string;
42
+ private token: string;
43
+ private sessionId: string;
44
+ private instanceId: string;
45
+ private startedAt: number;
46
+ private tabId: number | null = null;
47
+ private ready: boolean = false;
48
+ private nextId = 1;
49
+ private pending = new Map<number, {
50
+ resolve: (value: any) => void;
51
+ reject: (err: Error) => void;
52
+ method: string;
53
+ startedAt: number;
54
+ }>();
55
+ private discoveryServer: http.Server | null = null;
56
+ private discoveryPort: number | null = null;
57
+ private keepAliveTimer: ReturnType<typeof setInterval> | null = null;
58
+
59
+ constructor(options: RelayServerOptions = {}) {
60
+ super();
61
+ this.host = options.host || '127.0.0.1';
62
+ this.token = options.token || this.generateToken();
63
+ this.sessionId = options.sessionId || this.generateSessionId();
64
+ this.instanceId = crypto.randomUUID();
65
+ this.startedAt = Date.now();
66
+ this.port = options.port || 0;
67
+ }
68
+
69
+ /**
70
+ * Start WebSocket server
71
+ */
72
+ async start(): Promise<number> {
73
+ return new Promise((resolve, reject) => {
74
+ this.wss = new WebSocketServer({
75
+ host: this.host,
76
+ port: this.port
77
+ });
78
+
79
+ this.wss.on('listening', () => {
80
+ const address = this.wss!.address() as WebSocket.AddressInfo;
81
+ this.port = address.port;
82
+ debugLog(`[RelayServer] Listening on ws://${this.host}:${this.port}`);
83
+ resolve(this.port);
84
+ });
85
+
86
+ this.wss.on('error', (error) => {
87
+ debugLog('[RelayServer] Server error:', error);
88
+ reject(error);
89
+ });
90
+
91
+ this.wss.on('connection', (ws, req) => {
92
+ this.handleConnection(ws, req);
93
+ });
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Handle WebSocket connection from Extension
99
+ */
100
+ private handleConnection(ws: WebSocket, req: any) {
101
+ debugLog('[RelayServer] New connection from Extension');
102
+
103
+ // Validate token
104
+ const url = new URL(req.url || '', `ws://${this.host}`);
105
+ const clientToken = url.searchParams.get('token');
106
+ const clientSessionId = url.searchParams.get('sid');
107
+
108
+ if (clientToken !== this.token) {
109
+ debugLog('[RelayServer] Invalid token');
110
+ ws.close(1008, 'Invalid token');
111
+ return;
112
+ }
113
+ if (clientSessionId && clientSessionId !== this.sessionId) {
114
+ debugLog('[RelayServer] Invalid session id', {expected: this.sessionId, received: clientSessionId});
115
+ ws.close(1008, 'Invalid session id');
116
+ return;
117
+ }
118
+
119
+ // Only allow one connection
120
+ if (this.ws) {
121
+ debugLog('[RelayServer] Connection already exists');
122
+ ws.close(1008, 'Connection already exists');
123
+ return;
124
+ }
125
+
126
+ this.ws = ws;
127
+ this.startKeepAlive();
128
+
129
+ ws.on('message', (data) => {
130
+ this.handleMessage(data.toString());
131
+ });
132
+
133
+ // Guard: only update state if this socket is still the current one.
134
+ // Prevents a stale socket's close event from corrupting a newer connection.
135
+ ws.on('close', () => {
136
+ if (this.ws !== ws) {
137
+ debugLog('[RelayServer] Stale socket closed (ignored — already replaced)');
138
+ return;
139
+ }
140
+ debugLog('[RelayServer] Extension disconnected');
141
+ this.stopKeepAlive();
142
+ this.rejectPendingRequests(
143
+ new Error('RELAY_DISCONNECTED: Extension socket closed before request completion'),
144
+ );
145
+ this.ws = null;
146
+ this.ready = false;
147
+ this.emit('disconnected');
148
+ });
149
+
150
+ ws.on('error', (error) => {
151
+ debugLog('[RelayServer] WebSocket error:', error);
152
+ });
153
+
154
+ debugLog('[RelayServer] Extension connected');
155
+ }
156
+
157
+ private rejectPendingRequests(error: Error): void {
158
+ if (this.pending.size === 0) return;
159
+ const pendingEntries = Array.from(this.pending.entries());
160
+ this.pending.clear();
161
+ for (const [id, pending] of pendingEntries) {
162
+ debugLog('[RelayServer] Rejecting pending request', {
163
+ id,
164
+ method: pending.method,
165
+ startedAt: pending.startedAt,
166
+ reason: error.message,
167
+ });
168
+ pending.reject(error);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Handle message from Extension
174
+ */
175
+ private handleMessage(data: string) {
176
+ try {
177
+ const message = JSON.parse(data);
178
+
179
+ if (typeof message.id === 'number' && (message.result !== undefined || message.error !== undefined)) {
180
+ const pending = this.pending.get(message.id);
181
+ if (pending) {
182
+ this.pending.delete(message.id);
183
+ if (message.error) {
184
+ const error =
185
+ typeof message.error === 'string'
186
+ ? new Error(message.error)
187
+ : new Error(message.error.message || 'Unknown error');
188
+ pending.reject(error);
189
+ } else {
190
+ pending.resolve(message.result);
191
+ }
192
+ return;
193
+ }
194
+
195
+ if (message.error) {
196
+ const error =
197
+ typeof message.error === 'string'
198
+ ? message.error
199
+ : message.error.message || 'Unknown error';
200
+ this.emit('cdp-error', { id: message.id, error });
201
+ } else {
202
+ this.emit('cdp-result', { id: message.id, result: message.result });
203
+ }
204
+ return;
205
+ }
206
+
207
+ if (message?.method === 'forwardCDPEvent' && message.params) {
208
+ this.emit('cdp-event', {
209
+ method: message.params.method,
210
+ params: message.params.params,
211
+ sessionId: message.params.sessionId,
212
+ });
213
+ return;
214
+ }
215
+
216
+ switch (message.type) {
217
+ case 'ready':
218
+ this.tabId = message.tabId;
219
+ this.ready = true;
220
+ debugLog(`[RelayServer] Connection ready for tab ${this.tabId}`);
221
+ this.emit('ready', this.tabId);
222
+ break;
223
+ case 'pong':
224
+ debugLog('[RelayServer] Received keep-alive pong');
225
+ break;
226
+
227
+ case 'forwardCDPResult':
228
+ this.emit('cdp-result', { id: message.id, result: message.result });
229
+ break;
230
+
231
+ case 'forwardCDPError':
232
+ this.emit('cdp-error', { id: message.id, error: message.error });
233
+ break;
234
+
235
+ case 'forwardCDPEvent':
236
+ this.emit('cdp-event', {
237
+ method: message.method,
238
+ params: message.params
239
+ });
240
+ break;
241
+
242
+ case 'detached':
243
+ debugLog(`[RelayServer] Tab ${message.tabId} detached: ${message.reason}`);
244
+ this.emit('detached', message.reason);
245
+ break;
246
+
247
+ default:
248
+ debugLog('[RelayServer] Unknown message type:', message.type);
249
+ }
250
+ } catch (error) {
251
+ debugLog('[RelayServer] Failed to parse message:', error);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Send CDP command to Extension
257
+ */
258
+ sendCDPCommand(id: number, method: string, params?: any): void {
259
+ if (!this.ws || !this.ready) {
260
+ throw new Error('Extension not connected or not ready');
261
+ }
262
+
263
+ this.ws.send(JSON.stringify({
264
+ type: 'forwardCDPCommand',
265
+ id,
266
+ method,
267
+ params
268
+ }));
269
+ }
270
+
271
+ sendMessage(message: any): void {
272
+ if (!this.ws || !this.ready) {
273
+ throw new Error(
274
+ `Extension not connected or not ready (connected=${Boolean(this.ws)}, ready=${this.ready})`,
275
+ );
276
+ }
277
+ if (this.ws.readyState !== WebSocket.OPEN) {
278
+ throw new Error('WebSocket not open');
279
+ }
280
+ this.ws.send(JSON.stringify(message));
281
+ }
282
+
283
+ async sendRequest(method: string, params?: any, timeoutMs = 30000): Promise<any> {
284
+ if (!this.ws || !this.ready) {
285
+ throw new Error(
286
+ `Extension not connected or not ready (method=${method}, connected=${Boolean(this.ws)}, ready=${this.ready})`,
287
+ );
288
+ }
289
+ if (this.ws.readyState !== WebSocket.OPEN) {
290
+ throw new Error('WebSocket not open');
291
+ }
292
+ const id = this.nextId++;
293
+ const payload = {id, method, params};
294
+ const startedAt = Date.now();
295
+ const response = new Promise<any>((resolve, reject) => {
296
+ const timeoutId = setTimeout(() => {
297
+ this.pending.delete(id);
298
+ reject(new Error(`RELAY_REQUEST_TIMEOUT: method=${method} timeoutMs=${timeoutMs}`));
299
+ }, timeoutMs);
300
+ timeoutId.unref();
301
+ this.pending.set(id, {
302
+ resolve: (value: any) => {
303
+ clearTimeout(timeoutId);
304
+ debugLog(`[RelayServer] Request success: ${method}`, {id, elapsedMs: Date.now() - startedAt});
305
+ resolve(value);
306
+ },
307
+ reject: (err: Error) => {
308
+ clearTimeout(timeoutId);
309
+ debugLog(`[RelayServer] Request failed: ${method}`, {id, elapsedMs: Date.now() - startedAt, error: err.message});
310
+ reject(err);
311
+ },
312
+ method,
313
+ startedAt,
314
+ });
315
+ });
316
+ try {
317
+ this.ws.send(JSON.stringify(payload));
318
+ debugLog(`[RelayServer] Request sent: ${method}`, {id});
319
+ } catch (error) {
320
+ this.pending.delete(id);
321
+ throw error;
322
+ }
323
+ return response;
324
+ }
325
+
326
+ /**
327
+ * Start simple discovery HTTP server for extension to find relay URL.
328
+ * Extension polls this endpoint when user clicks the extension icon.
329
+ */
330
+ async startDiscoveryServer(options: {
331
+ tabUrl?: string;
332
+ tabId?: number;
333
+ newTab?: boolean;
334
+ allowTabTakeover?: boolean;
335
+ } = {}): Promise<number | null> {
336
+ const ports = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
337
+ const wsUrl = this.getConnectionURL();
338
+
339
+ for (const port of ports) {
340
+ const started = await new Promise<boolean>((resolve) => {
341
+ const server = http.createServer(async (req, res) => {
342
+ res.setHeader('Access-Control-Allow-Origin', '*');
343
+
344
+ if (req.method === 'GET' && req.url === '/relay-info') {
345
+ res.setHeader('Content-Type', 'application/json');
346
+ res.end(JSON.stringify({
347
+ wsUrl,
348
+ tabUrl: options.tabUrl || null,
349
+ tabId: options.tabId ?? null,
350
+ newTab: Boolean(options.newTab),
351
+ allowTabTakeover: Boolean(options.allowTabTakeover),
352
+ sessionId: this.sessionId,
353
+ startedAt: this.startedAt,
354
+ instanceId: this.instanceId,
355
+ expiresAt: Date.now() + 60000,
356
+ }));
357
+ return;
358
+ }
359
+
360
+ if (req.method === 'POST' && req.url === '/reload-extension') {
361
+ res.setHeader('Content-Type', 'application/json');
362
+ if (!this.ws || !this.ready) {
363
+ res.statusCode = 503;
364
+ res.end(JSON.stringify({ error: 'Extension not connected' }));
365
+ return;
366
+ }
367
+ try {
368
+ await this.sendRequest('reloadExtension');
369
+ res.end(JSON.stringify({ success: true }));
370
+ } catch (err: any) {
371
+ // Extension reloads and drops connection - this is expected
372
+ res.end(JSON.stringify({ success: true, note: 'Extension reloading' }));
373
+ }
374
+ return;
375
+ }
376
+
377
+ res.statusCode = 404;
378
+ res.end('Not Found');
379
+ });
380
+
381
+ server.on('error', (error: any) => {
382
+ if (error?.code === 'EADDRINUSE') {
383
+ resolve(false);
384
+ return;
385
+ }
386
+ debugLog('[RelayServer] Discovery server error:', error);
387
+ resolve(false);
388
+ });
389
+
390
+ server.listen(port, this.host, () => {
391
+ this.discoveryServer = server;
392
+ this.discoveryPort = port;
393
+ debugLog(`[RelayServer] Discovery available on http://${this.host}:${port}/relay-info`);
394
+ resolve(true);
395
+ });
396
+ });
397
+
398
+ if (started) {
399
+ return port;
400
+ }
401
+ }
402
+
403
+ debugLog('[RelayServer] Could not start discovery server on any port');
404
+ return null;
405
+ }
406
+
407
+ /**
408
+ * Stop server
409
+ */
410
+ async stop(): Promise<void> {
411
+ this.stopKeepAlive();
412
+
413
+ if (this.ws) {
414
+ try {
415
+ this.ws.close();
416
+ } catch {
417
+ // ignore close errors
418
+ }
419
+ this.ws = null;
420
+ }
421
+ this.ready = false;
422
+ this.tabId = null;
423
+
424
+ this.rejectPendingRequests(
425
+ new Error('RELAY_STOPPED: Relay stopped before request completion'),
426
+ );
427
+
428
+ if (this.discoveryServer) {
429
+ this.discoveryServer.close();
430
+ this.discoveryServer = null;
431
+ this.discoveryPort = null;
432
+ }
433
+
434
+ if (this.wss) {
435
+ return new Promise((resolve) => {
436
+ this.wss!.close(() => {
437
+ this.wss = null;
438
+ debugLog('[RelayServer] Server stopped');
439
+ resolve();
440
+ });
441
+ });
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Start keep-alive ping to prevent Service Worker from sleeping
447
+ */
448
+ private startKeepAlive(): void {
449
+ this.stopKeepAlive();
450
+ this.keepAliveTimer = setInterval(() => {
451
+ if (this.ws?.readyState === WebSocket.OPEN) {
452
+ this.ws.send(JSON.stringify({ type: 'ping' }));
453
+ debugLog('[RelayServer] Sent keep-alive ping');
454
+ }
455
+ }, 30000); // 30 seconds
456
+ debugLog('[RelayServer] Keep-alive started');
457
+ }
458
+
459
+ /**
460
+ * Stop keep-alive ping
461
+ */
462
+ private stopKeepAlive(): void {
463
+ if (this.keepAliveTimer) {
464
+ clearInterval(this.keepAliveTimer);
465
+ this.keepAliveTimer = null;
466
+ debugLog('[RelayServer] Keep-alive stopped');
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Generate random token
472
+ */
473
+ private generateToken(): string {
474
+ return crypto.randomBytes(32).toString('hex');
475
+ }
476
+
477
+ getPort(): number {
478
+ return this.port;
479
+ }
480
+
481
+ getToken(): string {
482
+ return this.token;
483
+ }
484
+
485
+ getTabId(): number | null {
486
+ return this.tabId;
487
+ }
488
+
489
+ isReady(): boolean {
490
+ return this.ready;
491
+ }
492
+
493
+
494
+ getConnectionURL(): string {
495
+ return `ws://${this.host}:${this.port}?token=${this.token}&sid=${encodeURIComponent(this.sessionId)}`;
496
+ }
497
+
498
+ getSessionId(): string {
499
+ return this.sessionId;
500
+ }
501
+
502
+ private generateSessionId(): string {
503
+ return crypto.randomBytes(16).toString('hex');
504
+ }
505
+ }