opendevbrowser 0.0.11 → 0.0.15

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -28
  3. package/dist/chunk-JVBMT2O5.js +7173 -0
  4. package/dist/chunk-JVBMT2O5.js.map +1 -0
  5. package/dist/cli/index.js +3690 -275
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1080 -2857
  8. package/dist/index.js.map +1 -1
  9. package/dist/opendevbrowser.js +1080 -2857
  10. package/dist/opendevbrowser.js.map +1 -1
  11. package/extension/dist/annotate-content.css +237 -0
  12. package/extension/dist/annotate-content.js +934 -0
  13. package/extension/dist/background.js +1291 -8
  14. package/extension/dist/logging.js +50 -0
  15. package/extension/dist/ops/dom-bridge.js +355 -0
  16. package/extension/dist/ops/ops-runtime.js +1249 -0
  17. package/extension/dist/ops/ops-session-store.js +189 -0
  18. package/extension/dist/ops/redaction.js +52 -0
  19. package/extension/dist/ops/snapshot-builder.js +4 -0
  20. package/extension/dist/ops/snapshot-shared.js +220 -0
  21. package/extension/dist/popup.js +398 -21
  22. package/extension/dist/relay-settings.js +3 -1
  23. package/extension/dist/services/CDPRouter.js +501 -103
  24. package/extension/dist/services/ConnectionManager.js +464 -57
  25. package/extension/dist/services/NativePortManager.js +182 -0
  26. package/extension/dist/services/RelayClient.js +227 -26
  27. package/extension/dist/services/TabManager.js +81 -0
  28. package/extension/dist/services/TargetSessionMap.js +146 -0
  29. package/extension/dist/services/cdp-router-commands.js +203 -0
  30. package/extension/dist/services/url-restrictions.js +41 -0
  31. package/extension/dist/types.js +3 -1
  32. package/extension/icons/icon128.png +0 -0
  33. package/extension/icons/icon16.png +0 -0
  34. package/extension/icons/icon32.png +0 -0
  35. package/extension/icons/icon48.png +0 -0
  36. package/extension/manifest.json +17 -3
  37. package/extension/popup.html +469 -65
  38. package/package.json +2 -2
  39. package/skills/AGENTS.md +34 -61
  40. package/skills/data-extraction/SKILL.md +95 -103
  41. package/skills/form-testing/SKILL.md +75 -82
  42. package/skills/login-automation/SKILL.md +76 -66
  43. package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
  44. package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
  45. package/dist/chunk-R5VUZEUU.js +0 -128
  46. package/dist/chunk-R5VUZEUU.js.map +0 -1
  47. package/extension/dist/popup.jsx +0 -150
