import type { Clock } from "../clock.ts"; import type { Dispatcher } from "undici"; import type { IncomingHttpHeaders } from "node:http"; import type { ProtectLogging } from "../logging.ts"; import type { StreamOptions } from "../event-bus.ts"; /** * The events the {@link Transport} publishes on its three-rail surface. Both are parameterless: a consumer that needs the cooldown duration reads it from the * {@link ProtectThrottledError} thrown by `send`, and a consumer that needs richer detail subscribes to the `unifi-protect:http:throttle:*` diagnostics channels. * * @category Transport */ export interface TransportEvents { throttleEntered: []; throttleExited: []; } /** * The HTTP methods the transport dispatches. One vocabulary for a request's verb, shared by {@link RequestOptions} and the device-command primitive so the set is defined * once rather than re-spelled. * * @category Transport */ export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST"; /** * Per-request options accepted by {@link Transport.request} and {@link Transport.send}. The library composes the caller's `signal` with its own timeout deadline, so * a caller can cancel independently of the timeout. `headers` here override the auth headers the transport stamps on automatically. * * @category Transport */ export interface RequestOptions { body?: string | Uint8Array; headers?: Record; method?: HttpMethod; signal?: AbortSignal; timeout?: number; } /** * The lower-level {@link Transport.send} options. Both flags follow the same pattern: a special caller opting out of one default send behavior. * * - `authRetry` controls the injected 401-relogin hook: the authenticated request path leaves it at its default of `true`, while the `AuthSession` login handshake * passes `false` so its own 401 cannot recurse back into relogin. * - `probe` marks a request as the connection-recovery reachability trial. It skips the open breaker's cooldown gate so the trial can dispatch at the * `ConnectionMonitor`'s cadence rather than waiting out the 300-second cooldown, while still booking its outcome through the normal path - so a successful probe * closes the breaker exactly as the autonomous half-open trial would. Only the monitor's recovery seams (`verify`, recovery `reBootstrap`) pass `true`; the refresh * failsafe and device commands leave it `false` and stay gated, so the breaker still protects against operational hammering. * * @category Transport */ export interface SendOptions extends RequestOptions { authRetry?: boolean; probe?: boolean; } /** * Construction options for {@link Transport}. `host` is the controller address; everything else is an injected seam. * * - `dispatcher` substitutes the undici dispatcher (an `undici.MockAgent` in tests, or a consumer-supplied pool). When injected, the transport never destroys or * rebuilds it - lifecycle stays with the injector, so `reset()` and disposal are no-ops on the dispatcher. * - `getAuthHeaders` supplies the cookie + CSRF headers stamped onto every request. `onUnauthorized` is the relogin hook invoked on a 401 before a single retry. * Both point upward at `AuthSession`, so they are passed as plain function seams (dependency inversion) rather than an `AuthSession` import. * * @category Transport */ export interface TransportOptions { clock?: Clock; dispatcher?: Dispatcher; getAuthHeaders?: () => Record; host: string; log?: ProtectLogging; onUnauthorized?: () => Promise; } /** * Determine whether an HTTP status code is a 2xx success. `undefined` (no response received) and any non-2xx code are failures. * * @param statusCode - The status code to test. * * @returns `true` for 200-299, `false` otherwise. */ export declare function responseOk(statusCode: number | undefined): boolean; /** * An HTTP response from the Protect controller, returned by {@link Transport.send}. The body is eagerly buffered (so no undrained stream can hold a slot in the * connection pool), and decoding is synchronous and lazy on top of that buffer. `send` returns this for *any* status; call {@link ProtectResponse.ensureOk} to turn * a non-2xx into its typed `FatalError`. * * @category Transport */ export declare class ProtectResponse { /** The HTTP status code. */ readonly statusCode: number; /** The response headers as undici delivered them. */ readonly headers: IncomingHttpHeaders; /** The fully buffered response body. Consumers needing raw bytes (e.g., a snapshot JPEG) read this directly. */ readonly body: Buffer; constructor(statusCode: number, headers: IncomingHttpHeaders, body: Buffer); /** * Parse the body as JSON. Synchronous - the bytes are already in hand. * * @typeParam T - The expected shape of the parsed document. * * @returns The parsed body. * * @throws {@link ProtectProtocolError} if the body is not valid JSON. */ json(): T; /** * Decode the body as a UTF-8 string. * * @returns The body text. */ text(): string; /** * Assert a successful response. Returns `this` unchanged on a 2xx so the call chains (`(await send(...)).ensureOk().json()`); throws the classified * `FatalError` on any non-2xx. * * @returns `this`, for chaining. * * @throws {@link ProtectAuthError} on 401, {@link ProtectAuthorizationError} on 403, {@link ProtectRequestError} on any other non-2xx. */ ensureOk(): this; } /** * The HTTP transport. Owns the undici connection pool, composes each request's deadline with the caller's cancellation, classifies every failure into the typed * `ProtectError` hierarchy, and runs the throttle circuit breaker that protects a degraded controller from being hammered. * * The breaker is the canonical three-state model - `closed -> open -> half-open -> closed`: * * - **closed:** normal operation. Each completed request books an outcome; a 2xx resets the consecutive-failure count, a non-2xx or transport exception increments * it. Crossing {@link PROTECT_API_ERROR_LIMIT} consecutive failures trips the breaker open. * - **open:** for {@link PROTECT_API_RETRY_INTERVAL} seconds, `send` throws {@link ProtectThrottledError} before dispatching - no traffic reaches the controller. * - **half-open:** once the cooldown elapses, requests are allowed through again as recovery probes. The first success closes the breaker (and emits `throttleExited` * only then, so the public signal never flaps); a failure re-arms the cooldown from that moment and stays open silently. * * Reachability lives here; session validity lives in `AuthSession`. The breaker never calls login to probe recovery - the next real request is * the probe, and the orthogonal 401-relogin concern is the injected `onUnauthorized` seam. The clock is injected so every transition is deterministically testable. * * @category Transport */ export declare class Transport implements AsyncDisposable { #private; constructor(options: TransportOptions); /** * Whether the throttle breaker is currently open (refusing or probing). Reads the breaker state synchronously - safe to call on a hot path. The * `ConnectionMonitor` reads this to fold transport health into the connection FSM. */ get isThrottled(): boolean; /** * Send a request and return the {@link ProtectResponse} for any HTTP status. Throttle-gates before dispatching, composes the caller's signal with the timeout * deadline, classifies transport failures into typed errors, books the breaker outcome, and runs the 401-relogin retry (once) when a hook is wired. * * This is the transport primitive. Callers that want the ergonomic "throw on non-2xx and parse JSON" path use {@link Transport.request}; callers that need raw * status, headers, or bytes (the auth handshake, snapshots) use `send` directly. * * @param url - The fully-qualified request URL. * @param opts - Per-request options. * * @returns The response. * * @throws {@link ProtectThrottledError} while the breaker is open and cooling; {@link ProtectTimeoutError}, {@link ProtectAbortedError}, or * {@link ProtectNetworkError} on transport-level failures. */ send(url: string, opts?: SendOptions): Promise; /** * Send a request, assert a 2xx, and parse the body as JSON. The ergonomic path over {@link Transport.send} for the common JSON case. * * @typeParam T - The expected shape of the parsed body. * * @param url - The fully-qualified request URL. * @param opts - Per-request options. * * @returns The parsed body. * * @throws The classified `FatalError` on a non-2xx, or a transport-level `ProtectError` on failure. */ request(url: string, opts?: RequestOptions): Promise; /** * Rebuild the owned connection pool, dropping every keepalive connection to the controller. Called when the breaker trips so the next attempt starts from a clean * pool rather than reusing sockets to a degraded controller. A no-op when the dispatcher was injected - that lifecycle belongs to the injector. */ reset(): void; /** * Subscribe to a transport event. Returns a `Disposable`; prefer `using sub = transport.on(...)` so the listener detaches at scope exit. * * @param event - The event to listen for. * @param handler - Invoked on each emission. * * @returns A `Disposable` that removes the listener when disposed. */ on(event: K, handler: (...args: TransportEvents[K]) => void): Disposable; /** * Wait for the next emission of a transport event. * * @param event - The event to await. * @param opts - Optional abort signal. * * @returns A promise resolving to the event's argument tuple. */ once(event: K, opts?: { signal?: AbortSignal; }): Promise; /** * Stream every subsequent emission of a transport event until the signal aborts. * * @param event - The event to stream. * @param opts - Stream options, including the abort signal. * * @returns An async iterable of the event's argument tuples. */ stream(event: K, opts?: StreamOptions): AsyncIterable; /** * Dispose the transport, destroying the owned pool. A no-op on an injected dispatcher. */ [Symbol.asyncDispose](): Promise; } //# sourceMappingURL=http.d.ts.map