/**
 * Crewly in Chrome — Browser Bridge Service
 *
 * Provides a raw WebSocket server that the Crewly in Chrome extension connects to.
 * Acts as a bridge between REST API calls (from remote-browser skill) and the
 * Chrome Extension. When a REST endpoint receives a command, it forwards it to
 * the connected Chrome Extension via WebSocket and waits for the response.
 *
 * Architecture:
 *   remote-browser skill ─HTTP→ /api/browser/* ─WS→ Chrome Extension
 *   Chrome Extension ─WS→ BrowserBridgeService ─HTTP response→ remote-browser skill
 *
 * @module services/browser/browser-bridge.service
 */
import { WebSocket } from 'ws';
import type { Server as HttpServer } from 'http';
/** Represents a connected Chrome Extension client */
export interface BrowserClient {
    /** Unique client identifier */
    id: string;
    /** WebSocket connection */
    ws: WebSocket;
    /** Timestamp when connected */
    connectedAt: Date;
    /** User-agent header from the connection */
    userAgent?: string;
}
/** Command sent to Chrome Extension via WebSocket */
export interface BrowserCommand {
    /** Unique command ID for request-response correlation */
    id: string;
    /** Tool name the Chrome Extension should execute */
    tool: string;
    /** Parameters for the tool */
    params?: Record<string, unknown>;
    /** Name of the agent sending this command (displayed in extension banner) */
    agentName?: string;
}
/** Response from Chrome Extension */
export interface BrowserCommandResponse {
    /** Command ID matching the request */
    id: string;
    /** Whether the command succeeded */
    success: boolean;
    /** Result data on success */
    result?: unknown;
    /** Error message on failure */
    error?: string;
}
/** Status information about the Crewly in Chrome bridge */
export interface BrowserBridgeStatus {
    /** Whether a Chrome Extension is reachable (direct WS or Cloud relay) */
    connected: boolean;
    /** Number of directly connected Chrome Extension clients via WebSocket */
    clientCount: number;
    /** WebSocket server path */
    wsPath: string;
    /** Whether the Cloud relay path to an Extension is available */
    relayAvailable?: boolean;
    /** Cloud device ID of the relay-connected Extension (null if not discovered) */
    relayDeviceId?: string | null;
    /** Number of currently active agent→tab bindings (per-tab dispatch). */
    bindingCount?: number;
}
/**
 * Per-tab dispatch (1 agent : 1 tab) — see
 * `.crewly/specs/crewly-in-chrome-per-tab-fix-2026-04-25.md`.
 *
 * Each binding records which agentSession owns which Extension tab, plus
 * a recency stamp used by the TTL sweep to evict idle bindings.
 */
export interface AgentTabBinding {
    /** Owning agent session (`X-Agent-Session` header value). */
    agentSession: string;
    /** Chrome tab id this agent has been bound to. */
    tabId: number;
    /** Window id the tab lives in (tracked for cross-window heuristics). */
    windowId?: number;
    /** Wall-clock time when the binding was first established. */
    boundAt: Date;
    /** Wall-clock time of the last command that touched this binding. Reset on activity. */
    lastActivityAt: Date;
}
/**
 * Tab descriptor reported by the Extension during the post-reconnect
 * inventory handshake (§4.4 Reconnect Reconciliation).
 */
export interface ExtensionTabDescriptor {
    tabId: number;
    url?: string;
    title?: string;
    windowId?: number;
    /** True when the tab is in Crewly's tab group — only these are eligible for auto-close. */
    crewlyOwned?: boolean;
}
/**
 * Reasons a tab binding can disappear. Surfaced in logs/metrics so we
 * can distinguish user-driven closes from sweep-driven evictions.
 */
export type AgentTabBindingRemovalReason = 'unbind' | 'tab_removed' | 'ttl_expired' | 'reconcile_extension_missing' | 'shutdown';
/**
 * BrowserBridgeService manages WebSocket connections from the Crewly Chrome
 * Extension and provides a command-response bridge for the REST API layer.
 *
 * Singleton pattern ensures a single WebSocket server per backend instance.
 */
