diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cdp/commands.ts | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/src/cdp/commands.ts b/src/cdp/commands.ts new file mode 100644 index 0000000..123b9f6 --- /dev/null +++ b/src/cdp/commands.ts @@ -0,0 +1,217 @@ +import { Type, type Static, type TSchema } from "typebox"; +import { Compile } from "typebox/compile"; +import { type Result, err, ok } from "../util/result"; +import { type CdpError, cdpError } from "./errors"; + +const cmd = <P extends TSchema, R extends TSchema>(params: P, result: R) => ({ + params, + result, + validate: Compile(result), +}); + +const Empty = Type.Object({}); + +const TargetInfo = Type.Object( + { + targetId: Type.String(), + type: Type.String(), + title: Type.String(), + url: Type.String(), + attached: Type.Optional(Type.Boolean()), + openerId: Type.Optional(Type.String()), + browserContextId: Type.Optional(Type.String()), + }, + { additionalProperties: true }, +); + +const RemoteObject = Type.Object( + { + type: Type.String(), + subtype: Type.Optional(Type.String()), + className: Type.Optional(Type.String()), + value: Type.Optional(Type.Unknown()), + description: Type.Optional(Type.String()), + objectId: Type.Optional(Type.String()), + }, + { additionalProperties: true }, +); + +const ExceptionDetails = Type.Object( + { text: Type.String(), exception: Type.Optional(RemoteObject) }, + { additionalProperties: true }, +); + +export const COMMANDS = { + // --- Target --- + "Target.attachToTarget": cmd( + Type.Object({ targetId: Type.String(), flatten: Type.Optional(Type.Boolean()) }), + Type.Object({ sessionId: Type.String() }), + ), + "Target.detachFromTarget": cmd( + Type.Object({ sessionId: Type.Optional(Type.String()) }), + Empty, + ), + "Target.createTarget": cmd( + Type.Object({ + url: Type.String(), + newWindow: Type.Optional(Type.Boolean()), + background: Type.Optional(Type.Boolean()), + windowId: Type.Optional(Type.Number()), + }), + Type.Object({ targetId: Type.String() }), + ), + "Target.closeTarget": cmd( + Type.Object({ targetId: Type.String() }), + Type.Object({ success: Type.Optional(Type.Boolean()) }), + ), + "Target.activateTarget": cmd(Type.Object({ targetId: Type.String() }), Empty), + "Target.getTargetInfo": cmd( + Type.Object({ targetId: Type.Optional(Type.String()) }), + Type.Object({ targetInfo: TargetInfo }), + ), + "Target.getTargets": cmd( + Type.Object({}), + Type.Object({ targetInfos: Type.Array(TargetInfo) }), + ), + "Target.setDiscoverTargets": cmd(Type.Object({ discover: Type.Boolean() }), Empty), + + // --- Browser --- + "Browser.getWindowForTarget": cmd( + Type.Object({ targetId: Type.Optional(Type.String()) }), + Type.Object({ windowId: Type.Number(), bounds: Type.Optional(Type.Unknown()) }), + ), + "Browser.setDownloadBehavior": cmd( + Type.Object({ + behavior: Type.String(), + downloadPath: Type.Optional(Type.String()), + eventsEnabled: Type.Optional(Type.Boolean()), + }), + Empty, + ), + + // --- Page --- + "Page.enable": cmd(Type.Object({}), Empty), + "Page.bringToFront": cmd(Type.Object({}), Empty), + "Page.navigate": cmd( + Type.Object({ url: Type.String(), referrer: Type.Optional(Type.String()) }), + Type.Object({ + frameId: Type.String(), + loaderId: Type.Optional(Type.String()), + errorText: Type.Optional(Type.String()), + }), + ), + "Page.reload": cmd(Type.Object({ ignoreCache: Type.Optional(Type.Boolean()) }), Empty), + "Page.getNavigationHistory": cmd( + Type.Object({}), + Type.Object({ + currentIndex: Type.Number(), + entries: Type.Array( + Type.Object({ id: Type.Number(), url: Type.String(), title: Type.String() }), + ), + }), + ), + "Page.navigateToHistoryEntry": cmd(Type.Object({ entryId: Type.Number() }), Empty), + "Page.captureScreenshot": cmd( + Type.Object({ + format: Type.Optional(Type.String()), + quality: Type.Optional(Type.Number()), + clip: Type.Optional(Type.Unknown()), + captureBeyondViewport: Type.Optional(Type.Boolean()), + }), + Type.Object({ data: Type.String() }), + ), + "Page.printToPDF": cmd(Type.Object({}, { additionalProperties: true }), Type.Object({ data: Type.String() })), + "Page.handleJavaScriptDialog": cmd( + Type.Object({ accept: Type.Boolean(), promptText: Type.Optional(Type.String()) }), + Empty, + ), + + // --- Runtime --- + "Runtime.evaluate": cmd( + Type.Object( + { expression: Type.String(), returnByValue: Type.Optional(Type.Boolean()), awaitPromise: Type.Optional(Type.Boolean()) }, + { additionalProperties: true }, + ), + Type.Object({ result: RemoteObject, exceptionDetails: Type.Optional(ExceptionDetails) }), + ), + "Runtime.callFunctionOn": cmd( + Type.Object( + { functionDeclaration: Type.String(), objectId: Type.Optional(Type.String()) }, + { additionalProperties: true }, + ), + Type.Object({ result: RemoteObject, exceptionDetails: Type.Optional(ExceptionDetails) }), + ), + "Runtime.releaseObject": cmd(Type.Object({ objectId: Type.String() }), Empty), + + // --- DOM --- + "DOM.getDocument": cmd( + Type.Object({ depth: Type.Optional(Type.Number()), pierce: Type.Optional(Type.Boolean()) }), + Type.Object({ root: Type.Object({ nodeId: Type.Number() }, { additionalProperties: true }) }), + ), + "DOM.querySelector": cmd( + Type.Object({ nodeId: Type.Number(), selector: Type.String() }), + Type.Object({ nodeId: Type.Number() }), + ), + "DOM.getBoxModel": cmd( + Type.Object({ nodeId: Type.Optional(Type.Number()), backendNodeId: Type.Optional(Type.Number()) }), + Type.Object({ + model: Type.Object( + { content: Type.Array(Type.Number()), width: Type.Number(), height: Type.Number() }, + { additionalProperties: true }, + ), + }), + ), + "DOM.resolveNode": cmd( + Type.Object({ nodeId: Type.Optional(Type.Number()), backendNodeId: Type.Optional(Type.Number()) }), + Type.Object({ object: RemoteObject }), + ), + "DOM.setFileInputFiles": cmd( + Type.Object({ + files: Type.Array(Type.String()), + nodeId: Type.Optional(Type.Number()), + backendNodeId: Type.Optional(Type.Number()), + objectId: Type.Optional(Type.String()), + }), + Empty, + ), + + // --- Input --- + "Input.dispatchMouseEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.dispatchKeyEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.dispatchDragEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.insertText": cmd(Type.Object({ text: Type.String() }), Empty), + + // --- Network / Emulation / Accessibility --- + "Network.getResponseBody": cmd( + Type.Object({ requestId: Type.String() }), + Type.Object({ body: Type.String(), base64Encoded: Type.Optional(Type.Boolean()) }), + ), + "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()) }), + ), +}; + +export type CdpMethod = keyof typeof COMMANDS; +export type ParamsOf<M extends CdpMethod> = Static<(typeof COMMANDS)[M]["params"]>; +export type ResultOf<M extends CdpMethod> = Static<(typeof COMMANDS)[M]["result"]>; + +export const decodeResult = <M extends CdpMethod>( + method: M, + raw: unknown, +): Result<ResultOf<M>, CdpError> => { + const spec = COMMANDS[method]; + if (spec === undefined) { + return err(cdpError("invalid_response", `unknown CDP method`, method)); + } + if (!spec.validate.Check(raw)) { + const why = spec.validate.Errors(raw).map((e) => e.message).join("; "); + return err(cdpError("invalid_response", why, method)); + } + // Guarded assertion — see Verified Fact 4. The Check above proves `raw` + // matches COMMANDS[method].result; TS cannot correlate a generic key to its + // mapped value, so this carries what the check established. The only + // assertion in the CDP layer; do not add another. + return ok(raw as ResultOf<M>); +}; |
