@vforsh/argus-watcher 0.1.3 → 0.1.5

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 (131) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/buffer/AuthHeaderBuffer.d.ts +18 -0
  4. package/dist/buffer/AuthHeaderBuffer.d.ts.map +1 -0
  5. package/dist/buffer/AuthHeaderBuffer.js +26 -0
  6. package/dist/buffer/AuthHeaderBuffer.js.map +1 -0
  7. package/dist/buffer/NetBuffer.d.ts +2 -0
  8. package/dist/buffer/NetBuffer.d.ts.map +1 -1
  9. package/dist/buffer/NetBuffer.js +5 -0
  10. package/dist/buffer/NetBuffer.js.map +1 -1
  11. package/dist/cdp/auth.d.ts +8 -0
  12. package/dist/cdp/auth.d.ts.map +1 -0
  13. package/dist/cdp/auth.js +60 -0
  14. package/dist/cdp/auth.js.map +1 -0
  15. package/dist/cdp/connection.d.ts +16 -1
  16. package/dist/cdp/connection.d.ts.map +1 -1
  17. package/dist/cdp/connection.js +1 -1
  18. package/dist/cdp/connection.js.map +1 -1
  19. package/dist/cdp/dom/selector.d.ts.map +1 -1
  20. package/dist/cdp/dom/selector.js +22 -0
  21. package/dist/cdp/dom/selector.js.map +1 -1
  22. package/dist/cdp/dom/setFile.d.ts.map +1 -1
  23. package/dist/cdp/dom/setFile.js +5 -62
  24. package/dist/cdp/dom/setFile.js.map +1 -1
  25. package/dist/cdp/dom/types.d.ts +0 -1
  26. package/dist/cdp/dom/types.d.ts.map +1 -1
  27. package/dist/cdp/dom.d.ts +0 -2
  28. package/dist/cdp/dom.d.ts.map +1 -1
  29. package/dist/cdp/dom.js +0 -1
  30. package/dist/cdp/dom.js.map +1 -1
  31. package/dist/cdp/editor.d.ts +23 -0
  32. package/dist/cdp/editor.d.ts.map +1 -0
  33. package/dist/cdp/editor.js +266 -0
  34. package/dist/cdp/editor.js.map +1 -0
  35. package/dist/cdp/eval.d.ts +1 -0
  36. package/dist/cdp/eval.d.ts.map +1 -1
  37. package/dist/cdp/eval.js +57 -12
  38. package/dist/cdp/eval.js.map +1 -1
  39. package/dist/cdp/mouse.d.ts.map +1 -1
  40. package/dist/cdp/mouse.js +12 -8
  41. package/dist/cdp/mouse.js.map +1 -1
  42. package/dist/cdp/networkCapture.d.ts.map +1 -1
  43. package/dist/cdp/networkCapture.js +16 -6
  44. package/dist/cdp/networkCapture.js.map +1 -1
  45. package/dist/cdp/redaction.d.ts +4 -0
  46. package/dist/cdp/redaction.d.ts.map +1 -1
  47. package/dist/cdp/redaction.js +91 -0
  48. package/dist/cdp/redaction.js.map +1 -1
  49. package/dist/cdp/screenshot.d.ts.map +1 -1
  50. package/dist/cdp/screenshot.js +2 -6
  51. package/dist/cdp/screenshot.js.map +1 -1
  52. package/dist/cdp/text-filter.d.ts +0 -13
  53. package/dist/cdp/text-filter.d.ts.map +1 -1
  54. package/dist/cdp/text-filter.js +2 -20
  55. package/dist/cdp/text-filter.js.map +1 -1
  56. package/dist/cdp/tracing.d.ts +1 -0
  57. package/dist/cdp/tracing.d.ts.map +1 -1
  58. package/dist/cdp/tracing.js +49 -5
  59. package/dist/cdp/tracing.js.map +1 -1
  60. package/dist/events.d.ts +4 -1
  61. package/dist/events.d.ts.map +1 -1
  62. package/dist/http/router.d.ts.map +1 -1
  63. package/dist/http/router.js +8 -2
  64. package/dist/http/router.js.map +1 -1
  65. package/dist/http/routes/getAuthCookies.d.ts +3 -0
  66. package/dist/http/routes/getAuthCookies.d.ts.map +1 -0
  67. package/dist/http/routes/getAuthCookies.js +19 -0
  68. package/dist/http/routes/getAuthCookies.js.map +1 -0
  69. package/dist/http/routes/getAuthInspect.d.ts +3 -0
  70. package/dist/http/routes/getAuthInspect.d.ts.map +1 -0
  71. package/dist/http/routes/getAuthInspect.js +21 -0
  72. package/dist/http/routes/getAuthInspect.js.map +1 -0
  73. package/dist/http/routes/postAttach.d.ts.map +1 -1
  74. package/dist/http/routes/postAttach.js +4 -3
  75. package/dist/http/routes/postAttach.js.map +1 -1
  76. package/dist/http/routes/postCodeEdit.d.ts +3 -0
  77. package/dist/http/routes/postCodeEdit.d.ts.map +1 -0
  78. package/dist/http/routes/postCodeEdit.js +32 -0
  79. package/dist/http/routes/postCodeEdit.js.map +1 -0
  80. package/dist/http/routes/postCodeGrep.d.ts +3 -0
  81. package/dist/http/routes/postCodeGrep.d.ts.map +1 -0
  82. package/dist/http/routes/postCodeGrep.js +26 -0
  83. package/dist/http/routes/postCodeGrep.js.map +1 -0
  84. package/dist/http/routes/postCodeList.d.ts +3 -0
  85. package/dist/http/routes/postCodeList.d.ts.map +1 -0
  86. package/dist/http/routes/postCodeList.js +20 -0
  87. package/dist/http/routes/postCodeList.js.map +1 -0
  88. package/dist/http/routes/postCodeRead.d.ts +3 -0
  89. package/dist/http/routes/postCodeRead.d.ts.map +1 -0
  90. package/dist/http/routes/postCodeRead.js +30 -0
  91. package/dist/http/routes/postCodeRead.js.map +1 -0
  92. package/dist/http/routes/postDetach.d.ts.map +1 -1
  93. package/dist/http/routes/postDetach.js +4 -3
  94. package/dist/http/routes/postDetach.js.map +1 -1
  95. package/dist/http/routes/postDomSetFile.d.ts.map +1 -1
  96. package/dist/http/routes/postDomSetFile.js +0 -9
  97. package/dist/http/routes/postDomSetFile.js.map +1 -1
  98. package/dist/http/routes/postEval.d.ts.map +1 -1
  99. package/dist/http/routes/postEval.js +1 -0
  100. package/dist/http/routes/postEval.js.map +1 -1
  101. package/dist/http/server.d.ts +6 -1
  102. package/dist/http/server.d.ts.map +1 -1
  103. package/dist/http/server.js.map +1 -1
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +112 -98
  107. package/dist/index.js.map +1 -1
  108. package/dist/native-messaging/session-manager.d.ts +4 -1
  109. package/dist/native-messaging/session-manager.d.ts.map +1 -1
  110. package/dist/native-messaging/session-manager.js +11 -3
  111. package/dist/native-messaging/session-manager.js.map +1 -1
  112. package/dist/native-messaging/types.d.ts +36 -3
  113. package/dist/native-messaging/types.d.ts.map +1 -1
  114. package/dist/sources/cdp-source.d.ts.map +1 -1
  115. package/dist/sources/cdp-source.js +1 -0
  116. package/dist/sources/cdp-source.js.map +1 -1
  117. package/dist/sources/extension-frame-state.d.ts +64 -0
  118. package/dist/sources/extension-frame-state.d.ts.map +1 -0
  119. package/dist/sources/extension-frame-state.js +117 -0
  120. package/dist/sources/extension-frame-state.js.map +1 -0
  121. package/dist/sources/extension-log-events.d.ts +15 -0
  122. package/dist/sources/extension-log-events.d.ts.map +1 -0
  123. package/dist/sources/extension-log-events.js +101 -0
  124. package/dist/sources/extension-log-events.js.map +1 -0
  125. package/dist/sources/extension-source.d.ts +0 -3
  126. package/dist/sources/extension-source.d.ts.map +1 -1
  127. package/dist/sources/extension-source.js +469 -185
  128. package/dist/sources/extension-source.js.map +1 -1
  129. package/dist/sources/types.d.ts +19 -2
  130. package/dist/sources/types.d.ts.map +1 -1
  131. package/package.json +1 -1
