@hydraharness/harness-client-connection 0.1.1-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Browser-trust fence for every /api request. Defends the two confused-deputy
3
+ * paths a browser opens against a local HTTP API — DNS rebinding (Host names
4
+ * the attacker's domain while the socket reaches this server) and cross-site
5
+ * requests fired from a malicious page. The Host fence binds every request,
6
+ * browser-looking or not: over plain HTTP a browser attaches neither Origin
7
+ * nor Fetch-Metadata to reads (images and navigations — those
8
+ * headers go only to trustworthy destinations), so an unmarked request may
9
+ * still be a rebound browser read and Host is the one header rebinding cannot
10
+ * forge. Non-browser and remote clients pass the same fence via loopback,
11
+ * deployment-derived LAN IP literals, or a declared `trustedHosts` authority.
12
+ * Network reachability and authentication stay out of scope: binding policy
13
+ * belongs to the webserver config, and this fence is not an auth layer.
14
+ */
15
+ import type { IncomingHttpHeaders } from 'node:http';
16
+ /** The request facts the fence reads from either HTTP representation. */
17
+ interface ApiTrustRequest {
18
+ headers: IncomingHttpHeaders | Headers;
19
+ }
20
+ /**
21
+ * Assert one configured `trustedHosts` entry is a bare authority (`host` or
22
+ * `host:port`) in canonical form: it must survive WHATWG parsing unchanged
23
+ * (case aside). Anything parsing would silently rewrite is refused as a typo
24
+ * that must fail the load loudly instead of being ignored until requests 403
25
+ * or quietly changing the grant: URL parts beyond the authority
26
+ * (`harness.internal/path`, `user@harness.internal` — which would authorize
27
+ * the embedded hostname), stripped whitespace, a dangling colon or
28
+ * zero-padded port (which would broaden an intended exact-port grant to every
29
+ * port), and non-canonical host spellings (`0x7f.0.0.1`, percent-encoding,
30
+ * unbracketed IPv6; IDN hosts are declared in punycode, the form the wire
31
+ * carries).
32
+ * @param entry - the configured value, verbatim.
33
+ */
34
+ export declare function assertTrustedAuthority(entry: string): void;
35
+ /**
36
+ * Decide whether one /api request may reach the RPC bridge.
37
+ * @param request - Node HTTP or Fetch request facts (headers).
38
+ * @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
39
+ * @returns true when the Host is ours (loopback or trusted) and any attached browser markers are same-origin.
40
+ */
41
+ export declare function isTrustedApiRequest(request: ApiTrustRequest, trustedHosts: readonly string[]): boolean;
42
+ export {};
43
+ //# sourceMappingURL=api-request-trust.d.ts.map
@@ -0,0 +1,20 @@
1
+ export type { ConversationRevision, SessionVersionState, ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ResponseValue, WorkspaceApi, WorkspaceId, WorkspaceView, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, WorkspaceSettingsStatus, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, AuthorizationAccountView, AuthorizationApi, AuthorizationAttemptView, AuthorizationEntryView, AuthorizationNoticeView, AuthorizationPromptView, AuthorizationUsageView, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, JobView, } from '@hydraharness/harness-host-apiproxy/api';
2
+ export type { ToolCallView, ToolResultView } from '@hydraharness/harness-tools/presentation';
3
+ export type { RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, } from '@hydraharness/harness-host-apiproxy/api';
4
+ export { RpcId, SESSION_SEARCH_RESULT_LIMIT, transportError, } from '@hydraharness/harness-host-apiproxy/api';
5
+ export { AbstractApiClient } from '@hydraharness/harness-host-apiproxy/client';
6
+ export type { IApiClient } from '@hydraharness/harness-host-apiproxy/client';
7
+ export type { SessionId, SessionEvent } from '@hydraharness/harness-session/types';
8
+ export type { MessageId } from '@hydraharness/harness-llm/brand';
9
+ export type { ContentBlock, StreamChunk } from '@hydraharness/harness-llm/types';
10
+ /** Successful value returned by the connection-generation host handshake. */
11
+ export type HostDescription = import('@hydraharness/harness-host-apiproxy/api').ResponseValue<'host.describe'>;
12
+ import type { RpcResponse, RpcResult } from '@hydraharness/harness-host-apiproxy/api';
13
+ /**
14
+ * Unwrap a unary response: RpcResponse<T> -> RpcResult<T> (business code only
15
+ * cares about the result slot).
16
+ * @param response - the unary response.
17
+ * @returns its result slot.
18
+ */
19
+ export declare function resultOf<T>(response: RpcResponse<T>): RpcResult<T>;
20
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,64 @@
1
+ import type { HostDescription, IApiClient, HostFrame, MuxFrame, RpcRequest } from './api.ts';
2
+ /** Reconnect/backoff tunables (deployment-varying — no hardcoded tunables; these become the
3
+ * future `ctx.connection` plugin's Config). All fields optional; defaults below. */
4
+ export interface ConnectionConfig {
5
+ /** First-retry backoff cap in ms (jittered: actual delay is cap/2..cap). */
6
+ backoffBaseMs?: number;
7
+ /** Exponential growth factor per consecutive failed attempt. */
8
+ backoffFactor?: number;
9
+ /** Upper bound for the backoff cap in ms. */
10
+ backoffMaxMs?: number;
11
+ /** Cap on waiting for both streams' onOpen before onConnected, in ms. The strict handshake
12
+ * waits for mux+host stream establishment plus describe; a carrier that never
13
+ * fires onOpen (misbehaving proxy) must not wedge the connection forever — on timeout the
14
+ * generation proceeds as connected and the live-gap repair path covers stragglers. */
15
+ streamOpenTimeoutMs?: number;
16
+ }
17
+ /** Coarse connection state for the UI: 'connected' after each generation's handshake,
18
+ * 'reconnecting' the moment the generation fails (covers the whole backoff+retry span). */
19
+ export type ConnectionState = 'connected' | 'reconnecting';
20
+ /** Frame sink callbacks: the Controller owns the physical streams; business dispatch belongs to
21
+ * SessionManager. */
22
+ export interface ConnectionSinks {
23
+ onMuxEnvelope?: (envelope: RpcRequest<MuxFrame>) => void;
24
+ onHostEnvelope?: (envelope: RpcRequest<HostFrame>) => void;
25
+ /** After each connection generation is established (both streams open + describe succeeded), first connect included. */
26
+ onConnected?: (description: HostDescription) => void;
27
+ /** Coarse state transitions (deduplicated: fires only on change). The initial pre-connect
28
+ * span reports nothing — the UI treats "no state yet" as connecting, not as an outage. */
29
+ onStateChange?: (state: ConnectionState) => void;
30
+ }
31
+ /**
32
+ * Opens both streams and keeps iterating (pull mode: nothing reads the socket and the tap
33
+ * never fires unless someone for-awaits), reconnecting with exponential backoff on loss.
34
+ * State (generation/attempt) is instance-private, never in the store.
35
+ * The pump body feeds each frame to a sink (sink exceptions must
36
+ * not kill the pump — a broken business layer must not drag down the connection layer).
37
+ */
38
+ export declare class ConnectionController {
39
+ private readonly api;
40
+ private readonly sinks;
41
+ private generation;
42
+ private attempt;
43
+ private current;
44
+ private running;
45
+ private lastState;
46
+ private readonly config;
47
+ constructor(api: IApiClient, sinks?: ConnectionSinks, config?: ConnectionConfig);
48
+ /** Idempotent: begin the connect/pump/reconnect loop. */
49
+ start(): void;
50
+ /** Stop the loop and abort the current generation's streams. */
51
+ stop(): void;
52
+ private backoffDelay;
53
+ /** Read through a method: stop() flips the flag across awaits, so narrowing from the loop condition must not stick. */
54
+ private isRunning;
55
+ /** Re-read both mutable liveness guards after a potentially reentrant sink. */
56
+ private isGenerationActive;
57
+ private loop;
58
+ /** Deduplicated state emission (sink isolation applies). */
59
+ private emitState;
60
+ private pumpStream;
61
+ /** Sink exception isolation: a business-layer throw is logged only, never affecting pump or reconnect semantics. */
62
+ private callSink;
63
+ }
64
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1,68 @@
1
+ import type { SessionId } from '@hydraharness/harness-session/types';
2
+ import type { ApiProxy, ClientResponse, HostFrame, MuxFrame, RpcReceipt, RpcRequest, RpcResponse } from './api.ts';
3
+ import type { RequestPayload, ResponseValue, RpcMethodMap } from '@hydraharness/harness-host-apiproxy/api';
4
+ import { AbstractApiClient } from './api.ts';
5
+ import type { ClientConnectionRpc } from '../rpc.ts';
6
+ /** Deterministic fixture branches used by keyless Web assembly tests. */
7
+ export interface FixtureOptions {
8
+ /** Start with no real Workspace or Session. */
9
+ empty?: boolean;
10
+ /** Reject every prompt before appending its user event. */
11
+ rejectPrompt?: boolean;
12
+ /** Publish the Session but fail its Workspace account write. */
13
+ failWorkspaceAttach?: boolean;
14
+ /** Publish and frame the Session, then throw instead of returning create. */
15
+ dropSessionCreateResponse?: boolean;
16
+ /** Order of the two successful create frames. */
17
+ createFrameOrder?: 'session-first' | 'workspace-first';
18
+ }
19
+ /**
20
+ * In-memory fake host: fx-alpha carries history and replay scripts; fx-beta is fx-alpha's child session (lineage indent material).
21
+ * @param options - fixture branches for empty state and failure timing.
22
+ * @returns an ApiProxy backed entirely by in-memory state — no host process, no network.
23
+ */
24
+ export declare function createFixtureApi(options?: FixtureOptions): ApiProxy;
25
+ /** Both fixture faces over one state graph. */
26
+ export interface FixtureWorld {
27
+ /** Legacy unary/stream API the fixture still answers. */
28
+ readonly api: ApiProxy;
29
+ /** Generic Remote caller for the endpoints business services own. */
30
+ readonly rpc: ClientConnectionRpc;
31
+ }
32
+ /**
33
+ * Build both fixture faces so a caller can drive the Remote endpoints and the
34
+ * legacy API against one in-memory state graph.
35
+ * @param options - fixture branches for empty state and failure timing.
36
+ * @returns the legacy API face and the Remote RPC face.
37
+ */
38
+ export declare function createFixtureFaces(options?: FixtureOptions): FixtureWorld;
39
+ /**
40
+ * Fixture platform subclass: there is no HTTP at all, so instead of a doFetch transport it
41
+ * overrides the protocol-level virtuals (callUnary/openMux/openHost/respond) to dispatch
42
+ * straight into the in-memory ApiProxy — while still minting rpcIds, fabricating the four
43
+ * named full forms, and feeding the same tap as a real carrier. TODO: delete when the fixture
44
+ * moves to the isomorphic pipeline (InProcessApiClient over toFetchHandler(fixtureImpl)).
45
+ */
46
+ export declare class FixtureApiClient extends AbstractApiClient {
47
+ private readonly api;
48
+ /** Generic Remote caller backed by the same in-memory state as the legacy fixture API. */
49
+ readonly rpc: ClientConnectionRpc;
50
+ constructor();
51
+ protected doFetch(): Promise<Response>;
52
+ protected callUnary<K extends keyof RpcMethodMap>(method: K, payload: RequestPayload<K>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<K>>>;
53
+ /** Method-key dispatch into the in-memory contract impl (a real carrier routes by URL path instead). */
54
+ private dispatch;
55
+ protected openMux(payload: {
56
+ since?: Record<SessionId, number>;
57
+ }, signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<MuxFrame>>;
58
+ protected openHost(payload: Record<never, never>, signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<HostFrame>>;
59
+ private tapStream;
60
+ /**
61
+ * Deliver a client response to the in-memory contract impl (no HTTP POST),
62
+ * echoing the envelope to the observation tap like every other path.
63
+ * @param message - the client-response envelope answering a server request.
64
+ * @returns the carrier receipt from the fixture impl.
65
+ */
66
+ respond(message: ClientResponse): Promise<RpcReceipt>;
67
+ }
68
+ //# sourceMappingURL=fixture.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Browser wire client. The plugin selects fixture or HTTP transport, provides
3
+ * the shared API client, and lets the runtime object layer start the stream
4
+ * controller with its sinks.
5
+ */
6
+ import type { Context } from '@hydraharness/cordis';
7
+ import type { HostDescription, IApiClient } from './api.ts';
8
+ import { type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts';
9
+ import { type RpcFetch } from './rpc.ts';
10
+ import type { ClientConnectionRpc } from '../rpc.ts';
11
+ export type { ConversationRevision, SessionVersionState, ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, JobView, RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, WorkspaceSettingsStatus, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, AuthorizationAccountView, AuthorizationApi, AuthorizationAttemptView, AuthorizationEntryView, AuthorizationNoticeView, AuthorizationPromptView, AuthorizationUsageView, } from './api.ts';
12
+ export { RpcId, AbstractApiClient, transportError, } from './api.ts';
13
+ export type { ConnectionConfig, ConnectionSinks, ConnectionState };
14
+ export type { ClientConnectionRpc } from '../rpc.ts';
15
+ export type { RpcFetch } from './rpc.ts';
16
+ /** Observable Host description published by each completed connection handshake. */
17
+ export interface HostDescriptionSource {
18
+ /** Latest connected-generation description; absent before connect and while reconnecting. */
19
+ getSnapshot(): HostDescription | undefined;
20
+ /** Subscribe to description replacement and connection loss. */
21
+ subscribe(listener: () => void): () => void;
22
+ }
23
+ /** Required services (none — this is the wire root). */
24
+ export declare const inject: string[];
25
+ /**
26
+ * Carrier override installed on the page global before plugin boot. The served
27
+ * web app leaves it unset and gets HTTP + WebSocket; a shell that owns a
28
+ * different physical transport (the worker preview's postMessage tunnel)
29
+ * provides both halves here instead of forking this plugin.
30
+ */
31
+ export interface ClientTransportHooks {
32
+ /** Build the API carrier: unary calls plus the two downstream event streams. */
33
+ createApiClient(): IApiClient;
34
+ /** Transport for generic unary RPC channels (the Typert gateway). */
35
+ fetch: RpcFetch;
36
+ /**
37
+ * Bundle transport for the module system, present when the carrier also owns
38
+ * bundle bytes (the worker tunnel). Absent in the served web app, whose
39
+ * bundles load over HTTP.
40
+ */
41
+ loadBundle?(url: string): Promise<void>;
42
+ }
43
+ /**
44
+ * The ctx.connection service API: the API client plus a one-shot
45
+ * controller starter (the runtime plugin supplies sinks when its object layer
46
+ * is ready — connection stays consumer-agnostic).
47
+ */
48
+ export interface ConnectionHandle {
49
+ /** Shared api client (fixture or real, decided at boot from the page URL). */
50
+ readonly api: IApiClient;
51
+ /** Whether the current page authority is loopback; non-browser contexts default to true. */
52
+ readonly isLoopback: boolean;
53
+ /** Generation-scoped Host facts, including the account home and native path-open capability. */
54
+ readonly hostDescription: HostDescriptionSource;
55
+ /** Generic logical RPC channels over the same Connection transport. */
56
+ readonly rpc: ClientConnectionRpc;
57
+ /**
58
+ * Start the connect/pump/reconnect loop with the consumer's frame sinks.
59
+ * One consumer owns the streams (the runtime object layer); a second call
60
+ * throws.
61
+ * @param sinks - frame/state callbacks.
62
+ * @param config - reconnect/backoff tunables.
63
+ * @returns stop handle for the loop.
64
+ */
65
+ start(sinks: ConnectionSinks, config?: ConnectionConfig): {
66
+ stop(): void;
67
+ };
68
+ }
69
+ /**
70
+ * Client plugin body: pick the api by page mode and provide ctx.connection.
71
+ * @param ctx - client cordis context.
72
+ */
73
+ export declare function apply(ctx: Context): void;
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Browser-safe UUID generation for client-side wire correlation. */
2
+ /**
3
+ * Generate an RFC 4122 version 4 UUID without requiring a secure context.
4
+ * @returns a UUID backed by `crypto.getRandomValues()`, which browsers expose on insecure origins.
5
+ */
6
+ export declare function randomUuid(): string;
7
+ //# sourceMappingURL=random-uuid.d.ts.map
@@ -0,0 +1,11 @@
1
+ /** Browser caller for generic Connection unary RPC channels. */
2
+ import type { ClientConnectionRpc } from '../rpc.ts';
3
+ /** Transport this caller posts through; same signature as the global `fetch`. */
4
+ export type RpcFetch = (input: URL, init: RequestInit) => Promise<Response>;
5
+ /**
6
+ * Create the browser-backed generic RPC caller.
7
+ * @param doFetch - transport override; defaults to the page's global fetch.
8
+ * @returns caller that owns request correlation and response-envelope validation.
9
+ */
10
+ export declare function createWebConnectionRpc(doFetch?: RpcFetch): ClientConnectionRpc;
11
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1,11 @@
1
+ /** Browser API carrier: HTTP upstream plus one WebSocket per downstream event stream. */
2
+ import type { ApiProxy, HostFrame, MuxFrame, RpcRequest } from './api.ts';
3
+ import { AbstractApiClient } from './api.ts';
4
+ /** Browser platform subclass: unary/respond use fetch; mux/host use downlink-only WebSockets. */
5
+ export declare class WebApiClient extends AbstractApiClient {
6
+ protected doFetch(input: URL, init?: RequestInit): Promise<Response>;
7
+ protected openMux(_payload: Parameters<ApiProxy['events']['mux']>[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<MuxFrame>>;
8
+ protected openHost(_payload: Parameters<ApiProxy['events']['host']>[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<HostFrame>>;
9
+ private readWebSocket;
10
+ }
11
+ //# sourceMappingURL=web-api-client.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * node:http ↔ WHATWG fetch bridge for the /api transport (host side of the
3
+ * web carrier; the fetch-shaped handler itself is transport-agnostic).
4
+ */
5
+ import type { IncomingMessage, ServerResponse } from 'node:http';
6
+ /** Default carrier cap for all HTTP RPC bodies: sized for the default
7
+ * aggregate image limit (100 MiB) after base64 expansion plus envelope
8
+ * headroom (~134.3 MiB required), rounded up for slack. The bridge buffers
9
+ * each body in memory, so this cap is also the per-request resident bound. */
10
+ export declare const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
11
+ /** Transport-independent request handler consumed by the Host HTTP bridge. */
12
+ export interface FetchHandler {
13
+ /**
14
+ * Handle one standard Fetch request.
15
+ * @param request - request produced by the active transport bridge.
16
+ * @returns complete or streaming Fetch response.
17
+ */
18
+ fetch(request: Request): Promise<Response>;
19
+ }
20
+ /**
21
+ * Bridge one node:http request to the fetch-shaped handler (client close
22
+ * aborts; SSE bodies stream out chunk by chunk).
23
+ * @param req - incoming node:http request (fully read before dispatch).
24
+ * @param res - node:http response the bridge writes and owns to completion.
25
+ * @param apiHandler - fetch-shaped API carrier the request is dispatched to.
26
+ * @param maxRequestBodyBytes - maximum body bytes buffered before dispatch.
27
+ */
28
+ export declare function bridge(req: IncomingMessage, res: ServerResponse, apiHandler: FetchHandler, maxRequestBodyBytes?: number): Promise<void>;
29
+ //# sourceMappingURL=http-bridge.d.ts.map
@@ -0,0 +1,36 @@
1
+ /** Host HTTP bridge for browser-client RPC. */
2
+ import type { Context } from '@hydraharness/cordis';
3
+ import z from '@hydraharness/schemastery';
4
+ export type { ConnectionRpcAuthority, ConnectionRpcEndpointMatcher, ConnectionRpcHandler, ConnectionRpcHandlerOptions, HostConnectionHandle, HostConnectionRpc, } from './rpc.ts';
5
+ export { HostConnectionService } from './rpc-host.ts';
6
+ export { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_PATH } from './api-path.ts';
7
+ /** Stable Cordis plugin name. */
8
+ export declare const name = "client-connection";
9
+ /** Services required before providing Connection; API Proxy is an optional `/api` fallback. */
10
+ export declare const inject: string[];
11
+ /** Plugin config: the deployment's non-loopback serving authorities. */
12
+ export interface ConnectionConfig {
13
+ /**
14
+ * Authorities this deployment serves beyond loopback: exact `host:port`, or
15
+ * port-less `host` matching any port. The /api trust fence refuses any
16
+ * request whose Host is neither loopback nor listed here, so a
17
+ * non-loopback (`0.0.0.0`) deployment must declare the names it is reached
18
+ * by (the hydra CLI derives the machine's LAN IP literals itself). An entry
19
+ * that is not a bare, canonical authority fails the plugin load.
20
+ */
21
+ trustedHosts?: string[];
22
+ /** Maximum buffered JSON body for every `/api` request. */
23
+ maxRequestBodyBytes?: number;
24
+ }
25
+ export declare const Config: z<ConnectionConfig>;
26
+ /**
27
+ * Mounts the API gateway under the browser transport prefix. Every request on
28
+ * the prefix passes the browser-trust fence first (DNS-rebinding and
29
+ * cross-site defense — [api-request-trust](./api-request-trust.ts));
30
+ * privileged methods additionally pass it with an empty trust list, which
31
+ * pins them to loopback.
32
+ * @param ctx - Host plugin context.
33
+ * @param config - resolved plugin config (schema defaults applied).
34
+ */
35
+ export declare function apply(ctx: Context, config?: ConnectionConfig): void;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-client-connection`.
3
+ * @module @hydraharness/harness-client-connection/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "client-connection-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser-safe, zero-dependency loopback classification shared by the `/api`
3
+ * Host fence and the package's `ctx.connection` state. The predicate stays
4
+ * package-internal; client plugins consume the derived state through Cordis.
5
+ */
6
+ /**
7
+ * Whether a normalized URL hostname names the local loopback authority.
8
+ * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
9
+ * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
10
+ */
11
+ export declare function isLoopbackHostname(hostname: string): boolean;
12
+ //# sourceMappingURL=loopback-hostname.d.ts.map
@@ -0,0 +1,34 @@
1
+ /** Host registry and HTTP adapter for generic Connection RPC channels. */
2
+ import { Context, Service } from '@hydraharness/cordis';
3
+ import { type FetchHandler } from './http-bridge.ts';
4
+ import type { HostConnectionHandle, HostConnectionRpc } from './rpc.ts';
5
+ declare module '@hydraharness/cordis' {
6
+ interface Context {
7
+ /** Host Connection transport and RPC registrations. */
8
+ connection: HostConnectionHandle;
9
+ }
10
+ }
11
+ /** Host Connection service whose channel registrations belong to the caller fiber. */
12
+ export declare class HostConnectionService extends Service implements HostConnectionHandle {
13
+ private readonly trustedHosts;
14
+ private readonly interceptors;
15
+ /**
16
+ * Provide the Host half over the active HTTP server.
17
+ * @param ctx - owning Connection plugin context.
18
+ * @param trustedHosts - deployment authorities accepted by trusted-host channels.
19
+ */
20
+ constructor(ctx: Context, trustedHosts: readonly string[]);
21
+ /** Generic channel registry scoped to the Context reading this service. */
22
+ get rpc(): HostConnectionRpc;
23
+ /**
24
+ * Compose one shared-channel Fetch handler from its interceptor and fallback.
25
+ * @param channel - shared channel mounted by Connection.
26
+ * @param privilegedEndpoints - endpoints pinned to loopback before target selection.
27
+ * @param fallback - handler for endpoints not claimed by the interceptor.
28
+ * @returns Fetch handler that selects exactly one target for each request.
29
+ */
30
+ createSharedFetchHandler(channel: '/api', privilegedEndpoints: ReadonlySet<string>, fallback: FetchHandler): FetchHandler;
31
+ private register;
32
+ private registerInterceptor;
33
+ }
34
+ //# sourceMappingURL=rpc-host.d.ts.map
@@ -0,0 +1,51 @@
1
+ /** Generic unary RPC contracts shared by the Host and Client Connection halves. */
2
+ import type { RpcResult } from '@hydraharness/harness-host-apiproxy/api';
3
+ /** Trust fence applied before a Host RPC channel reaches its handler. */
4
+ export type ConnectionRpcAuthority = 'trusted-host' | 'loopback';
5
+ /** Registration policy for one logical RPC channel. */
6
+ export interface ConnectionRpcHandlerOptions {
7
+ /** Browser authority accepted by every endpoint in this channel. */
8
+ readonly authority: ConnectionRpcAuthority;
9
+ }
10
+ /** Handler invoked after Connection has decoded the transport envelope. */
11
+ export type ConnectionRpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<RpcResult<unknown>>;
12
+ /** Synchronous ownership test for one endpoint on a shared RPC channel. */
13
+ export type ConnectionRpcEndpointMatcher = (endpoint: string) => boolean;
14
+ /** Host registry for logical RPC channels carried by the current transport. */
15
+ export interface HostConnectionRpc {
16
+ /**
17
+ * Register one absolute channel prefix and its trust policy.
18
+ * @param channel - absolute logical channel such as `/rpc`.
19
+ * @param handler - decoded endpoint handler returning the existing RPC result shape.
20
+ * @param options - channel trust policy.
21
+ * @returns asynchronous disposer removing the channel and its physical route.
22
+ */
23
+ handle(channel: string, handler: ConnectionRpcHandler, options: ConnectionRpcHandlerOptions): () => Promise<void>;
24
+ /**
25
+ * Intercept owned endpoints on the shared `/api` channel before its fallback.
26
+ * @param channel - reserved shared channel; currently `/api`.
27
+ * @param matches - synchronous endpoint ownership test.
28
+ * @param handler - decoded endpoint handler returning the existing RPC result shape.
29
+ * @param options - trust policy for every endpoint claimed by this interceptor.
30
+ * @returns asynchronous disposer removing the interceptor.
31
+ */
32
+ intercept(channel: '/api', matches: ConnectionRpcEndpointMatcher, handler: ConnectionRpcHandler, options: ConnectionRpcHandlerOptions): () => Promise<void>;
33
+ }
34
+ /** Host `ctx.connection` shape consumed by transport-independent adapters. */
35
+ export interface HostConnectionHandle {
36
+ /** Generic RPC channel registry. */
37
+ readonly rpc: HostConnectionRpc;
38
+ }
39
+ /** Client caller for logical RPC channels carried by the current transport. */
40
+ export interface ClientConnectionRpc {
41
+ /**
42
+ * Call one endpoint through an already registered logical channel.
43
+ * @param channel - absolute logical channel such as `/api`.
44
+ * @param endpoint - channel-relative endpoint such as `goals/create`.
45
+ * @param payload - channel-owned request payload.
46
+ * @param signal - optional caller cancellation.
47
+ * @returns the existing RPC success/error result; correlation stays inside Connection.
48
+ */
49
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>>;
50
+ }
51
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Host-side WebSocket carrier for the two server-to-browser event streams. */
2
+ import type { IncomingMessage } from 'node:http';
3
+ import type { Duplex } from 'node:stream';
4
+ import type { ApiProxy } from '@hydraharness/harness-host-apiproxy/api';
5
+ /**
6
+ * Owns WebSocket negotiation and frame pumping for the connection plugin's
7
+ * two downlinks. Client messages are a protocol violation: upstream traffic
8
+ * remains on HTTP.
9
+ */
10
+ export declare class WebSocketDownlinks {
11
+ private readonly api;
12
+ private readonly server;
13
+ private readonly pumps;
14
+ /** @param api - host API supplying the typed event streams. */
15
+ constructor(api: ApiProxy);
16
+ /**
17
+ * Upgrade one socket and pump the mux stream until either side closes.
18
+ * @param req - HTTP upgrade request.
19
+ * @param socket - Raw socket transferred by the HTTP server.
20
+ * @param head - Bytes already read after the upgrade headers.
21
+ */
22
+ handleMux(req: IncomingMessage, socket: Duplex, head: Buffer): void;
23
+ /**
24
+ * Upgrade one socket and pump the host stream until either side closes.
25
+ * @param req - HTTP upgrade request.
26
+ * @param socket - Raw socket transferred by the HTTP server.
27
+ * @param head - Bytes already read after the upgrade headers.
28
+ */
29
+ handleHost(req: IncomingMessage, socket: Duplex, head: Buffer): void;
30
+ /**
31
+ * Terminate owned sockets and await the no-server acceptor plus frame pumps.
32
+ * @returns A promise resolving after every socket and source iterator stops.
33
+ */
34
+ close(): Promise<void>;
35
+ private upgrade;
36
+ private pump;
37
+ }
38
+ /**
39
+ * Reject an untrusted upgrade before protocol negotiation.
40
+ * @param socket - Raw HTTP socket that remains owned by the caller.
41
+ */
42
+ export declare function rejectWebSocketUpgrade(socket: Duplex): void;
43
+ //# sourceMappingURL=websocket-downlink.d.ts.map
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@hydraharness/harness-client-connection",
3
+ "description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
4
+ "version": "0.1.1-rc.10",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
11
+ "directory": "packages/client/connection"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./client": {
26
+ "types": "./lib/types/client/index.d.ts",
27
+ "default": "./lib/client.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "hydra": {
33
+ "plugin": {
34
+ "application": "Send interface actions to the Host and receive live updates, including after a reconnect."
35
+ },
36
+ "client": {
37
+ "inject": [],
38
+ "platform": "web",
39
+ "immediately": true
40
+ }
41
+ },
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "ws": "^8.21.0",
45
+ "@hydraharness/schemastery": "^3.18.2"
46
+ },
47
+ "files": [
48
+ "lib/index.js",
49
+ "lib/invariant.js",
50
+ "lib/client.js",
51
+ "lib/types/**/*.d.ts"
52
+ ],
53
+ "peerDependencies": {
54
+ "@hydraharness/harness-host-webserver": "^0.1.1-rc.10",
55
+ "@hydraharness/harness-invariants": "^0.1.1-rc.10",
56
+ "@hydraharness/cordis": "^4.0.2",
57
+ "@hydraharness/harness-attachment": "^0.1.1-rc.10",
58
+ "@hydraharness/harness-host-apiproxy": "^0.1.1-rc.10",
59
+ "@hydraharness/harness-commands": "^0.1.1-rc.10",
60
+ "@hydraharness/harness-session": "^0.1.1-rc.10",
61
+ "@hydraharness/harness-tools": "^0.1.1-rc.10",
62
+ "@hydraharness/harness-llm": "^0.1.1-rc.10"
63
+ },
64
+ "devDependencies": {
65
+ "@types/ws": "^8.18.1",
66
+ "@hydraharness/harness-host-webserver": "^0.1.1-rc.10",
67
+ "@hydraharness/harness-invariants": "^0.1.1-rc.10",
68
+ "@hydraharness/cordis": "^4.0.2",
69
+ "@hydraharness/harness-host-apiproxy": "^0.1.1-rc.10",
70
+ "@hydraharness/harness-attachment": "^0.1.1-rc.10",
71
+ "@hydraharness/harness-commands": "^0.1.1-rc.10",
72
+ "@hydraharness/harness-llm": "^0.1.1-rc.10",
73
+ "@hydraharness/harness-session": "^0.1.1-rc.10",
74
+ "@hydraharness/harness-tools": "^0.1.1-rc.10"
75
+ }
76
+ }