Shared Types
The shared/ package defines the TypeScript types that form the contract between CLI, daemon, and extension. All three components import from this package. No runtime code — types only (plus a few const enums/objects that inline at compile time).
Decisions that constrain this: ADR-005 (TypeScript).
Project Layout
Section titled “Project Layout”shared/├── package.json # name: "@bproxy/shared", no runtime deps├── tsconfig.json└── src/ ├── index.ts # re-exports ├── protocol.ts # request/response envelope ├── actions.ts # action names, per-action params and results ├── handles.ts # daemon-owned element handle aliases at the CLI/daemon boundary ├── errors.ts # error codes, categories, structured error shape └── sessions.ts # session/tab identifiers, pacing configProtocol Envelope
Section titled “Protocol Envelope”import type { Action, ActionParams, ActionResult } from './actions';import type { BproxyError } from './errors';import type { SessionId } from './sessions';
export interface BproxyRequest<A extends Action = Action> { protocol_version: 1; id: string; action: A; params: ActionParams[A]; session: SessionId; deadline: number; // unix ms destructive: boolean;}
// Daemon → extension wire shape. The CLI's HTTP input is `BproxyRequest`;// the daemon owns the mapping session → tabId and wraps the request with// `target.tabId` at the dispatch site. Only forwarded actions use this shape —// daemon-local actions (session.*, debug.last, debug.status) never carry a target.//// `target.tabId` may be `null` for background-handled actions that do not// require an existing tab (tab.open, tab.list, tab.close).export type BproxyForwardedRequest<A extends Action = Action> = BproxyRequest<A> & { target: { tabId: number | null };};
export interface BproxySuccessResponse<A extends Action = Action> { protocol_version: 1; id: string; ok: true; data: ActionResult[A]; page: PageState; replay: boolean;}
export interface BproxyErrorResponse { protocol_version: 1; id: string; ok: false; error: BproxyError;}
export type BproxyResponse<A extends Action = Action> = | BproxySuccessResponse<A> | BproxyErrorResponse;
export interface PageState { url: string; title: string; state: 'loading' | 'ready' | 'error'; busy: boolean;}Actions — Discriminated Union
Section titled “Actions — Discriminated Union”export type Action = | 'navigate' | 'text' | 'links' | 'images' | 'elements' | 'outline' | 'dom' | 'inspect' | 'snapshot' | 'scroll' | 'click' | 'hover' | 'screenshot' | 'fill' | 'fill-form' | 'select' | 'wait' | 'require-human' | 'tab.list' | 'tab.pin' | 'tab.unpin' | 'tab.open' | 'tab.close' | 'session.create' | 'session.list' | 'session.bind' | 'session.unbind' | 'session.resume' | 'session.close' | 'debug.log' | 'debug.last' | 'debug.status';
// Types for fill methods and worldexport type FillMethod = 'direct' | 'paste' | 'runtime-api';export type ExecutionWorld = 'isolated' | 'main';
// Shadow-DOM route representation (ADR-014)export interface ElementRoute { hosts: Array<{ selector: string; index?: number }>; // shadow host chain from document target: string; // selector within deepest shadow root}
// Target must be exactly one strategy: light-DOM selector or shadow routeexport type ElementTarget = | { selector: string; route?: never } | { selector?: never; route: ElementRoute };
// Short-lived daemon-owned alias, accepted only at the CLI/daemon boundary.export type ElementHandle = string & { readonly __brand: 'ElementHandle' };export interface ElementHandleRef { handle: ElementHandle }export type ClientElementTarget = ElementTarget | ElementHandleRef;
// Params per action — exhaustive, compiler-checkedexport interface ActionParams { navigate: { url: string }; text: { selector?: string }; links: { selector?: string; visibleOnly?: boolean; limit?: number }; images: { selector?: string }; elements: { form?: boolean }; outline: Record<string, never>; dom: { selector?: string; depth?: number }; inspect: { selector: string; properties?: string[]; limit?: number }; snapshot: { selector?: string; maxDepth?: number; interactiveOnly?: boolean }; scroll: { target?: ClientElementTarget; by?: string; direction?: 'up' | 'down' }; click: { target: ClientElementTarget }; hover: { target: ClientElementTarget }; screenshot: { activate?: boolean; debugger?: boolean }; fill: { target: ClientElementTarget; value: string; method: FillMethod; // NOT optional — agent must choose world: ExecutionWorld; // NOT optional — 'isolated' or 'main' }; 'fill-form': { fields: Array<{ target: ClientElementTarget; value: string; method: FillMethod; world: ExecutionWorld; }> }; select: { trigger: ClientElementTarget; optionText: string }; wait: { strategy: 'selector' | 'url' | 'navigation'; target: string; timeout?: number }; 'require-human': { reason: string; forAttach?: string }; 'tab.list': Record<string, never>; 'tab.pin': { tab?: TabHandle }; 'tab.unpin': { tab?: TabHandle }; 'tab.open': { url: string }; 'tab.close': { tab?: TabHandle }; 'session.create': { label?: string }; 'session.list': Record<string, never>; 'session.bind': { tab: TabHandle; pacing?: PacingMode }; 'session.unbind': Record<string, never>; 'session.resume': Record<string, never>; 'session.close': Record<string, never>; 'debug.log': { id?: string; limit?: number }; 'debug.last': { count?: number }; 'debug.status': Record<string, never>;}
// Results per action — what data contains on successexport interface ActionResult { navigate: { url: string; title: string; loadTime: number }; text: { text: string }; links: { links: Array<LinkInfo> }; images: { images: Array<{ src: string; alt: string; width: number; height: number }> }; elements: { elements: Array<ElementInfo> }; outline: { landmarks: Array<Landmark>; headings: Array<Heading> }; dom: { html: string }; inspect: { elements: Array<InspectElement>; total: number }; snapshot: { tree: string; nodeCount: number }; scroll: { target: 'viewport' | 'element'; before: number; after: number; scrolledPx: number; moved: boolean; stable: boolean; scrollHeight?: number; clientHeight?: number; }; click: { clicked: true; disappeared: boolean; stable: boolean }; hover: { hovered: true; stable: boolean; elapsed: number }; screenshot: { base64: string; format: 'png' | 'jpeg' }; fill: { filled: boolean; verifiedValue: string }; 'fill-form': { results: Array<{ target: ElementTarget; filled: boolean; verifiedValue: string }> }; select: { selected: boolean; optionText: string }; wait: { matched: boolean; elapsed: number }; 'require-human': { resumed: boolean }; 'tab.list': { session: SessionId; tabs: Array<TabInfo> }; 'tab.pin': { tab: TabHandle; pinned: true }; 'tab.unpin': { tab: TabHandle; pinned: false }; 'tab.open': { session: SessionId; tab: TabHandle; bound: boolean; url: string }; 'tab.close': { tab: TabHandle; closed: true }; 'session.create': { session: SessionId; label?: string }; 'session.list': { sessions: Array<SessionInfo> }; 'session.bind': { session: SessionId; tab: TabHandle }; 'session.unbind': Record<string, never>; 'session.resume': { session: SessionId }; 'session.close': { session: SessionId; closedTabs: number }; 'debug.log': { entries: Array<TraceEntry> }; 'debug.last': { requests: Array<DaemonRequestTrace> }; 'debug.status': { daemon: { pid: number; port: number; uptimeSec: number; version: string; protocolVersion: number }; wsClients: Array<{ id: string; connectedAt: number; protocolVersion: number }>; sessions: Array<SessionInfo>; sessionTabs: Array<{ session: SessionId; tabs: Array<TabInfo> }>; pausedSessions: Array<{ session: SessionId; reason?: string }>; };}Adding a new action requires updating Action, ActionParams, and ActionResult. The compiler forces all consumers (CLI command, daemon dispatch, extension handler) to handle it.
Error Taxonomy
Section titled “Error Taxonomy”export type ErrorCode = // Transport | 'NO_EXTENSION' | 'TIMEOUT' | 'OVERLOADED' | 'WS_DISCONNECTED' // Target | 'TAB_NOT_FOUND' | 'ELEMENT_NOT_FOUND' | 'ELEMENT_NOT_ACTIONABLE' | 'SELECTOR_AMBIGUOUS' | 'INVALID_SESSION_ID' | 'SESSION_NOT_FOUND' | 'TAB_HANDLE_NOT_FOUND' | 'TAB_NOT_IN_SESSION' | 'ELEMENT_HANDLE_NOT_FOUND' | 'ELEMENT_HANDLE_STALE' | 'ELEMENT_HANDLE_SCOPE_MISMATCH' // Policy | 'HUMAN_REQUIRED' | 'DEBUGGER_DISABLED' | 'SESSION_REQUIRED' // Execution | 'SCRIPT_ERROR' | 'NAVIGATION_FAILED' | 'TAB_NOT_VISIBLE';
export type ErrorCategory = 'transport' | 'target' | 'policy' | 'execution';
export type RetryHint = 'safe' | 'conditional' | 'never';
export interface BproxyError { code: ErrorCode; category: ErrorCategory; retry: RetryHint; message: string; suggestedAction?: string; details?: Record<string, unknown>;}Session and Tab Identifiers
Section titled “Session and Tab Identifiers”// Branded types — prevent accidental string/number interchangedeclare const sessionIdBrand: unique symbol;declare const tabHandleBrand: unique symbol;
// 6-character base32 lowercase, e.g. "m4q7z2"export type SessionId = string & { readonly [sessionIdBrand]: 'SessionId' };
// Session-scoped logical handle, e.g. "t1", "t2"export type TabHandle = `t${number}` & { readonly [tabHandleBrand]: 'TabHandle' };
export type PacingMode = 'human' | 'fast';
export interface PacingConfig { navigate: { min: number; max: number }; scroll: { min: number; max: number }; interaction: { min: number; max: number }; fill: { min: number; max: number };}
export const PACING_PRESETS: Record<PacingMode, PacingConfig> = { human: { navigate: { min: 1500, max: 4000 }, scroll: { min: 4000, max: 8000 }, interaction: { min: 500, max: 2000 }, fill: { min: 500, max: 2000 }, }, fast: { navigate: { min: 300, max: 800 }, scroll: { min: 500, max: 1500 }, interaction: { min: 100, max: 400 }, fill: { min: 100, max: 400 }, },};
export interface SessionInfo { id: SessionId; label?: string; tab: TabHandle | null; // bound logical tab, or null if unbound pacing: PacingMode; paused: boolean; pauseReason?: string;}
export interface TabInfo { tab: TabHandle; // logical handle, never a raw Chrome id url: string; title: string; bound: boolean; // true if this is the session's active tab}Supporting Types
Section titled “Supporting Types”// Used in ActionResult. Composed from ElementTarget so an ElementInfo can be// passed directly anywhere an ElementTarget is expected.export type ElementInfo = ElementTarget & { tag: string; handle?: ElementHandle; type?: string; // input type label?: string; value?: string; placeholder?: string; required?: boolean; options?: string[]; // for select/dropdown role?: string; hasShadowRoot?: boolean; runtimeHandle?: 'quill' | 'lexical' | 'prosemirror' | 'codemirror' | 'monaco' | 'slate';};
export interface LinkInfo { text: string; href: string; target: ElementTarget; handle?: ElementHandle; title?: string; rel?: string; targetAttr?: string; visible?: boolean;}
export interface InspectElement { index: number; tag: string; id: string; classes: string; role: string; ariaLabel: string; rect: { x: number; y: number; width: number; height: number }; computed: Record<string, string>; children: number; descendants: number; textLength: number; scrollable: boolean; scrollInfo?: { scrollTop: number; scrollHeight: number; clientHeight: number }; selector: string;}
export interface Landmark { tag: string; role: string; label?: string;}
export interface Heading { level: number; text: string;}
// Extension-side ring buffer entry for debug.logexport interface TraceEntry { id: string; action: Action; tab: number; timestamp: number; elapsed: number; result: 'ok' | 'error'; errorCode?: ErrorCode; replay: boolean; extensionVersion: string;}
// Daemon-side ring buffer entry for debug.lastexport interface DaemonRequestTrace { id: string; action: Action; session: SessionId; receivedAt: number; elapsedMs: number; ok: boolean; errorCode?: ErrorCode; replayed?: boolean;}Package Configuration
Section titled “Package Configuration”{ "name": "@bproxy/shared", "version": "0.1.0", "type": "module", "exports": { ".": "./src/index.ts" }, "types": "./src/index.ts"}Consumers in the monorepo reference it as a workspace dependency. TypeScript resolves types directly from source — no build step needed for the shared package during development.