/** * Sub-client backend routes (T16): pairing claim and connection status for a * DSH process running `role: 'client'` — this machine's settings page is the * ONLY browser surface that talks to them, same-origin like the host's admin * routes (T14). The claim route forwards the pairing code to the SERVER's * gateway (`/lan-gate/pair/claim-desktop`) and hands the returned * token straight back to the browser, which writes it into the row's * `deviceToken` secret field; this process itself never persists or logs the * token — it only ever puts it into an Authorization header for the status * probe. The status route reads `serverUrl` / `deviceToken` from the plugin * ROW PER REQUEST (both are volatile `{ get() }`-wrapped fields, so the row * object `apply()` received is the only live source) and probes the server's * relay ping, reporting one of the five connection states. * * Same walls as the admin routes, in the same order: `admit` first (webServer * routes skip DSH's /api authentication), then for the POST also the * same-origin gate and a 16 KiB JSON-object body cap. Built by a factory so * the tests drive it over a real socket with a mock server gateway. * * T41b adds the plain-HTTP relay under `client/http/`: the GETs the * browser fetch wrapper intercepted (`/api/changes.summary` / `changes.diff` * naming a virtual session id) are re-issued here through the relay client's * `http()` with the original id restored, and the upstream status, * content type and body travel back verbatim. */ import type { IncomingMessage, ServerResponse } from 'node:http'; import type { AdminAdmission } from './admin-routes.js'; import type { FetchRouteInterceptDiagnostics } from './fetch-route-intercept.js'; import type { CurrentSessionRead, InterceptDiagnostics } from './intercept.js'; import type { PluginRouteInterceptDiagnostics } from './plugin-route-intercept.js'; import type { BridgeStatus } from './plugin-bridge.js'; import type { RelayClient } from './relay-client.js'; /** Prefix all client routes live under (one webServer prefix registration). */ export declare const CLIENT_ROUTE_PREFIX = "/_dsh/zen-remote/client"; /** POST `{serverUrl, code, name}`: redeem a desktop pairing code. */ export declare const CLIENT_CLAIM_ROUTE = "/_dsh/zen-remote/client/claim"; /** GET: the current connection state (never carries the token). */ export declare const CLIENT_STATUS_ROUTE = "/_dsh/zen-remote/client/status"; /** POST: one immediate reconnect attempt (T43, the settings page's 立即重连); * answered 409 unless the relay client is currently `offline`. */ export declare const CLIENT_RECONNECT_ROUTE = "/_dsh/zen-remote/client/reconnect"; /** POST: the browser's NETWORK-RESTORED hint. * * Why this route exists: the host half runs in a plain Node process with no * OS network events, and the browser half cannot touch the relay client's * memory across the HTTP boundary. So the browser — the only place that can * observe `online`/`visibilitychange` — reports the hint here, and the host * turns it into ONE controlled probe + reconnect. * * Deliberately narrower than `reconnect`: * - it first runs `probeHealth()` (authenticated GET /ping); a hint NEVER * alone proves the link is back (`navigator.onLine === true` is not * evidence); * - only a successful probe advances to `networkRestored()`, which itself * refuses outside `offline` and inside its 3s anti-storm gap; * - it answers the probe outcome so the page can log/annotate it. * * It NEVER changes state by itself: `probeHealth` is read-only, and * `networkRestored` is the same controlled path the settings button uses. */ export declare const CLIENT_NETWORK_HINT_ROUTE = "/_dsh/zen-remote/client/network-hint"; /** GET: the remote-session status the T34 client parts render from (T34) — * the relay's serving state, the interface-compatibility verdict, the * handshake's server name, and the closed-session map. The prefix is * registered on BOTH roles (T34-fix): a host — no relay client wired — * answers the empty `{state:'unpaired', versionMismatch:false, serverName:'', * closed:{}}` conclusion. It carries no token and no server address. */ export declare const CLIENT_REMOTE_STATUS_ROUTE = "/_dsh/zen-remote/client/remote-status"; /** POST `{sessionId: <虚拟 id>}`: close one remote session from THIS machine * (T34) — the backend forwards the ORIGINAL id through the relay's * `POST relay/v1/unshare`, so the server closes it with reason `'client'`. */ export declare const CLIENT_UNSHARE_ROUTE = "/_dsh/zen-remote/client/unshare"; /** POST `{sessionId: string | null}`: the browser's report of which session * the host UI currently has open (T62) — the intercept layer runs in this * backend process and cannot read the browser's localStorage itself. `null` * means a readable signal says nothing is open; the sink stores the value * and re-judges the mergers immediately. */ export declare const CLIENT_CURRENT_SESSION_ROUTE = "/_dsh/zen-remote/client/current-session"; /** Prefix of the plain-HTTP relay routes (T41b): * `GET ${CLIENT_HTTP_ROUTE_PREFIX}?` relays one intercepted * `/api/` call (the fetch wrapper's rewrites land here) to the * server, with the virtual session id swapped back to the original. */ export declare const CLIENT_HTTP_ROUTE_PREFIX = "/_dsh/zen-remote/client/http/"; /** The fetch face this module needs; injectable for tests. */ export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; export interface ClientHandlerOptions { /** The connection service's admit, injected by apply(); tests fake it. */ admit: (req: IncomingMessage) => AdminAdmission; /** The plugin ROW as apply() received it — `serverUrl` / `deviceToken` are * volatile, so they must be read through this at request time, never * snapshotted at registration. */ getRowConfig: () => unknown; /** Outbound fetch, defaulting to the global one. */ fetchImpl?: FetchLike; /** Upload-redirect service probe (RD-6): lazily reflects `connection` / * `fileUploads` from a context that already resolved, plus whether the * fetch-route inject callback ever fired. Answers WHY the upload * redirection is absent on compositions where one of the two services * never becomes available (cordis keeps such injects silently PENDING). */ getServiceProbe?: () => Record; /** The live relay client (T23a), once apply() built one. When it has LEFT * its initial `unpaired` state — a connection attempt ran — the status * route reports ITS verdict (state + handshake server name) and skips the * ping probe; the probe below stays as the fallback for a client that * never connected, so the settings page still gets a fresh answer while * the startup `connect()` is still unpaired. */ getRelayClient?: () => RelayClient | undefined; /** The typert interception's diagnostics (T23b-1), read live per request. * Shapes, counters and failure codes only — never a token. Undefined when * this composition never even attempted an install (no typertGateway). */ getIntercept?: () => InterceptDiagnostics | undefined; /** The exact-fetch-route interception's diagnostics (T51), read live per * request: the local upload route's remote-forwarding install state and * its refusal counters. Undefined when this composition never attempted * one (no connection service, or no fileUploads service to inject). */ getFetchRouteIntercept?: () => FetchRouteInterceptDiagnostics | undefined; /** The plugin-route interception's diagnostics (PB-4), read live per * request: the local webServer `match` wrap that forwards bridged-plugin * requests for remote sessions. Undefined when this composition never * attempted one (a host role, or no webServer). */ getPluginRouteIntercept?: () => PluginRouteInterceptDiagnostics | undefined; /** The plugin bridge's live verdict (PB-2), computed PER CALL off the local * registry, the last handshake's `plugins` table and the connection state — * never a stored conclusion. Wired only by the CLIENT role's mount; absent * (the host mount, old tests) both routes answer the empty shape: status's * `pluginBridge: []` and remote-status's `bridgedPlugins: []`. */ getPluginBridgeStatus?: () => BridgeStatus[]; /** Serve ONLY the remote-status route (T41a-fix2, the host role's mount): * every other route under the prefix answers the same 404 an unknown path * would. The claim / status / reconnect / unshare routes exist for a * CLIENT — a host carries no relay client and no pairing surface, so * answering them (even with their empty conclusions) is exposure without * a user. The remote-status route stays: the T34 client parts poll it on * whatever role this process runs. */ remoteStatusOnly?: boolean; /** T62: the sink for the browser's current-session reports — stores the * latest value (memory, last write wins across windows) and makes the * intercept re-judge at once. Wired only on the CLIENT mount; the HOST * mount's remoteStatusOnly wall 404s the route before this matters, and * a client handler WITHOUT the sink answers the same 404 (no sink, no * route). */ onCurrentSession?: (read: CurrentSessionRead) => void; } export type ClientHandler = (req: IncomingMessage, res: ServerResponse) => Promise; /** * Build the client route handler for one plugin row. The returned handler * owns the full response lifecycle of every request under * {@link CLIENT_ROUTE_PREFIX} and never throws. */ export declare function createClientHandler(options: ClientHandlerOptions): ClientHandler; //# sourceMappingURL=client-routes.d.ts.map