import { WebSocket } from 'ws'; import { type Capability, type CaptureHeaderDecl, type IndexedDbScopeDecl, type DomSelectorDecl, type GraphqlOpDeclaration, type StoragePointerDecl, type InnerFrame } from '@fetchproxy/protocol'; import { SessionState } from './session.js'; import type { Identity } from './identity.js'; import { type ExtensionTrustPort } from './extension-trust.js'; export interface PeerOpts { host: string; port: number; identity: Identity; mcpId: string; serverName: string; version: string; domains: string[]; /** * Inner-verb capabilities to declare on the peer's hello. Defaults * to `['fetch']` when omitted — keeps pre-capability callers compiling * and behaving identically on the wire. */ capabilities?: Capability[]; cookieKeys?: string[]; localStorageKeys?: string[]; sessionStorageKeys?: string[]; captureHeaders?: CaptureHeaderDecl[]; indexedDbScopes?: IndexedDbScopeDecl[]; localStoragePointers?: StoragePointerDecl[]; sessionStoragePointers?: StoragePointerDecl[]; domSelectors?: DomSelectorDecl[]; graphqlOps?: GraphqlOpDeclaration[]; /** * 1.12.0+ (#208): this MCP's pin on the extension's identity. Same store the * host path uses — a peer that becomes the host after an election must * recognise the same browser it recognised as a peer. */ extensionTrust: ExtensionTrustPort; /** * 1.12.0+ (#208): refuse to derive a session when the host forwards no * extension hello, instead of proceeding with a warning. * * A peer can only authenticate the extension from material the host relays, * and hosts before 1.12.0 relay none. Since whichever MCP wins the port * election is arbitrary, a strict default would break a mixed-version local * fleet at random — so the default warns. Deployments where the concentrator * is not a peer process on the same laptop should set this. * * @default false */ requireExtensionIdentity?: boolean; } /** * Public peer handle used by `FetchproxyServer` to send + receive * inner frames and to close the WebSocket. The bare WebSocket and the * session-key promise are NOT part of this surface — they live on * `InternalPeerHandle` below, which the peer's test suite reaches into * for handshake-level assertions but normal callers must not touch. */ export interface PeerHandle { sendInner: (inner: InnerFrame) => Promise; onInner: (cb: (inner: InnerFrame) => void) => void; /** * Subscribe to session renegotiation. Fires when a NEW ready frame * arrives for our mcpId after the first one — i.e. the extension * dropped (most commonly MV3 service-worker eviction) and reconnected, * causing the host to replay our hello and a fresh ephemeral keypair * to be derived on both ends. Any in-flight requests sent under the * old session key are now unreachable (the extension forgot them); * subscribers should reject their pending awaiters so callers fail * fast instead of hanging until the MCP-level timeout. The next * `sendInner` call will use the new session key automatically. */ onRenegotiate: (cb: () => void) => void; /** * 0.5.2+: fires when the host forwards a `pair-pending` frame for our * mcpId — the extension queued us for the user to approve in the * popup and we won't get a ready frame (or a working session) until * they do. Cleared when a ready frame arrives. */ onPendingPair: (cb: (pairCode: string) => void) => void; /** The most recent pair code received via pair-pending, or null if none. */ pendingPairCode: () => string | null; /** * 0.13.0+: fires when the WebSocket to the host closes — most importantly * when the host process dies, stranding this peer. The owning * `FetchproxyServer` uses this to tear down the dead peer handle and * re-elect (becoming the new host if the port is now free). Fires on ANY * close, including our own `close()`; the peer is intent-agnostic, so the * owner decides what a close means. */ onClose: (cb: () => void) => void; close: () => void; } /** * Internal-only extension of `PeerHandle`. Used by `peer.test.ts` to * verify the underlying WS handshake and (some day) by host-side code * that wants to assert on the derived session key. Not exported from * `@fetchproxy/server`'s public surface — anything that imports this * type is opting in to the internal contract. */ export interface InternalPeerHandle extends PeerHandle { ws: WebSocket; /** Resolves once the ready handshake has completed and sessionKey is derived. */ session: Promise; } export declare function startPeer(opts: PeerOpts): Promise;