diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-28 01:19:58 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | f9dcdf7f6dcdf2979693a9f179eda2b9995d57ae (patch) | |
| tree | 763ca6f879a693f56572ab256d33769d6d6bb0ad /src/cdp | |
| parent | b9c77bb6f9b8b83574489344ef6efd207a611e98 (diff) | |
refactor: close the remaining unguarded type assertions
Types the event-queue iterator's TReturn as undefined so the terminal
result needs no cast; declares the isError field the pi runtime reads by
duck-typing; binds socket to a local const in the daemon transport;
replaces the @ts-ignore on the optional sharp import with a non-literal
specifier so src/ still typechecks where sharp was not installed.
Adds src/util/guards.ts as the single home for narrowing over unknown —
isRecord plus scalar/array readers — and converts the two private
isObject copies in cdp/types.ts and daemon/protocol.ts along with every
inline `as Record<string, unknown>` in domains/js.ts and domains/render.ts.
Gives Accessibility.getFullAXTree a real result schema, closing the last
unvalidated CDP boundary: only nodeId is required, every object carries
additionalProperties, and each AX value stays Unknown, so the tree
decodes as a whole or fails as a whole rather than being filtered.
Guards the five tool-result `details` readers, the daemon attach
response, the persisted session state, the browser-namespace flag, and
the fs errno narrowing, each feeding an already-present failure path.
The daemon-transport socket binding is not a bug fix: send() runs
synchronously inside sendWithTimeout's promise executor, so socket cannot
be reassigned between the check and the write. It removes the assertion
and makes the write immune to a later refactor that adds an await.
Two assertions survive by design and are documented in the task report:
the never->unknown re-widening in util/tool.ts, which the type-erased
tool registry makes unavoidable, and the AsyncFunction constructor lookup
in domains/js.ts, which JavaScript exposes no typed binding for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/cdp')
| -rw-r--r-- | src/cdp/commands.ts | 29 | ||||
| -rw-r--r-- | src/cdp/daemon-transport.ts | 21 | ||||
| -rw-r--r-- | src/cdp/discovery.ts | 3 | ||||
| -rw-r--r-- | src/cdp/event-queue.ts | 13 | ||||
| -rw-r--r-- | src/cdp/session.ts | 4 | ||||
| -rw-r--r-- | src/cdp/types.ts | 9 |
6 files changed, 53 insertions, 26 deletions
diff --git a/src/cdp/commands.ts b/src/cdp/commands.ts index 95c8be2..e6d744d 100644 --- a/src/cdp/commands.ts +++ b/src/cdp/commands.ts @@ -50,6 +50,33 @@ const ExceptionDetails = Type.Object( { additionalProperties: true }, ); +const AxValue = Type.Object({ value: Type.Optional(Type.Unknown()) }, { additionalProperties: true }); + +const AxNode = Type.Object( + { + nodeId: Type.String(), + parentId: Type.Optional(Type.String()), + childIds: Type.Optional(Type.Array(Type.String())), + ignored: Type.Optional(Type.Boolean()), + role: Type.Optional(AxValue), + name: Type.Optional(AxValue), + value: Type.Optional(AxValue), + description: Type.Optional(AxValue), + properties: Type.Optional( + Type.Array( + Type.Object( + { name: Type.Optional(Type.String()), value: Type.Optional(AxValue) }, + { additionalProperties: true }, + ), + ), + ), + backendDOMNodeId: Type.Optional(Type.Number()), + }, + { additionalProperties: true }, +); + +export type AxNodeResult = Static<typeof AxNode>; + export const COMMANDS = { // --- Target --- "Target.attachToTarget": cmd( @@ -211,7 +238,7 @@ export const COMMANDS = { "Emulation.setDeviceMetricsOverride": cmd(Type.Object({}, { additionalProperties: true }), Empty), "Accessibility.getFullAXTree": cmd( Type.Object({ depth: Type.Optional(Type.Number()) }, { additionalProperties: true }), - Type.Object({ nodes: Type.Array(Type.Unknown()) }), + Type.Object({ nodes: Type.Array(AxNode) }), ), }; diff --git a/src/cdp/daemon-transport.ts b/src/cdp/daemon-transport.ts index 4b2fb78..819e581 100644 --- a/src/cdp/daemon-transport.ts +++ b/src/cdp/daemon-transport.ts @@ -67,21 +67,23 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { resolve(r); }; + let sock: Socket; try { - socket = createConnection(DAEMON_SOCKET_PATH); + sock = createConnection(DAEMON_SOCKET_PATH); } catch (e) { settle(err(cdpError("transport_closed", e instanceof Error ? e.message : String(e)))); return; } + socket = sock; const connectTimer = setTimeout(() => { cleanup("Connection timeout"); settle(err(cdpError("timeout", `Daemon connection timed out after ${timeoutMs}ms`))); }, timeoutMs); - socket.on("connect", () => { + sock.on("connect", () => { clearTimeout(connectTimer); - rl = createInterface({ input: socket!, crlfDelay: Infinity }); + rl = createInterface({ input: sock, crlfDelay: Infinity }); rl.on("line", (line: string) => { const msg = deserialize(line); @@ -130,11 +132,11 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { } }); - socket!.on("error", () => { + sock.on("error", () => { // Errors are surfaced via 'close' }); - socket!.on("close", () => { + sock.on("close", () => { clearTimeout(connectTimer); cleanup("Daemon socket closed"); // If connect hasn't settled yet, settle with error @@ -143,10 +145,10 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { // Send registration const regMsg: WireControl = { type: "control", action: "register", clientId }; - socket!.write(serialize(regMsg) + "\n"); + sock.write(serialize(regMsg) + "\n"); }); - socket.on("error", (e: NodeJS.ErrnoException) => { + sock.on("error", (e: NodeJS.ErrnoException) => { clearTimeout(connectTimer); const msg = e.code === "ENOENT" ? "Daemon not running — socket not found" @@ -176,7 +178,8 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { const timeoutMs = opts?.timeoutMs ?? 15_000; const sessionId = opts?.sessionId ?? null; - if (!socket || socket.destroyed || !registered) { + const sock = socket; + if (sock === null || sock.destroyed || !registered) { return Promise.resolve(err(cdpError("transport_closed", "Daemon not connected", method))); } @@ -189,7 +192,7 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { ...(sessionId !== null && sessionId !== undefined ? { sessionId } : {}), }; - return sendWithTimeout(pending, id, method, timeoutMs, "Daemon", () => socket!.write(serialize(req) + "\n")); + return sendWithTimeout(pending, id, method, timeoutMs, "Daemon", () => sock.write(serialize(req) + "\n")); }; // ── Events ───────────────────────────────────────────────────────────── diff --git a/src/cdp/discovery.ts b/src/cdp/discovery.ts index b4be31c..dc998b5 100644 --- a/src/cdp/discovery.ts +++ b/src/cdp/discovery.ts @@ -8,6 +8,7 @@ import { parseJson } from "../schemas/parse"; import { type Result, err, ok } from "../util/result"; import { userDataDirCandidates } from "../profile/paths"; import { type CdpError, cdpError } from "./errors"; +import { errnoCode } from "../util/guards"; const PORT_PROBE_DEADLINE_MS = 30_000; const PORT_PROBE_INTERVAL_MS = 1_000; @@ -149,7 +150,7 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>> // dir is on our list but the user hasn't installed that browser. EPERM/ // EACCES is common under sandboxes; remember it and fall back to network // probing rather than failing the whole discovery. - const code = (e as NodeJS.ErrnoException).code; + const code = errnoCode(e); if (code === "ENOENT" || code === undefined) continue; if (code === "EPERM" || code === "EACCES") { readErrors.push(`${portFile}: ${code}`); diff --git a/src/cdp/event-queue.ts b/src/cdp/event-queue.ts index 281fb3f..87f5551 100644 --- a/src/cdp/event-queue.ts +++ b/src/cdp/event-queue.ts @@ -30,7 +30,7 @@ export type EventQueue = { export const makeEventQueue = (): EventQueue => { const buf: CdpEvent[] = []; - const waiters: Array<(v: IteratorResult<CdpEvent>) => void> = []; + const waiters: Array<(v: IteratorResult<CdpEvent, undefined>) => void> = []; let ended = false; return { push(e) { @@ -41,19 +41,16 @@ export const makeEventQueue = (): EventQueue => { }, end() { ended = true; - // value: undefined as unknown as CdpEvent is the correct iterator-protocol - // pattern: when done=true the value field is conventionally undefined but - // the TS type still requires CdpEvent. - for (const w of waiters.splice(0)) w({ value: undefined as unknown as CdpEvent, done: true }); + for (const w of waiters.splice(0)) w({ value: undefined, done: true }); }, iter: { - [Symbol.asyncIterator]() { + [Symbol.asyncIterator](): AsyncIterator<CdpEvent, undefined, undefined> { return { - next: (): Promise<IteratorResult<CdpEvent>> => + next: (): Promise<IteratorResult<CdpEvent, undefined>> => new Promise((resolve) => { const next = buf.shift(); if (next) resolve({ value: next, done: false }); - else if (ended) resolve({ value: undefined as unknown as CdpEvent, done: true }); + else if (ended) resolve({ value: undefined, done: true }); else waiters.push(resolve); }), }; diff --git a/src/cdp/session.ts b/src/cdp/session.ts index de1ff4b..bf423e1 100644 --- a/src/cdp/session.ts +++ b/src/cdp/session.ts @@ -250,7 +250,8 @@ export const createCdpSession = ( const hw = ownership.harnessWindow(); if (hw && !live.has(hw)) ownership.setHarnessWindow(undefined); - if (survivors.length === 0) { + const anchorId = survivors[0]; + if (anchorId === undefined) { // No owned tab survived — the harness window is gone (user closed it, // or Chrome restarted and reassigned all ids). The persisted windowId // is now meaningless and could even collide with one of the user's @@ -263,7 +264,6 @@ export const createCdpSession = ( // Ownership is thus DERIVED from the real window, never from the bare // persisted integer (which Chrome may reassign to a user window on // restart — anchoring on a survivor makes that misfire impossible). - const anchorId = survivors[0]!; const anchorWin = await getWindowId(session, anchorId); if (anchorWin.success) { const anchorWindowId = anchorWin.data; diff --git a/src/cdp/types.ts b/src/cdp/types.ts index d57255f..33ab202 100644 --- a/src/cdp/types.ts +++ b/src/cdp/types.ts @@ -1,3 +1,5 @@ +import { isRecord } from "../util/guards"; + export type CdpEvent = { readonly method: string; readonly params: unknown; @@ -51,11 +53,8 @@ const INTERNAL_PREFIXES: ReadonlyArray<string> = [ export const isInternalUrl = (url: string): boolean => INTERNAL_PREFIXES.some((p) => url.startsWith(p)); -const isObject = (v: unknown): v is Readonly<Record<string, unknown>> => - typeof v === "object" && v !== null && !Array.isArray(v); - export const isCdpRawMessage = (v: unknown): v is CdpRawMessage => { - if (!isObject(v)) return false; + if (!isRecord(v)) return false; const id = v["id"]; if (id !== undefined && typeof id !== "number") return false; const method = v["method"]; @@ -64,7 +63,7 @@ export const isCdpRawMessage = (v: unknown): v is CdpRawMessage => { if (sessionId !== undefined && typeof sessionId !== "string") return false; const errVal = v["error"]; if (errVal !== undefined) { - if (!isObject(errVal)) return false; + if (!isRecord(errVal)) return false; if (typeof errVal["message"] !== "string") return false; const code = errVal["code"]; if (code !== undefined && typeof code !== "number") return false; |