@@ -2,255 +2,539 @@
2
2
  * Extension source for CDP access via Chrome extension Native Messaging.
3
3
  * Wraps SessionManager and provides a unified source interface.
4
4
  */
5
- import { previewStringify } from '@vforsh/argus-core';
6
5
  import { createNativeMessaging } from '../native-messaging/messaging.js';
7
6
  import { SessionManager } from '../native-messaging/session-manager.js';
7
+ import { buildFrameTargets, buildPageTarget, collectFrameTree, createEmptyFrameState, createNotAttachedError, frameToTarget, formatPageTargetId, parseExecutionContext, parseExtensionTargetId, parseFrame, resolveRequestedFrameId, } from './extension-frame-state.js';
8
+ import { toConsoleEvent, toExceptionEvent } from './extension-log-events.js';
8
9
  /**
9
10
  * Create an extension source that connects to Chrome via Native Messaging.
10
11
  * Returns a handle that can be used to control the source and access CDP session.
11
12
  */
12
13
  export const createExtensionSource = (options) => {
13
- const { events, ignoreMatcher, stripUrlPrefixes } = options;
14
+ const { events, ignoreMatcher, stripUrlPrefixes, watcherId, watcherHost, watcherPort } = options;
14
15
  const messaging = createNativeMessaging();
16
+ const hostInfo = {
17
+ watcherId: watcherId ?? 'extension',
18
+ watcherHost: watcherHost ?? '127.0.0.1',
19
+ watcherPort: watcherPort ?? 0,
20
+ watcherPid: process.pid,
21
+ };
15
22
  let currentSession = null;
16
23
  let stopping = false;
17
- // Create session manager with event handlers
24
+ const frameStateByTabId = new Map();
25
+ const delegatingSessions = new Set();
26
+ const getCurrentExtensionSession = () => {
27
+ if (!currentSession) {
28
+ throw createNotAttachedError();
29
+ }
30
+ return currentSession;
31
+ };
18
32
  const sessionManager = new SessionManager(messaging, {
19
33
  onAttach: (session) => {
20
34
  console.error(`[ExtensionSource] Tab attached: ${session.tabId} - ${session.url}`);
21
35
  currentSession = session;
22
- sessionManager.enableDomain(session.tabId, 'Page');
23
- // Subscribe to console events
24
- session.handle.onEvent('Runtime.consoleAPICalled', (params) => {
25
- const logEvent = toConsoleEvent(params, session, { ignoreMatcher, stripUrlPrefixes });
26
- events.onLog(logEvent);
27
- });
28
- // Subscribe to exception events
29
- session.handle.onEvent('Runtime.exceptionThrown', (params) => {
30
- const logEvent = toExceptionEvent(params, session, { ignoreMatcher, stripUrlPrefixes });
31
- events.onLog(logEvent);
32
- });
33
- // Notify status change
34
- const target = {
35
- id: String(session.tabId),
36
- title: session.title,
37
- url: session.url,
38
- type: 'page',
39
- faviconUrl: session.faviconUrl,
40
- attached: true,
41
- };
42
- events.onStatus({
43
- attached: true,
44
- target: {
45
- title: session.title,
46
- url: session.url,
47
- type: 'page',
48
- parentId: null,
49
- },
50
- reason: null,
51
- });
52
- void events.onAttach?.(session.handle, target);
36
+ rebindDelegatingSessions();
37
+ seedFrameState(session);
38
+ void bootstrapAttachedSession(session);
53
39
  },
54
40
  onDetach: (tabId, reason) => {
55
41
  console.error(`[ExtensionSource] Tab detached: ${tabId} - ${reason}`);
56
42
  if (currentSession?.tabId === tabId) {
57
43
  currentSession = null;
58
44
  }
59
- events.onStatus({
60
- attached: false,
61
- target: null,
62
- reason,
63
- });
45
+ rebindDelegatingSessions();
46
+ frameStateByTabId.delete(tabId);
47
+ emitStatus(null, reason);
64
48
  events.onDetach?.(reason);
65
49
  },
66
50
  onTabsUpdated: () => {
67
51
  // Tabs list updated - no action needed
68
52
  },
69
- });
70
- // Create a proxy session handle that delegates to the current session
71
- const proxySession = {
72
- isAttached: () => currentSession?.handle.isAttached() ?? false,
73
- sendAndWait: async (method, params, options) => {
53
+ onTargetSelected: (tabId, frameId) => {
74
54
  const session = currentSession;
75
- if (!session) {
76
- const error = new Error('No tab attached via extension');
77
- error.code = 'cdp_not_attached';
78
- throw error;
55
+ if (!session || session.tabId !== tabId) {
56
+ getOrCreateFrameState(tabId).requestedFrameId = frameId;
57
+ return;
79
58
  }
80
- return session.handle.sendAndWait(method, params, options);
59
+ requestTargetSelection(session, frameId);
81
60
  },
82
- onEvent: (method, handler) => {
83
- // For proxy session, we need to track all handlers and replay them
84
- // when a new session is attached. For now, just use the current session.
85
- if (!currentSession) {
86
- // Return a no-op unsubscribe function
87
- return () => { };
61
+ });
62
+ // Proxy session follows the currently selected page/frame target inside the attached tab.
63
+ const proxySession = createDelegatingSession({
64
+ getTargetContext: () => getCurrentTargetContext() ?? { kind: 'page' },
65
+ mapParams: (method, params) => {
66
+ const targetContext = getCurrentTargetContext();
67
+ if (method !== 'Runtime.evaluate' || targetContext?.kind !== 'frame' || params?.contextId != null) {
68
+ return params;
88
69
  }
89
- return currentSession.handle.onEvent(method, handler);
70
+ if (targetContext.executionContextId == null) {
71
+ throw new Error(`Selected frame is not ready yet: ${targetContext.frameId}`);
72
+ }
73
+ return {
74
+ ...(params ?? {}),
75
+ contextId: targetContext.executionContextId,
76
+ };
90
77
  },
91
- };
92
- // Start Native Messaging
78
+ });
79
+ // Page session always stays at the top-level tab context so page-level features
80
+ // (indicator, inject-on-attach, similar lifecycle hooks) don't accidentally run inside an iframe.
81
+ const pageSession = createDelegatingSession({
82
+ getTargetContext: () => ({ kind: 'page' }),
83
+ });
93
84
  messaging.start();
85
+ sendHostInfo();
94
86
  messaging.onDisconnect(() => {
95
87
  console.error('[ExtensionSource] Extension disconnected');
96
88
  if (!stopping) {
97
- events.onStatus({
98
- attached: false,
99
- target: null,
100
- reason: 'extension_disconnected',
101
- });
89
+ emitStatus(null, 'extension_disconnected');
102
90
  events.onDetach?.('extension_disconnected');
103
91
  }
104
92
  });
105
93
  const stop = async () => {
106
94
  stopping = true;
95
+ disposeDelegatingSessions();
107
96
  messaging.stop();
108
97
  };
109
98
  const listTargets = async () => {
110
99
  const tabs = await sessionManager.listTabs();
111
- return tabs.map(tabToTarget);
100
+ return tabs.flatMap((tab) => {
101
+ const session = currentSession?.tabId === tab.tabId ? currentSession : null;
102
+ const state = frameStateByTabId.get(tab.tabId);
103
+ if (!session || !state || state.frames.size === 0) {
104
+ return [tabToTarget(tab)];
105
+ }
106
+ const activeFrameId = state.activeFrameId;
107
+ const targets = [buildPageTarget(session, { attached: activeFrameId == null })];
108
+ for (const frame of buildFrameTargets(state, tab.tabId, activeFrameId, tab.faviconUrl)) {
109
+ targets.push(frame);
110
+ }
111
+ return targets;
112
+ });
112
113
  };
113
114
  const attachTarget = (targetId) => {
114
- sessionManager.attachTab(targetId);
115
+ const target = parseExtensionTargetId(targetId);
116
+ const session = currentSession;
117
+ if (!session || session.tabId !== target.tabId) {
118
+ getOrCreateFrameState(target.tabId).requestedFrameId = target.frameId;
119
+ sessionManager.attachTab(target.tabId);
120
+ return;
121
+ }
122
+ requestTargetSelection(session, target.frameId);
115
123
  };
116
124
  const detachTarget = (targetId) => {
117
- sessionManager.detachTab(targetId);
125
+ sessionManager.detachTab(parseExtensionTargetId(targetId).tabId);
118
126
  };
119
127
  return {
120
128
  session: proxySession,
129
+ pageSession,
130
+ syncWatcherInfo: (info) => {
131
+ hostInfo.watcherId = info.watcherId;
132
+ hostInfo.watcherHost = info.watcherHost;
133
+ hostInfo.watcherPort = info.watcherPort;
134
+ hostInfo.watcherPid = info.watcherPid;
135
+ sendHostInfo();
136
+ },
121
137
  stop,
122
138
  listTargets,
123
139
  attachTarget,
124
140
  detachTarget,
125
141
  };
126
- };
127
- /**
128
- * Convert TabInfo to CdpSourceTarget.
129
- */
130
- const tabToTarget = (tab) => ({
131
- id: String(tab.tabId),
132
- title: tab.title,
133
- url: tab.url,
134
- type: 'page',
135
- faviconUrl: tab.faviconUrl,
136
- attached: tab.attached,
137
- });
138
- /**
139
- * Convert Runtime.consoleAPICalled event to LogEvent.
140
- */
141
- const toConsoleEvent = (params, session, config) => {
142
- const record = params;
143
- const args = record.args?.map((a) => a.value) ?? [];
144
- const text = formatArgs(record.args ?? []);
145
- const frame = selectBestFrame(record.stackTrace?.callFrames, config.ignoreMatcher);
146
- return {
147
- ts: record.timestamp ?? Date.now(),
148
- level: normalizeLevel(record.type ?? 'log'),
149
- text,
150
- args,
151
- source: 'console',
152
- file: applyStripPrefixes(frame?.url ?? null, config.stripUrlPrefixes),
153
- line: frame?.lineNumber != null ? frame.lineNumber + 1 : null,
154
- column: frame?.columnNumber != null ? frame.columnNumber + 1 : null,
155
- pageUrl: session.url,
156
- pageTitle: session.title,
157
- };
158
- };
159
- /**
160
- * Convert Runtime.exceptionThrown event to LogEvent.
161
- */
162
- const toExceptionEvent = (params, session, config) => {
163
- const record = params;
164
- const details = record.exceptionDetails;
165
- const text = details?.exception?.description ?? details?.text ?? 'Exception';
166
- const args = details?.exception ? [details.exception.value ?? details.exception.description] : [];
167
- const frame = selectBestFrame(details?.stackTrace?.callFrames, config.ignoreMatcher);
168
- return {
169
- ts: record.timestamp ?? Date.now(),
170
- level: 'exception',
171
- text,
172
- args,
173
- source: 'exception',
174
- file: applyStripPrefixes(frame?.url ?? null, config.stripUrlPrefixes),
175
- line: frame?.lineNumber != null ? frame.lineNumber + 1 : null,
176
- column: frame?.columnNumber != null ? frame.columnNumber + 1 : null,
177
- pageUrl: session.url,
178
- pageTitle: session.title,
179
- };
180
- };
181
- /**
182
- * Format console args for display.
183
- */
184
- const formatArgs = (args) => {
185
- if (args.length === 0) {
186
- return '';
142
+ function sendHostInfo() {
143
+ messaging.send({
144
+ type: 'host_info',
145
+ watcherId: hostInfo.watcherId,
146
+ watcherHost: hostInfo.watcherHost,
147
+ watcherPort: hostInfo.watcherPort,
148
+ pid: hostInfo.watcherPid,
149
+ });
187
150
  }
188
- return args
189
- .map((arg) => {
190
- if (arg.value !== undefined) {
191
- return typeof arg.value === 'string' ? arg.value : previewStringify(arg.value);
151
+ function rebindDelegatingSessions() {
152
+ for (const controller of delegatingSessions) {
153
+ controller.rebind();
192
154
  }
193
- return arg.description ?? `[${arg.type}]`;
194
- })
195
- .join(' ');
196
- };
197
- /**
198
- * Normalize console log type to LogLevel.
199
- */
200
- const normalizeLevel = (type) => {
201
- switch (type) {
202
- case 'log':
203
- case 'info':
204
- case 'debug':
205
- case 'dir':
206
- case 'dirxml':
207
- case 'table':
208
- case 'trace':
209
- case 'count':
210
- case 'timeEnd':
211
- case 'timeLog':
212
- return type === 'debug' ? 'debug' : type === 'info' ? 'info' : 'log';
213
- case 'warn':
214
- case 'warning':
215
- return 'warning';
216
- case 'error':
217
- case 'assert':
218
- case 'exception':
219
- return type === 'exception' ? 'exception' : 'error';
220
- default:
221
- return 'log';
222
155
  }
223
- };
224
- /**
225
- * Select the best frame from the stack trace (first non-ignored frame).
226
- */
227
- const selectBestFrame = (frames, ignoreMatcher) => {
228
- if (!frames || frames.length === 0) {
229
- return null;
156
+ function disposeDelegatingSessions() {
157
+ for (const controller of delegatingSessions) {
158
+ controller.dispose();
159
+ }
160
+ }
161
+ async function bootstrapAttachedSession(session) {
162
+ try {
163
+ registerSessionEventHandlers(session);
164
+ await enableBootstrapDomains(session);
165
+ await refreshFrameTree(session);
166
+ if (currentSession?.tabId !== session.tabId) {
167
+ return;
168
+ }
169
+ const target = buildPageTarget(session, { attached: true });
170
+ emitStatus(target, null);
171
+ await events.onAttach?.(session.handle, target);
172
+ reconcileTargetSelection(session);
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ console.error(`[ExtensionSource] Failed to bootstrap attached tab ${session.tabId}: ${message}`);
177
+ if (currentSession?.tabId === session.tabId) {
178
+ emitStatus(null, message);
179
+ events.onDetach?.(message);
180
+ }
181
+ }
182
+ }
183
+ /**
184
+ * Subscribe before enabling Runtime/Page so we don't miss the initial execution contexts
185
+ * that Chrome emits as soon as Runtime becomes active.
186
+ */
187
+ async function enableBootstrapDomains(session) {
188
+ await session.handle.sendAndWait('Runtime.enable');
189
+ await session.handle.sendAndWait('Page.enable');
190
+ }
191
+ function registerSessionEventHandlers(session) {
192
+ session.handle.onEvent('Runtime.executionContextCreated', (params, meta) => {
193
+ const context = parseExecutionContext(params);
194
+ if (!context?.isDefault || !context.frameId) {
195
+ return;
196
+ }
197
+ const state = getOrCreateFrameState(session.tabId);
198
+ state.executionContexts.set(context.frameId, context.id);
199
+ const frame = state.frames.get(context.frameId);
200
+ if (frame) {
201
+ frame.sessionId = meta.sessionId ?? null;
202
+ }
203
+ void refreshFrameTitle(session, context.frameId, context.id);
204
+ reconcileTargetSelection(session);
205
+ });
206
+ session.handle.onEvent('Runtime.executionContextsCleared', () => {
207
+ const state = getOrCreateFrameState(session.tabId);
208
+ state.executionContexts.clear();
209
+ state.pendingTitleLoads.clear();
210
+ });
211
+ session.handle.onEvent('Runtime.executionContextDestroyed', (params) => {
212
+ const record = params;
213
+ if (record.executionContextId == null) {
214
+ return;
215
+ }
216
+ const state = getOrCreateFrameState(session.tabId);
217
+ for (const [frameId, contextId] of state.executionContexts.entries()) {
218
+ if (contextId === record.executionContextId) {
219
+ state.executionContexts.delete(frameId);
220
+ state.pendingTitleLoads.delete(frameId);
221
+ }
222
+ }
223
+ });
224
+ session.handle.onEvent('Page.frameNavigated', (params, meta) => {
225
+ const frame = parseFrame(params);
226
+ if (!frame) {
227
+ return;
228
+ }
229
+ const state = getOrCreateFrameState(session.tabId);
230
+ state.frames.set(frame.frameId, frame);
231
+ frame.sessionId = meta.sessionId ?? null;
232
+ if (!frame.parentFrameId) {
233
+ if (!meta.sessionId) {
234
+ state.topFrameId = frame.frameId;
235
+ session.url = frame.url;
236
+ }
237
+ else if (state.topFrameId == null) {
238
+ state.topFrameId = frame.parentFrameId;
239
+ }
240
+ }
241
+ else if (state.executionContexts.has(frame.frameId)) {
242
+ void refreshFrameTitle(session, frame.frameId);
243
+ }
244
+ currentSession = session;
245
+ if (!frame.parentFrameId && !meta.sessionId) {
246
+ events.onPageNavigation?.({ url: frame.url, title: session.title ?? null });
247
+ }
248
+ if (reconcileTargetSelection(session)) {
249
+ return;
250
+ }
251
+ if ((!frame.parentFrameId && state.activeFrameId == null) || state.activeFrameId === frame.frameId) {
252
+ emitTargetChanged(session);
253
+ }
254
+ });
255
+ session.handle.onEvent('Page.frameAttached', (params, meta) => {
256
+ const record = params;
257
+ if (!record.frameId) {
258
+ return;
259
+ }
260
+ const state = getOrCreateFrameState(session.tabId);
261
+ state.frames.set(record.frameId, {
262
+ frameId: record.frameId,
263
+ parentFrameId: record.parentFrameId ?? state.topFrameId ?? null,
264
+ url: '',
265
+ title: null,
266
+ sessionId: meta.sessionId ?? null,
267
+ });
268
+ reconcileTargetSelection(session);
269
+ });
270
+ session.handle.onEvent('Page.frameDetached', (params) => {
271
+ const record = params;
272
+ if (!record.frameId) {
273
+ return;
274
+ }
275
+ removeFrame(session.tabId, record.frameId);
276
+ reconcileTargetSelection(session);
277
+ });
278
+ session.handle.onEvent('Page.domContentEventFired', () => {
279
+ events.onPageLoad?.();
280
+ });
281
+ session.handle.onEvent('Runtime.consoleAPICalled', (params) => {
282
+ events.onLog(toConsoleEvent(params, session, { ignoreMatcher, stripUrlPrefixes }));
283
+ });
284
+ session.handle.onEvent('Runtime.exceptionThrown', (params) => {
285
+ events.onLog(toExceptionEvent(params, session, { ignoreMatcher, stripUrlPrefixes }));
286
+ });
230
287
  }
231
- if (!ignoreMatcher) {
232
- return frames[0] ?? null;
288
+ function getCurrentTargetContext() {
289
+ const session = currentSession;
290
+ if (!session) {
291
+ return null;
292
+ }
293
+ const state = frameStateByTabId.get(session.tabId);
294
+ if (!state?.activeFrameId) {
295
+ return { kind: 'page' };
296
+ }
297
+ return {
298
+ kind: 'frame',
299
+ frameId: state.activeFrameId,
300
+ executionContextId: state.executionContexts.get(state.activeFrameId) ?? null,
301
+ sessionId: state.frames.get(state.activeFrameId)?.sessionId ?? null,
302
+ };
233
303
  }
234
- for (const frame of frames) {
235
- if (frame.url && !ignoreMatcher(frame.url)) {
236
- return frame;
304
+ function getOrCreateFrameState(tabId) {
305
+ const existing = frameStateByTabId.get(tabId);
306
+ if (existing) {
307
+ return existing;
237
308
  }
309
+ const created = createEmptyFrameState();
310
+ frameStateByTabId.set(tabId, created);
311
+ return created;
238
312
  }
239
- // Fall back to first frame if all are ignored
240
- return frames[0] ?? null;
241
- };
242
- /**
243
- * Apply strip prefixes to a file URL.
244
- */
245
- const applyStripPrefixes = (file, prefixes) => {
246
- if (!file || !prefixes || prefixes.length === 0) {
247
- return file;
313
+ function seedFrameState(session) {
314
+ const state = getOrCreateFrameState(session.tabId);
315
+ if (state.frames.size > 0 || session.frames.length === 0) {
316
+ return state;
317
+ }
318
+ state.topFrameId = session.topFrameId;
319
+ for (const frame of session.frames) {
320
+ state.frames.set(frame.frameId, {
321
+ frameId: frame.frameId,
322
+ parentFrameId: frame.parentFrameId,
323
+ url: frame.url,
324
+ title: frame.title,
325
+ sessionId: frame.sessionId,
326
+ });
327
+ }
328
+ return state;
329
+ }
330
+ function emitStatus(target, reason) {
331
+ events.onStatus({
332
+ attached: Boolean(target),
333
+ target: target
334
+ ? {
335
+ title: target.title,
336
+ url: target.url,
337
+ type: target.type ?? 'page',
338
+ parentId: target.parentId ?? null,
339
+ }
340
+ : null,
341
+ reason,
342
+ });
343
+ }
344
+ function emitTargetChanged(session) {
345
+ const target = getSelectedTarget(session);
346
+ if (!target) {
347
+ return;
348
+ }
349
+ const state = getOrCreateFrameState(session.tabId);
350
+ if (state.activeAttachedAt == null) {
351
+ state.activeAttachedAt = Date.now();
352
+ }
353
+ emitStatus(target, null);
354
+ messaging.send({
355
+ type: 'target_info',
356
+ targetId: target.id,
357
+ title: target.title,
358
+ url: target.url,
359
+ attachedAt: state.activeAttachedAt,
360
+ });
361
+ events.onTargetChanged?.(session.handle, target);
362
+ }
363
+ function getSelectedTarget(session) {
364
+ const state = frameStateByTabId.get(session.tabId);
365
+ if (!state?.activeFrameId) {
366
+ return buildPageTarget(session, { attached: true });
367
+ }
368
+ const frame = state.frames.get(state.activeFrameId);
369
+ if (!frame) {
370
+ return buildPageTarget(session, { attached: true });
371
+ }
372
+ return frameToTarget(session.tabId, frame, { attached: true, faviconUrl: session.faviconUrl, topFrameId: state.topFrameId });
373
+ }
374
+ function requestTargetSelection(session, frameId) {
375
+ const state = getOrCreateFrameState(session.tabId);
376
+ state.requestedFrameId = frameId;
377
+ reconcileTargetSelection(session);
248
378
  }
249
- for (const prefix of prefixes) {
250
- if (file.startsWith(prefix)) {
251
- return file.slice(prefix.length);
379
+ /**
380
+ * Popup selection can arrive before the iframe's session is fully bootstrapped.
381
+ * Keep the request pending until the frame is discovered instead of routing commands
382
+ * to the parent page and pretending the switch already happened.
383
+ */
384
+ function reconcileTargetSelection(session) {
385
+ const state = getOrCreateFrameState(session.tabId);
386
+ const nextActiveFrameId = resolveRequestedFrameId(state, state.requestedFrameId);
387
+ if (state.requestedFrameId == null) {
388
+ if (state.activeFrameId == null) {
389
+ if (state.activeAttachedAt == null) {
390
+ state.activeAttachedAt = Date.now();
391
+ emitTargetChanged(session);
392
+ return true;
393
+ }
394
+ return false;
395
+ }
396
+ state.activeFrameId = null;
397
+ state.activeAttachedAt = Date.now();
398
+ emitTargetChanged(session);
399
+ return true;
400
+ }
401
+ if (nextActiveFrameId == null || state.activeFrameId === nextActiveFrameId) {
402
+ return false;
403
+ }
404
+ state.activeFrameId = nextActiveFrameId;
405
+ state.activeAttachedAt = Date.now();
406
+ emitTargetChanged(session);
407
+ return true;
408
+ }
409
+ async function refreshFrameTree(session) {
410
+ const state = getOrCreateFrameState(session.tabId);
411
+ const frameTree = (await session.handle.sendAndWait('Page.getFrameTree'));
412
+ collectFrameTree(frameTree.frameTree, state);
413
+ await Promise.all([...state.executionContexts.keys()].map((frameId) => refreshFrameTitle(session, frameId)));
414
+ }
415
+ function removeFrame(tabId, frameId) {
416
+ const state = getOrCreateFrameState(tabId);
417
+ const childIds = [...state.frames.values()].filter((frame) => frame.parentFrameId === frameId).map((frame) => frame.frameId);
418
+ for (const childId of childIds) {
419
+ removeFrame(tabId, childId);
420
+ }
421
+ if (state.requestedFrameId === frameId) {
422
+ state.requestedFrameId = null;
423
+ }
424
+ state.frames.delete(frameId);
425
+ state.executionContexts.delete(frameId);
426
+ state.pendingTitleLoads.delete(frameId);
427
+ }
428
+ /**
429
+ * Frame tree metadata does not include the iframe document title, so we resolve it from the
430
+ * frame's default execution context and cache it alongside the rest of the frame state.
431
+ */
432
+ async function refreshFrameTitle(session, frameId, contextId) {
433
+ const state = getOrCreateFrameState(session.tabId);
434
+ if (frameId === state.topFrameId || state.pendingTitleLoads.has(frameId)) {
435
+ return;
436
+ }
437
+ const executionContextId = contextId ?? state.executionContexts.get(frameId);
438
+ if (executionContextId == null) {
439
+ return;
440
+ }
441
+ const frame = state.frames.get(frameId);
442
+ if (!frame) {
443
+ return;
444
+ }
445
+ state.pendingTitleLoads.add(frameId);
446
+ try {
447
+ const evaluated = (await session.handle.sendAndWait('Runtime.evaluate', {
448
+ expression: 'document.title',
449
+ contextId: executionContextId,
450
+ returnByValue: true,
451
+ silent: true,
452
+ }, frame.sessionId ? { sessionId: frame.sessionId } : undefined));
453
+ const title = typeof evaluated.result?.value === 'string' ? evaluated.result.value.trim() : '';
454
+ const latestFrame = state.frames.get(frameId);
455
+ if (!latestFrame) {
456
+ return;
457
+ }
458
+ latestFrame.title = title || null;
459
+ if (state.activeFrameId === frameId) {
460
+ emitTargetChanged(session);
461
+ }
462
+ }
463
+ catch {
464
+ // Ignore transient frame-title lookup failures during navigation/bootstrap.
465
+ }
466
+ finally {
467
+ state.pendingTitleLoads.delete(frameId);
252
468
  }
253
469
  }
254
- return file;
470
+ function createDelegatingSession(config) {
471
+ const subscriptions = new Set();
472
+ const rebindSubscriptions = () => {
473
+ // Delegating sessions are created before a tab may be attached, so event listeners
474
+ // must follow the active extension session instead of binding once and going stale.
475
+ for (const subscription of subscriptions) {
476
+ subscription.unbind();
477
+ if (!currentSession) {
478
+ continue;
479
+ }
480
+ subscription.off = currentSession.handle.onEvent(subscription.method, subscription.handler);
481
+ }
482
+ };
483
+ const disposeSubscriptions = () => {
484
+ for (const subscription of subscriptions) {
485
+ subscription.unbind();
486
+ }
487
+ subscriptions.clear();
488
+ };
489
+ const controller = {
490
+ rebind: rebindSubscriptions,
491
+ dispose: () => {
492
+ disposeSubscriptions();
493
+ delegatingSessions.delete(controller);
494
+ },
495
+ };
496
+ delegatingSessions.add(controller);
497
+ controller.rebind();
498
+ return {
499
+ isAttached: () => currentSession?.handle.isAttached() ?? false,
500
+ sendAndWait: async (method, params, options) => {
501
+ const session = getCurrentExtensionSession();
502
+ const targetContext = config.getTargetContext();
503
+ const nextParams = config.mapParams ? config.mapParams(method, params) : params;
504
+ const nextOptions = targetContext.kind === 'frame' && targetContext.sessionId ? { ...(options ?? {}), sessionId: targetContext.sessionId } : options;
505
+ return session.handle.sendAndWait(method, nextParams, nextOptions);
506
+ },
507
+ onEvent: (method, handler) => {
508
+ const subscription = createDelegatingEventSubscription(method, handler);
509
+ subscriptions.add(subscription);
510
+ controller.rebind();
511
+ return () => {
512
+ subscription.unbind();
513
+ subscriptions.delete(subscription);
514
+ };
515
+ },
516
+ getTargetContext: config.getTargetContext,
517
+ };
518
+ }
255
519
  };
520
+ /**
521
+ * Convert TabInfo to CdpSourceTarget.
522
+ */
523
+ const tabToTarget = (tab) => ({
524
+ id: formatPageTargetId(tab.tabId),
525
+ title: tab.title,
526
+ url: tab.url,
527
+ type: 'page',
528
+ faviconUrl: tab.faviconUrl,
529
+ attached: tab.attached,
530
+ });
531
+ const createDelegatingEventSubscription = (method, handler) => ({
532
+ method,
533
+ handler,
534
+ off: null,
535
+ unbind() {
536
+ this.off?.();
537
+ this.off = null;
538
+ },
539
+ });
256
540
  //# sourceMappingURL=extension-source.js.map