export declare class BrowserBridgeService {
    private static instance;
    private readonly logger;
    private wss;
    private clients;
    private pendingCommands;
    private commandCounter;
    /**
     * Per-agent tab bindings (per-tab dispatch §3.2). Keyed by `agentSession`
     * so each agent has at most one bound tab. Cleared on `unbindAgentTab`,
     * tab-removed events from the Extension, TTL expiry, or shutdown.
     */
    private agentTabBindings;
    /** Periodic timer that evicts idle bindings (§4.2). Only running while attached. */
    private sweepTimer;
    /** Last time we logged the soft-cap warning, used to throttle to once per sweep cycle. */
    private softWarnLoggedAt;
    private constructor();
    /**
     * Get the singleton instance.
     *
     * @returns BrowserBridgeService instance
     */
    static getInstance(): BrowserBridgeService;
    /**
     * Reset singleton instance (for testing).
     */
    static resetInstance(): void;
    /**
     * Attach the WebSocket server to an existing HTTP server.
     * Uses noServer mode to avoid conflicting with Socket.IO's upgrade handler.
     * When two WebSocketServer instances both use `server: httpServer`, they both
     * register 'upgrade' event handlers. The ws library sends a 400 Bad Request
     * for non-matching paths, which corrupts the Socket.IO WebSocket connection
     * (the raw "HTTP/1.1 400" bytes appear as an invalid frame with RSV1 set).
     *
     * @param httpServer - The HTTP server to attach to
     */
    attach(httpServer: HttpServer): void;
    /**
     * Handle incoming WebSocket message from a Chrome Extension client.
     * Messages are either heartbeat pings or command responses.
     *
     * @param clientId - ID of the client that sent the message
     * @param data - Raw WebSocket message data
     */
    private handleMessage;
    /**
     * Send a command to the Chrome Extension and wait for the response.
     * Commands are sent to the first connected client.
     *
     * @param tool - Tool name to execute (e.g., 'navigate', 'screenshot')
     * @param params - Tool parameters
     * @param timeoutMs - Command timeout in milliseconds
     * @param agentName - Optional agent name to display in the extension banner
     * @returns Response from the Chrome Extension
     * @throws Error if no client is connected or command times out
     */
    sendCommand(tool: string, params?: Record<string, unknown>, timeoutMs?: number, agentName?: string): Promise<BrowserCommandResponse>;
    /**
     * Look up the current binding for an agent session, if any. Read-only —
     * does NOT update `lastActivityAt`. Use `sendCommandForAgent` for the
     * activity-tracking lookup path.
     *
     * @param agentSession `X-Agent-Session` header value.
     * @returns The binding, or `undefined` if this agent has no bound tab.
     */
    getBinding(agentSession: string): AgentTabBinding | undefined;
    /**
     * Snapshot the current bindings as a plain array — used by `GET
     * /api/browser/bindings` and by tests.
     */
    listBindings(): AgentTabBinding[];
    /**
     * Bind an agent to a fresh Chrome tab via the Extension. Idempotent: if
     * the agent already has a binding, return it instead of creating a new
     * tab (avoids tab leak when skill cache and backend disagree under
     * concurrent calls).
     *
     * @param agentSession Owning agent session.
     * @param opts.foreground Pass `active: true` to the Extension on create.
     *   Defaults to `false` so the user's foreground tab is never disturbed.
     * @param opts.timeoutMs Optional override for the create-tab WS round-trip.
     * @returns The (possibly pre-existing) binding.
     * @throws `Error` with a `tab_pool_full` cause when at hard cap.
     */
    bindAgentTab(agentSession: string, opts?: {
        foreground?: boolean;
        timeoutMs?: number;
        agentName?: string;
    }): Promise<AgentTabBinding>;
    /**
     * Release an agent's bound tab. Optionally instruct the Extension to
     * close the tab as well (default: true). Safe to call when no binding
     * exists — returns `{ released: false, tabClosed: false }` instead of
     * throwing.
     */
    unbindAgentTab(agentSession: string, opts?: {
        closeTab?: boolean;
        reason?: AgentTabBindingRemovalReason;
        timeoutMs?: number;
    }): Promise<{
        released: boolean;
        tabClosed: boolean;
    }>;
    /**
     * Send a command on behalf of an agent. Resolution priority (matches §4.2):
     *
     *   1. Explicit `tabId` in `params` — used as-is, NO ownership check
     *      (legacy testing/debug only; controller layer is responsible for
     *      enforcing cross-agent ownership rules before reaching here).
     *   2. `agentSession` provided AND has a binding — inject bound tabId,
     *      bump `lastActivityAt`.
     *   3. `agentSession` provided AND no binding — auto-bind first, then
     *      forward the original command with the new tabId.
     *   4. No `agentSession` — pass through to plain `sendCommand` (active-tab
     *      fallback in Extension, preserving backward compat).
     *
     * @param agentSession Owning agent session, or `undefined` for legacy fallback.
     * @param tool Tool name (e.g. `navigate`, `read-text`).
     * @param params Tool params; if it contains `tabId` we treat as explicit override.
     * @param timeoutMs Optional command timeout.
     * @param agentName Optional agent name for the Extension banner.
     */
    sendCommandForAgent(agentSession: string | undefined, tool: string, params?: Record<string, unknown>, timeoutMs?: number, agentName?: string): Promise<BrowserCommandResponse>;
    /**
     * Handle a `tabRemoved` event from the Extension (§4.3 — fires when
     * the user manually closes a Crewly tab or when Chrome reaps it).
     * Idempotent: tabs not in any binding are ignored.
     */
    handleTabRemoved(tabId: number): void;
    /**
     * Reconcile bindings against the Extension's current tab inventory
     * (§4.4). Called whenever a fresh WS connection completes the post-
     * reconnect handshake.
     *
     * - For each backend binding whose tabId no longer exists in the
     *   Extension → drop the binding (`reconcile_extension_missing`).
     * - For each Extension tab that is Crewly-owned but unknown to any
     *   binding → ask the Extension to close it (orphan cleanup).
     *
     * Caller is responsible for actually sending the close commands; this
     * method returns the orphan list rather than firing `unbindTab` itself
     * to keep the function pure-ish and easier to test.
     *
     * @returns Orphan tabs the caller should close.
     */
    handleTabInventory(extensionTabs: ExtensionTabDescriptor[]): {
        orphans: number[];
    };
    /** Effective hard cap, allowing env override `CREWLY_TAB_BIND_MAX`. */
    private getTabPoolCap;
    /** Effective TTL in ms, allowing env override `CREWLY_TAB_BIND_TTL_MINUTES`. */
    private getTabBindingTtlMs;
    /**
     * Soft warning ping when bindings cross the warn threshold. Throttled
     * to once per sweep cycle so we don't flood the logs during a burst.
     */
    private maybeWarnSoftCap;
    /**
     * Run the TTL sweep once. Idle bindings (`lastActivityAt + TTL < now`)
     * are removed. Sends `unbindTab` to the Extension when a client is
     * connected, but never fails the sweep on Extension errors.
     *
     * Exported via `runSweepOnce` for tests so we don't have to advance
     * fake timers across the full 5-minute interval.
     *
     * @returns Number of bindings evicted in this pass.
     */
    runSweepOnce(): Promise<number>;
    private startSweepTimer;
    private stopSweepTimer;
    /**
     * Get the current bridge status, including relay availability.
     *
     * @returns Status information with direct WS and relay details
     */
    getStatus(): BrowserBridgeStatus;
    /**
     * Best-effort getter for the BrowserProxyService singleton, hidden behind
     * a try/require so importing it eagerly never deadlocks BrowserBridge's
     * boot path. Returns null if the proxy module isn't present (test
     * fixtures) or hasn't been instantiated yet.
     */
    private tryGetProxyInstance;
    /**
     * Check if a Chrome Extension is reachable (direct WS or Cloud relay).
     *
     * @returns True if connected via direct WS or relay is available
     */
    isConnected(): boolean;
    /**
     * Get the first active (OPEN) client, cleaning up stale ones.
     *
     * @returns Active BrowserClient or null
     */
    private getActiveClient;
    /**
     * Shut down the WebSocket server and disconnect all clients.
     */
    stop(): void;
}
//# sourceMappingURL=browser-bridge.service.d.ts.map