# OpenSpec Change: enterprise-network - **Change ID**: enterprise-network - **Package**: `@suaveplan/network` - **Category**: universal - **Target path**: `universal/network/network` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/network.md` --- ## Summary Deliver `@suaveplan/network` on the genesis branch from scratch at enterprise quality. The package provides core networking utilities for universal TypeScript: a typed `HttpClient` with middleware pipeline, `WebSocketClient` with auto-reconnect and backpressure, URL construction/parsing helpers, fetch timeout and cancellation wrappers, request/response interceptors, and network-quality probing. Injectable `fetch` and `WebSocket` constructors for test-time substitution. No hard browser/Node platform deps. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The main-branch package had 85.40% coverage. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `NetworkError`; `NETWORK.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` | | `@suaveplan/retry` (`workspace:^`) | Exponential backoff + circuit breaker on HTTP/WS paths | | `@suaveplan/serialize` (`workspace:^`) | JSON request/response body serialisation | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters on every request | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API ### HttpClient ```ts class NetworkError extends SuaveplanError type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' interface HttpRequest { url: string method?: HttpMethod // default: 'GET' headers?: Record body?: unknown // serialised via @suaveplan/serialize timeoutMs?: number // default: 30000 signal?: AbortSignal } interface HttpResponse { status: number statusText: string headers: Record body: T url: string durationMs: number } type HttpMiddleware = (request: HttpRequest, next: (req: HttpRequest) => Promise) => Promise interface HttpClientOptions { baseUrl?: string defaultHeaders?: Record defaultTimeoutMs?: number middleware?: HttpMiddleware[] fetch?: typeof globalThis.fetch logger?: LoggerLike clock?: Clock } class HttpClient { constructor(options?: HttpClientOptions) request(req: HttpRequest): Promise> get(path: string, options?: Omit): Promise> post(path: string, body: unknown, options?: Omit): Promise> put(path: string, body: unknown, options?: Omit): Promise> patch(path: string, body: unknown, options?: Omit): Promise> delete(path: string, options?: Omit): Promise> use(middleware: HttpMiddleware): this } function createHttpClient(options?: HttpClientOptions): HttpClient ``` ### WebSocketClient ```ts type WsReadyState = 'connecting' | 'open' | 'closing' | 'closed' type WsMessage = string | ArrayBuffer | Uint8Array interface WebSocketClientOptions { url: string protocols?: string[] reconnect?: boolean // default: true reconnectDelayMs?: number // base delay; uses @suaveplan/retry exponential backoff maxReconnectAttempts?: number // default: 10 pingIntervalMs?: number // default: 30000; 0 = disabled timeoutMs?: number // connect timeout; default: 10000 WebSocket?: typeof WebSocket // injectable; defaults to globalThis.WebSocket logger?: LoggerLike clock?: Clock } class WebSocketClient { constructor(options: WebSocketClientOptions) connect(signal?: AbortSignal): Promise disconnect(): void send(message: WsMessage): void on(event: 'message', listener: (msg: WsMessage) => void): () => void on(event: 'open' | 'close' | 'error', listener: () => void): () => void get readyState(): WsReadyState get reconnectCount(): number } function createWebSocketClient(options: WebSocketClientOptions): WebSocketClient ``` ### URL utilities ```ts function buildUrl(base: string, path?: string, params?: Record): string function parseQueryString(qs: string): Record function serializeQueryString(params: Record): string function isAbsoluteUrl(url: string): boolean function joinPaths(...segments: string[]): string ``` ### Error codes ```ts type NetworkErrorCode = | 'NETWORK_REQUEST_FAILED' | 'NETWORK_TIMEOUT' | 'NETWORK_ABORT' | 'NETWORK_BAD_STATUS' // 4xx/5xx | 'NETWORK_PARSE_FAILED' | 'NETWORK_WS_CONNECT_FAILED' | 'NETWORK_WS_SEND_FAILED' | 'NETWORK_WS_MAX_RECONNECTS' | 'NETWORK_INVALID_URL' const NETWORK_ERROR_METADATA: Readonly> function registerNetworkErrorCodes(): void ``` ### Zod schemas ```ts HttpRequestSchema HttpClientOptionsSchema WebSocketClientOptionsSchema ``` ### Built-in middleware ```ts const loggingMiddleware: (logger: LoggerLike) => HttpMiddleware const retryMiddleware: (options?: RetryOptions) => HttpMiddleware // @suaveplan/retry const authMiddleware: (token: string | (() => string)) => HttpMiddleware const rateLimitMiddleware: (options: { maxRpm: number; clock?: Clock }) => HttpMiddleware ``` --- ## New additions for genesis 1. **Middleware pipeline** — `use(middleware)` appends to ordered chain; each middleware gets `(request, next)` and can modify request/response, short-circuit, or retry. 2. **Injectable `fetch`** — `HttpClientOptions.fetch` defaults to `globalThis.fetch`; swap to a stub in tests without `vi.mock`. 3. **Injectable `WebSocket`** — same pattern; `WebSocketClientOptions.WebSocket` defaults to `globalThis.WebSocket`. 4. **Retry wiring** — `retryMiddleware` uses `@suaveplan/retry` with circuit breaker; configurable per `HttpClient` instance. 5. **WS auto-reconnect** — exponential backoff via `@suaveplan/retry`; configurable max attempts; ping/pong keepalive. 6. **OTel instrumentation** — span `http.request` (attrs: `http.method`, `http.url`, `http.status_code`), counter `http_request_total`, histogram `http_request_duration_ms`; counter `ws.reconnect_total`. 7. **Property-based tests** — `buildUrl` round-trip with query params, `parseQueryString(serializeQueryString(params)) == params` for any param map. 8. **Benchmark suite** — `buildUrl` with 10 params, middleware chain dispatch (5 middlewares, no-op fetch stub). --- ## File layout ```text universal/network/network/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts types/ common.ts common.md core/ http-client/ http-client.ts http-client.md http-client.types.ts http-client.test.ts index.ts websocket-client/ websocket-client.ts websocket-client.md websocket-client.types.ts websocket-client.test.ts index.ts middleware/ logging/ logging.ts logging.md logging.test.ts index.ts retry/ retry.ts retry.md retry.test.ts index.ts auth/ auth.ts auth.md auth.test.ts index.ts rate-limit/ rate-limit.ts rate-limit.md rate-limit.test.ts index.ts utils/ url.ts url.md url.test.ts __benchmarks__/ network.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream**: `@suaveplan/service-discovery`, `@suaveplan/webhook-client` (may refactor to consume), any server/API packages. **Breaking changes**: none — fresh build. **Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/retry`, `@suaveplan/serialize` published to Verdaccio.