feat: add TypeRT remote gateway infrastructure

This commit is contained in:
imccyu
2026-08-05 11:17:47 +08:00
parent effd8e1ebd
commit 64a963da0b
98 changed files with 7812 additions and 444 deletions
@@ -8,7 +8,9 @@ import type { IApiClient } from './api.ts'
import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts'
import { FixtureApiClient } from './fixture.ts'
import { WebApiClient } from './web-api-client.ts'
import { createUnavailableConnectionRpc, createWebConnectionRpc } from './rpc.ts'
import { isLoopbackHostname } from '../loopback-hostname.ts'
import type { ClientConnectionRpc } from '../rpc.ts'
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
export type {
@@ -36,6 +38,7 @@ export {
// Connection loop types are public through ConnectionHandle.start; the
// controller remains package-internal.
export type { ConnectionConfig, ConnectionSinks, ConnectionState }
export type { ClientConnectionRpc } from '../rpc.ts'
/** Required services (none — this is the wire root). */
@@ -51,6 +54,8 @@ export interface ConnectionHandle {
readonly api: IApiClient
/** Whether the current page authority is loopback; non-browser contexts default to true. */
readonly isLoopback: boolean
/** Generic logical RPC channels over the same Connection transport. */
readonly rpc: ClientConnectionRpc
/**
* Start the connect/pump/reconnect loop with the consumer's frame sinks.
* One consumer owns the streams (the runtime object layer); a second call
@@ -70,10 +75,12 @@ export function apply(ctx: Context): void {
const pageLocation = typeof location === 'undefined' ? undefined : location
const fixture = pageLocation !== undefined && new URLSearchParams(pageLocation.search).has('fixture')
const api: IApiClient = fixture ? new FixtureApiClient() : new WebApiClient()
const rpc = fixture ? createUnavailableConnectionRpc() : createWebConnectionRpc()
let started = false
const handle: ConnectionHandle = {
api,
isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname),
rpc,
start(sinks, config) {
if (started) throw new Error('connection: the stream loop is already owned by another consumer')
started = true
@@ -0,0 +1,75 @@
/** Browser caller for generic Connection unary RPC channels. */
import {
RpcId,
serverResponseSchema,
type ClientRequest,
} from '@deepseek-ai/dsh-host-apiproxy/api'
import type { ClientConnectionRpc } from '../rpc.ts'
const INTERNAL_BASE = 'http://dsh.internal'
const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/
const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
/**
* Create the browser-backed generic RPC caller.
* @returns caller that owns request correlation and response-envelope validation.
*/
export function createWebConnectionRpc(): ClientConnectionRpc {
return {
async call(channel, endpoint, payload, signal) {
assertTarget(channel, endpoint)
const rpcId = RpcId(crypto.randomUUID())
const message: ClientRequest = {
type: 'client-request',
rpcId,
method: endpoint,
payload,
}
const response = await globalThis.fetch(
new URL(`${channel}/${endpoint}`, resolveBase()),
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(message),
...signal === undefined ? {} : { signal },
},
)
if (!response.ok) {
throw new Error(`transport failure for ${channel}/${endpoint}: HTTP ${response.status}`)
}
const full = serverResponseSchema.parse(await response.json())
if (full.rpcId !== rpcId) {
throw new Error(`rpcId mismatch for ${endpoint}: sent ${rpcId}, got ${full.rpcId}`)
}
return full.result
},
}
}
/**
* Create the fixture-mode caller, where no Host Remote registry exists.
* @returns caller that rejects every generic Remote invocation.
*/
export function createUnavailableConnectionRpc(): ClientConnectionRpc {
return {
call(channel, endpoint) {
return Promise.reject(new Error(`connection RPC ${channel}/${endpoint} is unavailable in fixture mode`))
},
}
}
function resolveBase(): string {
const location = (globalThis as { location?: { origin?: string } }).location
return location?.origin !== undefined && location.origin !== 'null' ? location.origin : INTERNAL_BASE
}
function assertTarget(channel: string, endpoint: string): void {
const segments = endpoint.split('/')
if (!CHANNEL_PATTERN.test(channel)
|| segments.length === 0
|| segments.some(segment =>
segment === '' || segment === '.' || segment === '..' || !ENDPOINT_SEGMENT_PATTERN.test(segment))) {
throw new Error(`connection: invalid RPC target ${JSON.stringify(`${channel}/${endpoint}`)}`)
}
}
+57 -44
View File
@@ -7,15 +7,25 @@ import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
import { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_PATH } from './api-path.ts'
import { bridge } from './http-bridge.ts'
import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts'
import { HostConnectionService } from './rpc-host.ts'
import { rejectWebSocketUpgrade, WebSocketDownlinks } from './websocket-downlink.ts'
export type {
ConnectionRpcAuthority,
ConnectionRpcHandler,
ConnectionRpcHandlerOptions,
HostConnectionHandle,
HostConnectionRpc,
} from './rpc.ts'
export { HostConnectionService } from './rpc-host.ts'
export { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_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']
/** Services required before providing Connection; legacy `/api` attaches when apiProxy is present. */
export const inject = ['httpServer']
/** Plugin config: the deployment's non-loopback serving authorities. */
export interface ConnectionConfig {
@@ -83,49 +93,52 @@ export function apply(ctx: Context, config?: ConnectionConfig): void {
// 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 downlinks = new WebSocketDownlinks(ctx.apiProxy)
const route: WebRoute = {
kind: 'prefix',
path: API_PATH,
handler: async (req, res) => {
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
const method = pathname.startsWith(`${API_PATH}/`)
? pathname.slice(API_PATH.length + 1)
: undefined
const allowed = method !== undefined && PRIVILEGED_METHODS.has(method)
? isTrustedApiRequest(req, [])
: isTrustedApiRequest(req, trustedHosts)
if (!allowed) {
res.writeHead(403)
res.end('forbidden')
return
}
if (req.method === 'GET' && (pathname === MUX_EVENTS_PATH || pathname === HOST_EVENTS_PATH)) {
res.writeHead(426, { connection: 'Upgrade', upgrade: 'websocket' })
res.end('upgrade required')
return
}
await bridge(req, res, apiHandler)
},
}
ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
const registerDownlink = (
path: string,
handle: WebUpgradeRoute['handler'],
): void => {
ctx.effect(() => ctx.httpServer.registerUpgrade({
path,
handler: (req, socket, head) => {
if (!isTrustedApiRequest(req, trustedHosts)) {
rejectWebSocketUpgrade(socket)
new HostConnectionService(ctx, trustedHosts)
ctx.inject(['apiProxy'], (apiCtx) => {
const apiHandler = toFetchHandler(apiCtx.apiProxy)
const downlinks = new WebSocketDownlinks(apiCtx.apiProxy)
const route: WebRoute = {
kind: 'prefix',
path: API_PATH,
handler: async (req, res) => {
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
const method = pathname.startsWith(`${API_PATH}/`)
? pathname.slice(API_PATH.length + 1)
: undefined
const allowed = method !== undefined && PRIVILEGED_METHODS.has(method)
? isTrustedApiRequest(req, [])
: isTrustedApiRequest(req, trustedHosts)
if (!allowed) {
res.writeHead(403)
res.end('forbidden')
return
}
return handle(req, socket, head)
if (req.method === 'GET' && (pathname === MUX_EVENTS_PATH || pathname === HOST_EVENTS_PATH)) {
res.writeHead(426, { connection: 'Upgrade', upgrade: 'websocket' })
res.end('upgrade required')
return
}
await bridge(req, res, apiHandler)
},
}), `client-connection: ${path} WebSocket`)
}
ctx.effect(() => () => downlinks.close(), 'client-connection: WebSocket downlinks')
registerDownlink(MUX_EVENTS_PATH, (req, socket, head) => { downlinks.handleMux(req, socket, head) })
registerDownlink(HOST_EVENTS_PATH, (req, socket, head) => { downlinks.handleHost(req, socket, head) })
}
apiCtx.effect(() => apiCtx.httpServer.register(route), 'client-connection: /api route')
const registerDownlink = (
path: string,
handle: WebUpgradeRoute['handler'],
): void => {
apiCtx.effect(() => apiCtx.httpServer.registerUpgrade({
path,
handler: (req, socket, head) => {
if (!isTrustedApiRequest(req, trustedHosts)) {
rejectWebSocketUpgrade(socket)
return
}
return handle(req, socket, head)
},
}), `client-connection: ${path} WebSocket`)
}
apiCtx.effect(() => () => downlinks.close(), 'client-connection: WebSocket downlinks')
registerDownlink(MUX_EVENTS_PATH, (req, socket, head) => { downlinks.handleMux(req, socket, head) })
registerDownlink(HOST_EVENTS_PATH, (req, socket, head) => { downlinks.handleHost(req, socket, head) })
})
}
+150
View File
@@ -0,0 +1,150 @@
/** Host registry and HTTP adapter for generic Connection RPC channels. */
import { Context, Service } from 'cordis'
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
import {
clientRequestSchema,
RpcId,
type ClientRequest,
type RpcError,
type RpcId as RpcIdType,
type ServerResponse as RpcServerResponse,
} from '@deepseek-ai/dsh-host-apiproxy/api'
import { bridge } from './http-bridge.ts'
import { isTrustedApiRequest } from './api-request-trust.ts'
import type {
ConnectionRpcHandler,
ConnectionRpcHandlerOptions,
HostConnectionHandle,
HostConnectionRpc,
} from './rpc.ts'
const INVALID_REQUEST_RPC_ID = RpcId('invalid-request')
const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/
const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
/** Host Connection service whose channel registrations belong to the caller fiber. */
export class HostConnectionService extends Service implements HostConnectionHandle {
/**
* Provide the Host half over the active HTTP server.
* @param ctx - owning Connection plugin context.
* @param trustedHosts - deployment authorities accepted by trusted-host channels.
*/
constructor(ctx: Context, private readonly trustedHosts: readonly string[]) {
super(ctx, 'connection')
}
/** Generic channel registry scoped to the Context reading this service. */
get rpc(): HostConnectionRpc {
const owner = this.ctx
return {
handle: (channel, handler, options) => this.register(owner, channel, handler, options),
}
}
private register(
owner: Context,
channel: string,
handler: ConnectionRpcHandler,
options: ConnectionRpcHandlerOptions,
): () => Promise<void> {
assertChannel(channel)
const trustedHosts = options.authority === 'loopback' ? [] : this.trustedHosts
const fetchHandler = rpcFetchHandler(channel, handler)
const route: WebRoute = {
kind: 'prefix',
path: channel,
handler: async (req, res) => {
if (!isTrustedApiRequest(req, trustedHosts)) {
res.writeHead(403)
res.end('forbidden')
return
}
await bridge(req, res, fetchHandler)
},
}
return owner.effect(
() => owner.httpServer.register(route),
`client-connection: ${channel} rpc channel`,
)
}
}
function rpcFetchHandler(
channel: string,
handler: ConnectionRpcHandler,
): { fetch: typeof fetch } {
return {
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const request = input instanceof Request ? input : new Request(input, init)
const endpoint = endpointFromPath(channel, new URL(request.url).pathname)
if (request.method !== 'POST' || endpoint === undefined) {
return new Response('not found', { status: 404 })
}
const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()
if (mediaType !== 'application/json') {
return new Response('content type must be application/json', { status: 415 })
}
let body: unknown
try {
body = await request.json()
} catch {
return new Response('body is not JSON', { status: 400 })
}
const envelope = clientRequestSchema.safeParse(body)
if (!envelope.success) {
const rawId = (body as { rpcId?: unknown } | null)?.rpcId
const rpcId = typeof rawId === 'string' ? RpcId(rawId) : INVALID_REQUEST_RPC_ID
return errorResponse(rpcId, {
code: 'bad-request',
message: 'invalid client-request message',
details: { issues: envelope.error.issues },
})
}
const message: ClientRequest = envelope.data
if (message.method !== endpoint) {
return errorResponse(message.rpcId, {
code: 'bad-request',
message: `method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
details: { issues: [] },
})
}
try {
const result = await handler(endpoint, message.payload, request.signal)
return fullResponse(message.rpcId, result)
} catch (error) {
return new Response(`handler failure: ${String(error)}`, { status: 500 })
}
},
}
}
function endpointFromPath(channel: string, pathname: string): string | undefined {
if (!pathname.startsWith(`${channel}/`)) return undefined
const endpoint = pathname.slice(channel.length + 1)
const segments = endpoint.split('/')
if (segments.length === 0 || segments.some(segment =>
segment === '' || segment === '.' || segment === '..' || !ENDPOINT_SEGMENT_PATTERN.test(segment))) {
return undefined
}
return endpoint
}
function errorResponse(rpcId: RpcIdType, error: RpcError): Response {
return fullResponse(rpcId, { ok: false, error })
}
function fullResponse(rpcId: RpcIdType, result: RpcServerResponse['result']): Response {
const body: RpcServerResponse = { type: 'server-response', rpcId, result }
return Response.json(body)
}
function assertChannel(channel: string): void {
if (!CHANNEL_PATTERN.test(channel) || channel === '/api') {
throw new Error(`connection: invalid or reserved RPC channel ${JSON.stringify(channel)}`)
}
}
+59
View File
@@ -0,0 +1,59 @@
/** Generic unary RPC contracts shared by the Host and Client Connection halves. */
import type { RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
/** Trust fence applied before a Host RPC channel reaches its handler. */
export type ConnectionRpcAuthority = 'trusted-host' | 'loopback'
/** Registration policy for one logical RPC channel. */
export interface ConnectionRpcHandlerOptions {
/** Browser authority accepted by every endpoint in this channel. */
readonly authority: ConnectionRpcAuthority
}
/** Handler invoked after Connection has decoded the transport envelope. */
export type ConnectionRpcHandler = (
endpoint: string,
payload: unknown,
signal: AbortSignal,
) => Promise<RpcResult<unknown>>
/** Host registry for logical RPC channels carried by the current transport. */
export interface HostConnectionRpc {
/**
* Register one absolute channel prefix and its trust policy.
* @param channel - absolute logical channel such as `/api2`.
* @param handler - decoded endpoint handler returning the existing RPC result shape.
* @param options - channel trust policy.
* @returns asynchronous disposer removing the channel and its physical route.
*/
handle(
channel: string,
handler: ConnectionRpcHandler,
options: ConnectionRpcHandlerOptions,
): () => Promise<void>
}
/** Host `ctx.connection` shape consumed by transport-independent adapters. */
export interface HostConnectionHandle {
/** Generic RPC channel registry. */
readonly rpc: HostConnectionRpc
}
/** Client caller for logical RPC channels carried by the current transport. */
export interface ClientConnectionRpc {
/**
* Call one endpoint through an already registered logical channel.
* @param channel - absolute logical channel such as `/api2`.
* @param endpoint - channel-relative endpoint such as `goals/create`.
* @param payload - channel-owned request payload.
* @param signal - optional caller cancellation.
* @returns the existing RPC success/error result; correlation stays inside Connection.
*/
call(
channel: string,
endpoint: string,
payload: unknown,
signal?: AbortSignal,
): Promise<RpcResult<unknown>>
}