aboutsummaryrefslogtreecommitdiff
path: root/src/cdp/session.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-27 23:46:25 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit753c2ad615fa9827e168be256e1a7c7384605855 (patch)
tree03d62eb1816f4ad0ad3b634b87c4dcdb6fdbd83c /src/cdp/session.ts
parentd54d8e9e2dfbd31358b1f48e17338d74f5d270c0 (diff)
refactor(cdp): validate event params against a typed table
Malformed or unknown events are now dropped rather than hand-narrowed; consumers get typed params. Unknown methods are ignored, since the transport delivers every event Chrome emits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/cdp/session.ts')
-rw-r--r--src/cdp/session.ts139
1 files changed, 85 insertions, 54 deletions
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index 99e68b4..de1ff4b 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -4,6 +4,7 @@ import type { DialogInfo } from "./types";
import type { OwnershipRegistry } from "./ownership";
import type { CdpTransport } from "./transport";
import { type CdpMethod, type ParamsOf, type ResultOf, decodeResult } from "./commands";
+import { decodeEvent } from "./events";
import { createNetworkBuffer, type DrainResult, type NetworkFilter } from "./network-buffer";
import { createConsoleBuffer, type ConsoleDrainResult, type ConsoleFilter } from "./console-buffer";
import { attachTo } from "./attach";
@@ -29,6 +30,9 @@ type TabSession = {
refSig: Map<string, string>;
};
+const dialogType = (raw: string): DialogInfo["type"] =>
+ raw === "confirm" || raw === "prompt" || raw === "beforeunload" ? raw : "alert";
+
export type CdpSession = {
attachFirstPage(): Promise<Result<{ readonly targetId: string; readonly sessionId: string }, CdpError>>;
switchTo(targetId: string): Promise<Result<void, CdpError>>;
@@ -88,66 +92,93 @@ export const createCdpSession = (
if (ev.method !== "Target.targetDestroyed") {
if (ev.sessionId && ev.sessionId !== sessionId) continue;
}
- if (ev.method === "Page.javascriptDialogOpening") {
- const tab = targetId ? tabs.get(targetId) : undefined;
- if (!tab) continue;
- const params = ev.params as Partial<DialogInfo> | undefined;
- tab.dialog = {
- type: (params?.type as DialogInfo["type"]) ?? "alert",
- message: params?.message ?? "",
- ...(params?.defaultPrompt !== undefined ? { defaultPrompt: params.defaultPrompt } : {}),
- };
- continue;
- }
- // Page.javascriptDialogClosed is intentionally NOT cleared here —
- // the dialog stays in the buffer until takeDialog() is called.
- // This prevents fast dismiss flows from dropping a dialog the agent
- // was about to read. (Fix for spec §7 predictability bug #2.)
- if (ev.method === "Page.frameNavigated" || ev.method === "Page.loadEventFired") {
- const tab = targetId ? tabs.get(targetId) : undefined;
- if (tab) tab.pageInfoDirty = true;
- }
- if (ev.method === "Target.targetCreated" && ownership) {
- // Adopt tabs opened BY a tab we already own (window.open, target=_blank,
- // popups). Chrome sets openerId to the opener target; if that opener is
- // owned, the child belongs to this session's window and must be
- // controllable + cleaned up. Targets the user opens themselves have no
- // owned opener, so they are never adopted.
- const info = (ev.params as { targetInfo?: { targetId?: string; type?: string; openerId?: string } } | undefined)?.targetInfo;
- if (info?.type === "page" && info.targetId && info.openerId && ownership.has(info.openerId)) {
- ownership.add(info.targetId);
+ switch (ev.method) {
+ case "Page.javascriptDialogOpening": {
+ const tab = targetId ? tabs.get(targetId) : undefined;
+ if (!tab) break;
+ const decoded = decodeEvent(ev.method, ev.params);
+ tab.dialog = decoded.success
+ ? {
+ type: dialogType(decoded.data.type),
+ message: decoded.data.message,
+ ...(decoded.data.defaultPrompt !== undefined
+ ? { defaultPrompt: decoded.data.defaultPrompt }
+ : {}),
+ }
+ : { type: "alert", message: "" };
+ break;
}
- }
- if (ev.method === "Target.targetDestroyed" && ownership) {
- const params = ev.params as { targetId?: string } | undefined;
- if (params?.targetId) {
- ownership.remove(params.targetId);
+ // Page.javascriptDialogClosed is intentionally NOT cleared here —
+ // the dialog stays in the buffer until takeDialog() is called.
+ // This prevents fast dismiss flows from dropping a dialog the agent
+ // was about to read. (Fix for spec §7 predictability bug #2.)
+ case "Page.frameNavigated":
+ case "Page.loadEventFired": {
+ const tab = targetId ? tabs.get(targetId) : undefined;
+ if (tab) tab.pageInfoDirty = true;
+ break;
+ }
+ case "Target.targetCreated": {
+ // Adopt tabs opened BY a tab we already own (window.open, target=_blank,
+ // popups). Chrome sets openerId to the opener target; if that opener is
+ // owned, the child belongs to this session's window and must be
+ // controllable + cleaned up. Targets the user opens themselves have no
+ // owned opener, so they are never adopted.
+ if (!ownership) break;
+ const decoded = decodeEvent(ev.method, ev.params);
+ if (!decoded.success) break;
+ const info = decoded.data.targetInfo;
+ if (info.type === "page" && info.targetId && info.openerId && ownership.has(info.openerId)) {
+ ownership.add(info.targetId);
+ }
+ break;
+ }
+ case "Target.targetDestroyed": {
+ if (!ownership) break;
+ const decoded = decodeEvent(ev.method, ev.params);
+ if (!decoded.success || !decoded.data.targetId) break;
+ const destroyed = decoded.data.targetId;
+ ownership.remove(destroyed);
// Prune per-tab state for the destroyed target
- const tab = tabs.get(params.targetId);
+ const tab = tabs.get(destroyed);
if (tab) {
sessionIdToTargetId.delete(tab.sessionId);
- tabs.delete(params.targetId);
+ tabs.delete(destroyed);
}
+ break;
}
- }
- if (ev.method === "Network.requestWillBeSent") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.networkBuffer.ingestRequestWillBeSent(ev.params);
- } else if (ev.method === "Network.responseReceived") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.networkBuffer.ingestResponseReceived(ev.params);
- } else if (ev.method === "Network.loadingFinished") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.networkBuffer.ingestLoadingFinished(ev.params);
- } else if (ev.method === "Network.loadingFailed") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.networkBuffer.ingestLoadingFailed(ev.params);
- } else if (ev.method === "Runtime.consoleAPICalled") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.consoleBuffer.ingestConsoleApi(ev.params);
- } else if (ev.method === "Log.entryAdded") {
- const tab = resolveTab(ev.sessionId);
- if (tab) tab.consoleBuffer.ingestLogEntry(ev.params);
+ case "Network.requestWillBeSent": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.networkBuffer.ingestRequestWillBeSent(ev.params);
+ break;
+ }
+ case "Network.responseReceived": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.networkBuffer.ingestResponseReceived(ev.params);
+ break;
+ }
+ case "Network.loadingFinished": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.networkBuffer.ingestLoadingFinished(ev.params);
+ break;
+ }
+ case "Network.loadingFailed": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.networkBuffer.ingestLoadingFailed(ev.params);
+ break;
+ }
+ case "Runtime.consoleAPICalled": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.consoleBuffer.ingestConsoleApi(ev.params);
+ break;
+ }
+ case "Log.entryAdded": {
+ const tab = resolveTab(ev.sessionId);
+ if (tab) tab.consoleBuffer.ingestLogEntry(ev.params);
+ break;
+ }
+ default:
+ break;
}
}
};