/** Host HTTP bridge for browser-client RPC. */ import type { Context } from 'cordis' import z from 'schemastery' // Activates the httpServer Context merge used below. import type { WebRoute } from '@deepseek-ai/dsh-host-webserver' import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy' import { API_PATH } from './api-path.ts' import { bridge } from './http-bridge.ts' import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts' export { API_PATH } from './api-path.ts' /** Stable Cordis plugin name. */ export const name = 'client-connection' /** Services required before mounting the route. */ export const inject = ['httpServer', 'apiProxy'] /** Plugin config: the deployment's non-loopback serving authorities. */ export interface ConnectionConfig { /** * Authorities this deployment serves beyond loopback: exact `host:port`, or * port-less `host` matching any port. The /api trust fence refuses any * request whose Host is neither loopback nor listed here, so a * non-loopback (`0.0.0.0`) deployment must declare the names it is reached * by (the dsh CLI derives the machine's LAN IP literals itself). An entry * that is not a bare, canonical authority fails the plugin load. */ trustedHosts?: string[] } export const Config: z = z.object({ trustedHosts: z.array(String).default([]), }) /** * Mounts the API gateway under the browser transport prefix. Every request on * the prefix passes the browser-trust fence first (DNS-rebinding and * cross-site defense — [api-request-trust](./api-request-trust.ts)). * @param ctx - Host plugin context. * @param config - resolved plugin config (schema defaults applied). */ export function apply(ctx: Context, config?: ConnectionConfig): void { // The Loader resolves schema defaults; hand-built test contexts may pass none. const trustedHosts = config?.trustedHosts ?? [] // Config boundary: a malformed entry fails the load loudly here rather than // silently authorizing its hostname prefix at request time. for (const entry of trustedHosts) assertTrustedAuthority(entry) const apiHandler = toFetchHandler(ctx.apiProxy) const route: WebRoute = { kind: 'prefix', path: API_PATH, handler: async (req, res) => { if (!isTrustedApiRequest(req, trustedHosts)) { res.writeHead(403) res.end('forbidden') return } await bridge(req, res, apiHandler) }, } ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route') }