@@ -1,79 +1,510 @@
1
+ import { TabManager } from "./TabManager.js";
2
+ import { TargetSessionMap } from "./TargetSessionMap.js";
3
+ import { logError } from "../logging.js";
4
+ import { handleSetDiscoverTargets, handleSetAutoAttach, handleCreateTarget, handleCloseTarget, handleActivateTarget, handleAttachToTarget, handleRoutedCommand } from "./cdp-router-commands.js";
5
+ const FLAT_SESSION_ERROR = "Chrome 125+ required for extension relay (flat sessions).";
6
+ const DEPRECATED_SEND_MESSAGE = "Target.sendMessageToTarget is deprecated in flat session mode. Use sessionId routing.";
7
+ const DEFAULT_BROWSER_CONTEXT_ID = "default";
1
8
  export class CDPRouter {
2
- debuggee = null;
9
+ debuggees = new Map();
10
+ sessions = new TargetSessionMap();
11
+ tabManager = new TabManager();
12
+ rootAttachedSessions = new Set();
3
13
  callbacks = null;
14
+ autoAttachOptions = { autoAttach: false, waitForDebuggerOnStart: false, flatten: true };
15
+ discoverTargets = false;
16
+ listenersActive = false;
17
+ flatSessionValidated = false;
18
+ primaryTabId = null;
19
+ lastActiveTabId = null;
20
+ sessionCounter = 1;
21
+ quarantinedSessions = new Map();
22
+ churnTracker = new Map();
23
+ churnWindowMs = 5000;
24
+ churnThreshold = 3;
4
25
  handleEventBound = (source, method, params) => {
5
26
  this.handleEvent(source, method, params);
6
27
  };
7
- handleDetachBound = (source) => {
8
- this.handleDetach(source);
28
+ handleDetachBound = (source, reason) => {
29
+ this.handleDetach(source, reason);
9
30
  };
10
31
  setCallbacks(callbacks) {
11
32
  this.callbacks = callbacks;
12
33
  }
13
34
  async attach(tabId) {
14
- if (this.debuggee?.tabId === tabId) {
35
+ await this.attachInternal(tabId, true);
36
+ }
37
+ async attachInternal(tabId, allowRetry) {
38
+ if (this.debuggees.has(tabId)) {
39
+ this.updatePrimaryTab(tabId);
15
40
  return;
16
41
  }
17
- if (this.debuggee) {
18
- await this.detach();
19
- }
20
- this.debuggee = { tabId };
42
+ const debuggee = { tabId };
43
+ this.debuggees.set(tabId, debuggee);
44
+ this.ensureListeners();
21
45
  try {
22
46
  await this.runDebuggerAction((done) => {
23
- chrome.debugger.attach(this.debuggee, "1.3", done);
47
+ chrome.debugger.attach(debuggee, "1.3", done);
24
48
  });
25
- chrome.debugger.onEvent.addListener(this.handleEventBound);
26
- chrome.debugger.onDetach.addListener(this.handleDetachBound);
49
+ await this.ensureFlatSessionSupport(debuggee);
50
+ const targetInfo = await this.registerRootTab(tabId);
51
+ if (this.discoverTargets) {
52
+ this.emitTargetCreated(targetInfo);
53
+ }
54
+ if (this.autoAttachOptions.autoAttach) {
55
+ await this.applyAutoAttach(debuggee);
56
+ this.emitRootAttached(targetInfo);
57
+ }
58
+ this.updatePrimaryTab(tabId);
27
59
  }
28
60
  catch (error) {
29
- this.debuggee = null;
61
+ this.debuggees.delete(tabId);
62
+ if (this.debuggees.size === 0) {
63
+ this.removeListeners();
64
+ }
65
+ await this.safeDetach(debuggee);
66
+ if (allowRetry && this.isStaleTabError(error)) {
67
+ const activeTabId = await this.tabManager.getActiveTabId();
68
+ if (activeTabId && activeTabId !== tabId) {
69
+ return await this.attachInternal(activeTabId, false);
70
+ }
71
+ const fallbackTabId = await this.tabManager.getFirstHttpTabId();
72
+ if (fallbackTabId && fallbackTabId !== tabId) {
73
+ return await this.attachInternal(fallbackTabId, false);
74
+ }
75
+ }
30
76
  throw error;
31
77
  }
32
78
  }
33
- async detach() {
34
- if (!this.debuggee)
79
+ async detachAll() {
80
+ const entries = Array.from(this.debuggees.entries());
81
+ this.debuggees.clear();
82
+ this.removeListeners();
83
+ for (const [tabId, debuggee] of entries) {
84
+ this.detachTabState(tabId);
85
+ await this.safeDetach(debuggee);
86
+ }
87
+ this.primaryTabId = null;
88
+ this.lastActiveTabId = null;
89
+ this.callbacks?.onDetach({ reason: "manual_disconnect" });
90
+ }
91
+ async detachTab(tabId) {
92
+ const debuggee = this.debuggees.get(tabId);
93
+ if (!debuggee) {
35
94
  return;
36
- const current = this.debuggee;
37
- this.debuggee = null;
38
- chrome.debugger.onEvent.removeListener(this.handleEventBound);
39
- chrome.debugger.onDetach.removeListener(this.handleDetachBound);
40
- await this.runDebuggerAction((done) => {
41
- chrome.debugger.detach(current, done);
42
- });
95
+ }
96
+ this.debuggees.delete(tabId);
97
+ this.detachTabState(tabId);
98
+ await this.safeDetach(debuggee);
99
+ if (this.debuggees.size === 0) {
100
+ this.removeListeners();
101
+ this.primaryTabId = null;
102
+ this.lastActiveTabId = null;
103
+ }
104
+ else if (this.primaryTabId === tabId) {
105
+ this.updatePrimaryTab(this.selectFallbackPrimary());
106
+ }
107
+ this.callbacks?.onDetach({ tabId, reason: "manual_disconnect" });
108
+ }
109
+ getPrimaryTabId() {
110
+ return this.primaryTabId;
43
111
  }
44
- getAttachedTabId() {
45
- return this.debuggee?.tabId ?? null;
112
+ getAttachedTabIds() {
113
+ return Array.from(this.debuggees.keys());
46
114
  }
47
115
  async handleCommand(command) {
48
- if (!this.debuggee || !this.callbacks) {
49
- this.callbacks?.onResponse({ id: command.id, error: { message: "No tab attached" } });
116
+ if (!this.callbacks)
117
+ return;
118
+ if (this.debuggees.size === 0) {
119
+ this.respondError(command.id, "No tab attached");
50
120
  return;
51
121
  }
52
122
  const { method, params, sessionId } = command.params;
53
- if (sessionId && method !== "Target.sendMessageToTarget") {
54
- const message = JSON.stringify({ id: command.id, method, params });
55
- try {
56
- await this.sendCommand("Target.sendMessageToTarget", { sessionId, message });
123
+ const commandParams = isRecord(params) ? params : {};
124
+ const ctx = this.buildCommandContext();
125
+ switch (method) {
126
+ case "Browser.getVersion": {
127
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "OpenDevBrowser Relay";
128
+ this.respond(command.id, {
129
+ protocolVersion: "1.3",
130
+ product: "Chrome",
131
+ revision: "",
132
+ userAgent,
133
+ jsVersion: ""
134
+ });
135
+ return;
57
136
  }
58
- catch (error) {
59
- this.callbacks.onResponse({ id: command.id, error: { message: getErrorMessage(error) } });
137
+ case "Browser.setDownloadBehavior":
138
+ this.respond(command.id, {});
139
+ return;
140
+ case "Target.getBrowserContexts":
141
+ this.respond(command.id, { browserContextIds: [DEFAULT_BROWSER_CONTEXT_ID] });
142
+ return;
143
+ case "Target.attachToBrowserTarget": {
144
+ const rootSession = await this.ensureRootSessionForPrimary();
145
+ if (!rootSession) {
146
+ this.respondError(command.id, "No tab attached");
147
+ return;
148
+ }
149
+ this.respond(command.id, { sessionId: rootSession.sessionId });
150
+ return;
151
+ }
152
+ case "Target.sendMessageToTarget":
153
+ this.respondError(command.id, DEPRECATED_SEND_MESSAGE);
154
+ return;
155
+ case "Target.setDiscoverTargets":
156
+ await handleSetDiscoverTargets(ctx, command.id, commandParams);
157
+ return;
158
+ case "Target.getTargets":
159
+ this.respond(command.id, { targetInfos: this.sessions.listTargetInfos() });
160
+ return;
161
+ case "Target.getTargetInfo": {
162
+ const targetId = typeof commandParams.targetId === "string" ? commandParams.targetId : "";
163
+ const record = targetId ? this.sessions.getByTargetId(targetId) : null;
164
+ const targetInfo = record?.targetInfo
165
+ ?? (record?.kind === "root" ? this.sessions.getByTabId(record.tabId)?.targetInfo ?? null : null);
166
+ this.respond(command.id, { targetInfo });
167
+ return;
60
168
  }
169
+ case "Target.setAutoAttach":
170
+ await handleSetAutoAttach(ctx, command.id, commandParams, sessionId);
171
+ return;
172
+ case "Target.createTarget":
173
+ await handleCreateTarget(ctx, command.id, commandParams);
174
+ return;
175
+ case "Target.closeTarget":
176
+ await handleCloseTarget(ctx, command.id, commandParams);
177
+ return;
178
+ case "Target.activateTarget":
179
+ await handleActivateTarget(ctx, command.id, commandParams);
180
+ return;
181
+ case "Target.attachToTarget":
182
+ await handleAttachToTarget(ctx, command.id, commandParams, sessionId);
183
+ return;
184
+ default:
185
+ await handleRoutedCommand(ctx, command.id, method, commandParams, sessionId);
186
+ }
187
+ }
188
+ buildCommandContext() {
189
+ return {
190
+ debuggees: this.debuggees,
191
+ sessions: this.sessions,
192
+ tabManager: this.tabManager,
193
+ autoAttachOptions: this.autoAttachOptions,
194
+ discoverTargets: this.discoverTargets,
195
+ flatSessionError: FLAT_SESSION_ERROR,
196
+ setAutoAttachOptions: (next) => {
197
+ this.autoAttachOptions = next;
198
+ },
199
+ setDiscoverTargets: (value) => {
200
+ this.discoverTargets = value;
201
+ },
202
+ respond: this.respond.bind(this),
203
+ respondError: this.respondError.bind(this),
204
+ emitTargetCreated: this.emitTargetCreated.bind(this),
205
+ emitRootAttached: this.emitRootAttached.bind(this),
206
+ emitRootDetached: this.emitRootDetached.bind(this),
207
+ resetRootAttached: this.resetRootAttached.bind(this),
208
+ updatePrimaryTab: this.updatePrimaryTab.bind(this),
209
+ detachTabState: this.detachTabState.bind(this),
210
+ safeDetach: this.safeDetach.bind(this),
211
+ attach: this.attach.bind(this),
212
+ registerRootTab: this.registerRootTab.bind(this),
213
+ applyAutoAttach: this.applyAutoAttach.bind(this),
214
+ sendCommand: this.sendCommand.bind(this),
215
+ getPrimaryDebuggee: this.getPrimaryDebuggee.bind(this)
216
+ };
217
+ }
218
+ async registerRootTab(tabId) {
219
+ const existing = this.sessions.getByTabId(tabId);
220
+ const sessionId = existing?.rootSessionId ?? this.createRootSessionId();
221
+ const targetInfo = await this.buildTargetInfo(tabId);
222
+ this.sessions.registerRootTab(tabId, targetInfo, sessionId);
223
+ return targetInfo;
224
+ }
225
+ updatePrimaryTab(tabId) {
226
+ if (tabId === this.primaryTabId)
227
+ return;
228
+ this.primaryTabId = tabId;
229
+ if (tabId !== null) {
230
+ this.lastActiveTabId = tabId;
231
+ }
232
+ this.callbacks?.onPrimaryTabChange?.(tabId);
233
+ }
234
+ selectFallbackPrimary() {
235
+ if (this.lastActiveTabId && this.debuggees.has(this.lastActiveTabId)) {
236
+ return this.lastActiveTabId;
237
+ }
238
+ const [first] = this.debuggees.keys();
239
+ return first ?? null;
240
+ }
241
+ getPrimaryDebuggee() {
242
+ if (this.primaryTabId !== null && this.debuggees.has(this.primaryTabId)) {
243
+ return { tabId: this.primaryTabId };
244
+ }
245
+ const [first] = this.debuggees.keys();
246
+ return typeof first === "number" ? { tabId: first } : null;
247
+ }
248
+ async ensureRootSessionForPrimary() {
249
+ const debuggee = this.getPrimaryDebuggee();
250
+ if (!debuggee || typeof debuggee.tabId !== "number") {
251
+ return null;
252
+ }
253
+ const existing = this.sessions.getByTabId(debuggee.tabId);
254
+ if (existing) {
255
+ return { sessionId: existing.rootSessionId, targetInfo: existing.targetInfo };
256
+ }
257
+ const targetInfo = await this.registerRootTab(debuggee.tabId);
258
+ const refreshed = this.sessions.getByTabId(debuggee.tabId);
259
+ if (!refreshed) {
260
+ return null;
261
+ }
262
+ return { sessionId: refreshed.rootSessionId, targetInfo: targetInfo ?? refreshed.targetInfo };
263
+ }
264
+ ensureListeners() {
265
+ if (this.listenersActive)
266
+ return;
267
+ chrome.debugger.onEvent.addListener(this.handleEventBound);
268
+ chrome.debugger.onDetach.addListener(this.handleDetachBound);
269
+ this.listenersActive = true;
270
+ }
271
+ removeListeners() {
272
+ if (!this.listenersActive)
273
+ return;
274
+ chrome.debugger.onEvent.removeListener(this.handleEventBound);
275
+ chrome.debugger.onDetach.removeListener(this.handleDetachBound);
276
+ this.listenersActive = false;
277
+ }
278
+ async ensureFlatSessionSupport(debuggee) {
279
+ if (this.flatSessionValidated)
61
280
  return;
281
+ try {
282
+ await this.sendCommand(debuggee, "Target.setAutoAttach", {
283
+ autoAttach: false,
284
+ waitForDebuggerOnStart: false,
285
+ flatten: true
286
+ });
287
+ this.flatSessionValidated = true;
288
+ }
289
+ catch (error) {
290
+ const detail = getErrorMessage(error);
291
+ console.warn(`[opendevbrowser] Target.setAutoAttach(flatten) failed: ${detail}`);
292
+ throw new Error(`${FLAT_SESSION_ERROR} (${detail})`);
293
+ }
294
+ }
295
+ async applyAutoAttach(debuggee) {
296
+ const params = {
297
+ autoAttach: this.autoAttachOptions.autoAttach,
298
+ waitForDebuggerOnStart: this.autoAttachOptions.waitForDebuggerOnStart,
299
+ flatten: true
300
+ };
301
+ if (typeof this.autoAttachOptions.filter !== "undefined") {
302
+ params.filter = this.autoAttachOptions.filter;
62
303
  }
63
304
  try {
64
- const result = await this.sendCommand(method, params ?? {});
65
- this.callbacks.onResponse({ id: command.id, result });
305
+ await this.sendCommand(debuggee, "Target.setAutoAttach", params);
66
306
  }
67
307
  catch (error) {
68
- this.callbacks.onResponse({ id: command.id, error: { message: getErrorMessage(error) } });
308
+ const detail = getErrorMessage(error);
309
+ console.warn(`[opendevbrowser] Target.setAutoAttach failed: ${detail}`);
310
+ throw new Error(`${FLAT_SESSION_ERROR} (${detail})`);
311
+ }
312
+ }
313
+ async applyAutoAttachToChild(tabId, sessionId) {
314
+ if (!this.autoAttachOptions.autoAttach)
315
+ return;
316
+ const params = {
317
+ autoAttach: true,
318
+ waitForDebuggerOnStart: this.autoAttachOptions.waitForDebuggerOnStart,
319
+ flatten: true
320
+ };
321
+ if (typeof this.autoAttachOptions.filter !== "undefined") {
322
+ params.filter = this.autoAttachOptions.filter;
69
323
  }
324
+ await this.sendCommand({ tabId, sessionId }, "Target.setAutoAttach", params);
70
325
  }
71
- async sendCommand(method, params) {
72
- if (!this.debuggee) {
73
- throw new Error("No tab attached");
326
+ recordSessionChurn(tabId, sessionId, reason) {
327
+ const now = Date.now();
328
+ const existing = this.churnTracker.get(tabId);
329
+ const record = !existing || now > existing.resetAt
330
+ ? { count: 0, resetAt: now + this.churnWindowMs }
331
+ : existing;
332
+ record.count += 1;
333
+ this.churnTracker.set(tabId, record);
334
+ const quarantined = this.quarantinedSessions.get(sessionId);
335
+ if (!quarantined) {
336
+ this.quarantinedSessions.set(sessionId, { tabId, count: 1, lastSeen: now });
337
+ }
338
+ if (record.count >= this.churnThreshold) {
339
+ this.churnTracker.delete(tabId);
340
+ this.reapplyAutoAttach(tabId, reason).catch((error) => {
341
+ logError("cdp.reapply_auto_attach", error, { code: "auto_attach_failed" });
342
+ });
74
343
  }
344
+ }
345
+ quarantineUnknownSession(tabId, sessionId, method) {
346
+ const now = Date.now();
347
+ const existing = this.quarantinedSessions.get(sessionId);
348
+ if (existing) {
349
+ existing.count += 1;
350
+ existing.lastSeen = now;
351
+ return;
352
+ }
353
+ this.quarantinedSessions.set(sessionId, { tabId, count: 1, lastSeen: now });
354
+ this.recordSessionChurn(tabId, sessionId, `unknown_${method}`);
355
+ }
356
+ async reapplyAutoAttach(tabId, reason) {
357
+ if (!this.autoAttachOptions.autoAttach)
358
+ return;
359
+ const debuggee = this.debuggees.get(tabId);
360
+ if (!debuggee)
361
+ return;
362
+ try {
363
+ await this.applyAutoAttach(debuggee);
364
+ }
365
+ catch (error) {
366
+ const detail = getErrorMessage(error);
367
+ console.warn(`[opendevbrowser] Auto-attach retry failed (${reason}): ${detail}`);
368
+ }
369
+ }
370
+ handleEvent(source, method, params) {
371
+ if (!this.callbacks)
372
+ return;
373
+ const tabId = typeof source.tabId === "number" ? source.tabId : null;
374
+ if (tabId === null || !this.debuggees.has(tabId))
375
+ return;
376
+ if (method === "Target.receivedMessageFromTarget")
377
+ return;
378
+ if (method === "Target.attachedToTarget" && params && isRecord(params)) {
379
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : null;
380
+ const targetInfo = isTargetInfo(params.targetInfo) ? params.targetInfo : null;
381
+ if (sessionId && targetInfo) {
382
+ this.sessions.registerChildSession(tabId, targetInfo, sessionId);
383
+ this.quarantinedSessions.delete(sessionId);
384
+ this.applyAutoAttachToChild(tabId, sessionId).catch((error) => {
385
+ logError("cdp.apply_auto_attach_child", error, { code: "auto_attach_failed" });
386
+ });
387
+ }
388
+ else if (sessionId) {
389
+ this.recordSessionChurn(tabId, sessionId, "attach_missing_target");
390
+ }
391
+ }
392
+ if (method === "Target.detachedFromTarget" && params && isRecord(params)) {
393
+ const detachedSessionId = typeof params.sessionId === "string" ? params.sessionId : null;
394
+ if (detachedSessionId) {
395
+ const removed = this.sessions.removeBySessionId(detachedSessionId);
396
+ if (!removed) {
397
+ this.recordSessionChurn(tabId, detachedSessionId, "detach_unknown");
398
+ this.quarantineUnknownSession(tabId, detachedSessionId, method);
399
+ return;
400
+ }
401
+ }
402
+ }
403
+ const sourceSessionId = source.sessionId;
404
+ if (typeof sourceSessionId === "string" && !this.sessions.hasSession(sourceSessionId)) {
405
+ this.quarantineUnknownSession(tabId, sourceSessionId, method);
406
+ return;
407
+ }
408
+ const forwardSessionId = this.resolveForwardSessionId(method, source);
409
+ this.emitEvent(method, params, forwardSessionId);
410
+ }
411
+ handleDetach(source, reason) {
412
+ const tabId = typeof source.tabId === "number" ? source.tabId : null;
413
+ if (tabId === null || !this.debuggees.has(tabId))
414
+ return;
415
+ this.debuggees.delete(tabId);
416
+ this.detachTabState(tabId);
417
+ if (this.debuggees.size === 0) {
418
+ this.removeListeners();
419
+ this.callbacks?.onDetach({ tabId, reason });
420
+ }
421
+ }
422
+ detachTabState(tabId) {
423
+ const record = this.sessions.removeByTabId(tabId);
424
+ if (record) {
425
+ this.rootAttachedSessions.delete(record.rootSessionId);
426
+ if (this.autoAttachOptions.autoAttach) {
427
+ this.emitTargetDetached(record.rootSessionId, record.targetInfo.targetId);
428
+ }
429
+ if (this.discoverTargets) {
430
+ this.emitTargetDestroyed(record.targetInfo.targetId);
431
+ }
432
+ }
433
+ if (tabId === this.primaryTabId) {
434
+ const next = this.selectFallbackPrimary();
435
+ this.updatePrimaryTab(next);
436
+ }
437
+ }
438
+ resolveForwardSessionId(method, source) {
439
+ if (method === "Target.attachedToTarget" || method === "Target.detachedFromTarget") {
440
+ return undefined;
441
+ }
442
+ const sessionId = source.sessionId;
443
+ if (typeof sessionId === "string") {
444
+ return this.sessions.getBySessionId(sessionId) ? sessionId : undefined;
445
+ }
446
+ const tabId = typeof source.tabId === "number" ? source.tabId : null;
447
+ if (tabId === null)
448
+ return undefined;
449
+ const record = this.sessions.getByTabId(tabId);
450
+ if (!record)
451
+ return undefined;
452
+ return this.rootAttachedSessions.has(record.rootSessionId) ? record.rootSessionId : undefined;
453
+ }
454
+ async buildTargetInfo(tabId) {
455
+ const tab = await this.tabManager.getTab(tabId);
456
+ return {
457
+ targetId: `tab-${tabId}`,
458
+ type: "page",
459
+ browserContextId: DEFAULT_BROWSER_CONTEXT_ID,
460
+ title: tab?.title ?? undefined,
461
+ url: tab?.url ?? undefined
462
+ };
463
+ }
464
+ emitTargetCreated(targetInfo) {
465
+ this.emitEvent("Target.targetCreated", { targetInfo });
466
+ }
467
+ emitTargetDestroyed(targetId) {
468
+ this.emitEvent("Target.targetDestroyed", { targetId });
469
+ }
470
+ emitTargetDetached(sessionId, targetId) {
471
+ this.emitEvent("Target.detachedFromTarget", { sessionId, targetId });
472
+ }
473
+ emitRootAttached(targetInfo) {
474
+ const record = this.sessions.getByTargetId(targetInfo.targetId);
475
+ if (!record || record.kind !== "root")
476
+ return;
477
+ if (this.rootAttachedSessions.has(record.sessionId))
478
+ return;
479
+ this.rootAttachedSessions.add(record.sessionId);
480
+ this.emitEvent("Target.attachedToTarget", {
481
+ sessionId: record.sessionId,
482
+ targetInfo,
483
+ waitingForDebugger: false
484
+ });
485
+ }
486
+ emitRootDetached() {
487
+ for (const targetInfo of this.sessions.listTargetInfos()) {
488
+ const record = this.sessions.getByTargetId(targetInfo.targetId);
489
+ if (!record || record.kind !== "root")
490
+ continue;
491
+ if (!this.rootAttachedSessions.has(record.sessionId))
492
+ continue;
493
+ this.rootAttachedSessions.delete(record.sessionId);
494
+ this.emitTargetDetached(record.sessionId, targetInfo.targetId);
495
+ }
496
+ }
497
+ resetRootAttached() {
498
+ this.rootAttachedSessions.clear();
499
+ }
500
+ createRootSessionId() {
501
+ const sessionId = `pw-tab-${this.sessionCounter}`;
502
+ this.sessionCounter += 1;
503
+ return sessionId;
504
+ }
505
+ async sendCommand(debuggee, method, params) {
75
506
  return new Promise((resolve, reject) => {
76
- chrome.debugger.sendCommand(this.debuggee, method, params, (result) => {
507
+ chrome.debugger.sendCommand(debuggee, method, params, (result) => {
77
508
  const lastError = chrome.runtime.lastError;
78
509
  if (lastError) {
79
510
  reject(new Error(lastError.message));
@@ -83,6 +514,10 @@ export class CDPRouter {
83
514
  });
84
515
  });
85
516
  }
517
+ isStaleTabError(error) {
518
+ const message = error instanceof Error ? error.message : String(error);
519
+ return message.includes("No tab with given id");
520
+ }
86
521
  async runDebuggerAction(action) {
87
522
  return new Promise((resolve, reject) => {
88
523
  action(() => {
@@ -95,68 +530,38 @@ export class CDPRouter {
95
530
  });
96
531
  });
97
532
  }
98
- handleEvent(source, method, params) {
99
- if (!this.matchesDebuggee(source) || !this.callbacks) {
100
- return;
101
- }
102
- if (method === "Target.receivedMessageFromTarget" && params && isRecord(params)) {
103
- const nested = parseNestedMessage(params.message);
104
- const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
105
- if (nested && (typeof nested.id === "string" || typeof nested.id === "number")) {
106
- const error = normalizeError(nested.error);
107
- this.callbacks.onResponse({
108
- id: nested.id,
109
- result: nested.result,
110
- error,
111
- sessionId
112
- });
113
- return;
114
- }
115
- if (nested && typeof nested.method === "string") {
116
- this.callbacks.onEvent({
117
- method: "forwardCDPEvent",
118
- params: {
119
- method: nested.method,
120
- params: nested.params,
121
- sessionId
122
- }
123
- });
124
- return;
125
- }
533
+ async safeDetach(debuggee) {
534
+ try {
535
+ await this.runDebuggerAction((done) => {
536
+ chrome.debugger.detach(debuggee, done);
537
+ });
126
538
  }
127
- this.callbacks.onEvent({
128
- method: "forwardCDPEvent",
129
- params: {
130
- method,
131
- params
132
- }
133
- });
134
- }
135
- handleDetach(source) {
136
- if (!this.matchesDebuggee(source) || !this.callbacks) {
137
- return;
539
+ catch (error) {
540
+ logError("cdp.safe_detach", error, { code: "detach_failed" });
138
541
  }
139
- this.callbacks.onDetach();
140
542
  }
141
- matchesDebuggee(source) {
142
- if (!this.debuggee)
143
- return false;
144
- return source.tabId === this.debuggee.tabId;
543
+ respond(id, result, sessionId) {
544
+ if (!this.callbacks)
545
+ return;
546
+ this.callbacks.onResponse({ id, result, ...(sessionId ? { sessionId } : {}) });
145
547
  }
146
- }
147
- const parseNestedMessage = (value) => {
148
- if (typeof value !== "string")
149
- return null;
150
- try {
151
- const parsed = JSON.parse(value);
152
- return parsed;
548
+ respondError(id, message, sessionId) {
549
+ if (!this.callbacks)
550
+ return;
551
+ this.callbacks.onResponse({ id, error: { message }, ...(sessionId ? { sessionId } : {}) });
153
552
  }
154
- catch {
155
- return null;
553
+ emitEvent(method, params, sessionId) {
554
+ if (!this.callbacks)
555
+ return;
556
+ const payload = { method, params };
557
+ if (sessionId) {
558
+ payload.sessionId = sessionId;
559
+ }
560
+ this.callbacks.onEvent({ method: "forwardCDPEvent", params: payload });
156
561
  }
157
- };
158
- const isRecord = (value) => {
159
- return typeof value === "object" && value !== null;
562
+ }
563
+ const isTargetInfo = (value) => {
564
+ return isRecord(value) && typeof value.targetId === "string" && typeof value.type === "string";
160
565
  };
161
566
  const getErrorMessage = (error) => {
162
567
  if (error instanceof Error) {
@@ -164,13 +569,6 @@ const getErrorMessage = (error) => {
164
569
  }
165
570
  return "Unknown error";
166
571
  };
167
- const normalizeError = (value) => {
168
- if (!isRecord(value)) {
169
- return undefined;
170
- }
171
- const message = value.message;
172
- if (typeof message !== "string") {
173
- return undefined;
174
- }
175
- return { message };
572
+ const isRecord = (value) => {
573
+ return typeof value === "object" && value !== null;
176
574
  };