/** Node half: registers the /api prefix route bridging to the api gateway. */ import { EventEmitter } from 'node:events' import { createServer, request as httpRequest } from 'node:http' import { Readable } from 'node:stream' import { Context } from '@deepseek-ai/cordis' import { describe, expect, it } from 'vitest' import type { AddressInfo } from 'node:net' import type { IncomingMessage, ServerResponse } from 'node:http' import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api' import type { AttachmentStore } from '@deepseek-ai/dsh-attachment' import { RpcId, type ClientRequest } from '@deepseek-ai/dsh-host-apiproxy/api' import type { WebServer, WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver' import { API_PATH, apply, inject, type HostConnectionHandle } from '../src/index.ts' import { DEFAULT_MAX_REQUEST_BODY_BYTES } from '../src/http-bridge.ts' import { provideBrowserCredentials } from './browser-credentials.ts' /** Structural webServer fake recording both route registries. */ function fakeHttpServer( routes: WebRoute[], upgrades: WebUpgradeRoute[], ): Pick { return { register(route) { if (routes.some(candidate => candidate.kind === route.kind && candidate.path === route.path)) { throw new Error(`duplicate route ${route.path}`) } routes.push(route) return () => { routes.splice(routes.indexOf(route), 1) } }, registerUpgrade(route) { upgrades.push(route) return () => { upgrades.splice(upgrades.indexOf(route), 1) } }, tapIndex: () => () => {}, port: 0, } } /** Bodyless GET carrying the given headers (enough for the trust fence + bridge). */ function fakeRequest(headers: Record, url = `${API_PATH}/session.list`): IncomingMessage { const request = Readable.from([]) as unknown as IncomingMessage Object.assign(request, { url, method: 'GET', headers }) return request } /** JSON POST carrying a complete client-request envelope. */ function fakePost(headers: Record, url: string, body: unknown): IncomingMessage { const request = Readable.from([Buffer.from(JSON.stringify(body))]) as unknown as IncomingMessage Object.assign(request, { url, method: 'POST', headers: { 'content-type': 'application/json', ...headers } }) return request } /** Raw POST for malformed-body and media-type boundary cases. */ function fakeRawPost(headers: Record, url: string, body: string): IncomingMessage { const request = Readable.from([Buffer.from(body)]) as unknown as IncomingMessage Object.assign(request, { url, method: 'POST', headers }) return request } /** Response recorder compatible with both the fence's short-circuit and the bridge. */ function fakeResponse(): { response: ServerResponse state: { status?: number; headers?: Record; body?: unknown } } { const state: { status?: number; headers?: Record; body?: unknown } = {} const chunks: Buffer[] = [] const response = Object.assign(new EventEmitter(), { writableEnded: false, writeHead(value: number, headers?: Record) { state.status = value if (headers !== undefined) state.headers = headers return this }, write(value: string | Uint8Array) { chunks.push(Buffer.from(value)); return true }, end(this: { writableEnded: boolean }, value?: unknown) { if (typeof value === 'string' || value instanceof Uint8Array) chunks.push(Buffer.from(value)) else if (value !== undefined) throw new TypeError('fake response only accepts string or Uint8Array bodies') if (chunks.length > 0) state.body = Buffer.concat(chunks).toString() this.writableEnded = true return this }, }) as unknown as ServerResponse return { response, state } } async function mounted(config?: { trustedHosts?: string[] }): Promise<{ routes: WebRoute[] upgrades: WebUpgradeRoute[] connection: HostConnectionHandle dispose: () => Promise }> { const ctx = new Context() const routes: WebRoute[] = [] const upgrades: WebUpgradeRoute[] = [] provideBrowserCredentials(ctx) ctx.provide('webServer', fakeHttpServer(routes, upgrades) as WebServer) ctx.provide('apiProxy', {} as unknown as ApiProxy) const fiber = ctx.plugin({ inject: [...inject], apply }, config) await fiber.await() return { routes, upgrades, connection: ctx.get('connection') as HostConnectionHandle, dispose: () => fiber.dispose(), } } /** Exchange a service's process token for one authority-bound Cookie header. */ function browserCookie(connection: HostConnectionHandle, authority: string): string { const url = new URL(connection.authenticatedUrl(`http://${authority}`)) const exchanged = fakeResponse() connection.authorizeIndex( fakeRequest({ host: authority }, `${url.pathname}${url.search}`), exchanged.response, ) const setCookie = exchanged.state.headers?.['set-cookie'] if (setCookie === undefined) throw new Error('browser token exchange did not set a cookie') return setCookie.split(';', 1)[0]! } describe('connection node half', () => { it('reserves enough default carrier capacity for the 200 MiB image batch', () => { expect(DEFAULT_MAX_REQUEST_BODY_BYTES).toBe(300 * 1024 * 1024) expect(DEFAULT_MAX_REQUEST_BODY_BYTES).toBeGreaterThan(Math.ceil(200 * 1024 * 1024 * 4 / 3) + 1024 * 1024) }) it('fails loud when the carrier cap cannot hold the configured image batch', async () => { const ctx = new Context() const routes: WebRoute[] = [] ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer) ctx.provide('attachments', { imageLimits: { maxMessageImageBytes: 20 * 1024 * 1024 }, } as AttachmentStore) ctx.provide('apiProxy', {} as ApiProxy) await expect(apply(ctx, { maxRequestBodyBytes: 1024 })) .rejects.toThrow(/must be at least .* aggregate image limit/) expect(routes).toHaveLength(0) }) it('fails the load on a trustedHosts entry that is not a bare authority', async () => { const routes: WebRoute[] = [] const upgrades: WebUpgradeRoute[] = [] const ctx = new Context() provideBrowserCredentials(ctx) ctx.provide('webServer', fakeHttpServer(routes, upgrades) as WebServer) ctx.provide('apiProxy', {} as unknown as ApiProxy) const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.internal/path'] }) await expect(fiber).rejects.toThrow(/not a bare host\[:port\] authority/) expect(routes).toHaveLength(0) expect(upgrades).toHaveLength(0) }) it('registers only the HTTP route and removes it with the fiber', async () => { const { routes, upgrades, dispose } = await mounted() expect(routes).toHaveLength(1) expect(routes[0]).toMatchObject({ kind: 'prefix', path: API_PATH }) expect(upgrades).toHaveLength(0) await dispose() expect(routes).toHaveLength(0) expect(upgrades).toHaveLength(0) }) it('refuses an untrusted Host on any /api path before the bridge runs', async () => { const { routes, dispose } = await mounted() const { response, state } = fakeResponse() await routes[0]!.handler(fakeRequest({ host: 'harness.example', origin: 'http://harness.example', 'sec-fetch-site': 'same-origin', }), response) expect(state.status).toBe(403) expect(state.body).toBe('forbidden') await dispose() }) it('requires the same browser session for every method on every trusted authority', async () => { const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example'] }) const methods = [ 'host.openPath', 'settings.describe', 'settings.update', 'credentials.describe', 'credentials.set', 'llm.discoverModels', 'llm.models', 'agentPreset.openDocument', ] for (const method of methods) { const denied = fakeResponse() await routes[0]!.handler(fakeRequest({ host: 'harness.example' }, `${API_PATH}/${method}`), denied.response) expect([method, denied.state.status, denied.state.body]).toEqual([method, 401, 'unauthorized']) } const cookie = browserCookie(connection, 'harness.example') for (const method of methods) { const allowed = fakeResponse() await routes[0]!.handler( fakeRequest({ host: 'harness.example', cookie }, `${API_PATH}/${method}`), allowed.response, ) expect([method, allowed.state.status]).toEqual([method, 404]) } const forged = fakeResponse() await routes[0]!.handler(fakeRequest({ host: 'localhost:3080' }), forged.response) expect(forged.state).toMatchObject({ status: 401, body: 'unauthorized' }) await dispose() }) it('passes loopback and declared-authority requests through to the bridge', async () => { const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example:3080', '192.168.1.5'] }) // Loopback, no browser markers (curl shape): the fence passes; the carrier // answers 404 for a GET unary path — proof the bridge ran. const loopback = fakeResponse() await routes[0]!.handler(fakeRequest({ host: '127.0.0.1:3080', cookie: browserCookie(connection, '127.0.0.1:3080'), }), loopback.response) expect(loopback.state.status).toBe(404) // An all-interfaces composition derives port-less LAN IP literals, which // pass markerless curl on any port. const lan = fakeResponse() await routes[0]!.handler(fakeRequest({ host: '192.168.1.5:3080', cookie: browserCookie(connection, '192.168.1.5:3080'), }), lan.response) expect(lan.state.status).toBe(404) // Declared public authority, same-origin browser shape. const declared = fakeResponse() await routes[0]!.handler(fakeRequest({ host: 'harness.example:3080', origin: 'http://harness.example:3080', 'sec-fetch-site': 'same-origin', cookie: browserCookie(connection, 'harness.example:3080'), }), declared.response) expect(declared.state.status).toBe(404) await dispose() }) it('shares its configured trust and authentication policy with sibling routes', async () => { const { connection, dispose } = await mounted({ trustedHosts: ['harness.example'] }) const loopback = fakeRequest({ host: '127.0.0.1:3080' }) const declared = fakeRequest({ host: 'harness.example' }) expect(connection.requestRejection(loopback)).toBe(401) expect(connection.requestRejection(declared)).toBe(401) expect(connection.requestRejection(fakeRequest({ host: 'harness.example', cookie: browserCookie(connection, 'harness.example'), }))).toBeUndefined() await dispose() }) it('provides a disposable dedicated RPC channel without requiring apiProxy', async () => { const ctx = new Context() const routes: WebRoute[] = [] provideBrowserCredentials(ctx) ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer) const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() expect(routes).toHaveLength(1) expect(routes[0]).toMatchObject({ kind: 'prefix', path: API_PATH }) const connection = ctx.get('connection') as HostConnectionHandle const calls: unknown[] = [] const remove = connection.rpc.handle('/rpc', async (endpoint, payload) => { calls.push({ endpoint, payload }) return { ok: true, value: { accepted: true } } }) const route = routes.find(candidate => candidate.path === '/rpc') expect(route).toBeDefined() const request: ClientRequest = { type: 'client-request', rpcId: RpcId('rpc-dedicated'), method: 'goals/create', payload: { args: { agentId: 'agent-1' } }, } const result = fakeResponse() await route!.handler(fakePost({ host: '127.0.0.1:3080', cookie: browserCookie(connection, '127.0.0.1:3080'), }, '/rpc/goals/create', request), result.response) expect(result.state.status).toBe(200) expect(JSON.parse(String(result.state.body))).toEqual({ type: 'server-response', rpcId: 'rpc-dedicated', result: { ok: true, value: { accepted: true } }, }) expect(calls).toEqual([{ endpoint: 'goals/create', payload: { args: { agentId: 'agent-1' } }, }]) expect(() => connection.rpc.handle('/rpc', async () => ({ ok: true, value: null }))) .toThrow(/duplicate route/) await remove() expect(routes.map(candidate => candidate.path)).toEqual([API_PATH]) await fiber.dispose() expect(routes).toHaveLength(0) }) it('dispatches claimed /api endpoints before the API Proxy fallback and withdraws the claim', async () => { const ctx = new Context() const routes: WebRoute[] = [] provideBrowserCredentials(ctx) ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer) ctx.provide('apiProxy', {} as unknown as ApiProxy) const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.example'] }) await fiber.await() const connection = ctx.get('connection') as HostConnectionHandle const calls: unknown[] = [] const remove = connection.rpc.intercept( '/api', endpoint => endpoint === 'goals/create', async (endpoint, payload) => { calls.push({ endpoint, payload }) return { ok: true, value: { accepted: true } } }, ) expect(() => connection.rpc.intercept( '/api', () => true, async () => ({ ok: true, value: null }), )).toThrow('already has an interceptor') expect(() => connection.rpc.intercept( '/rpc' as '/api', () => true, async () => ({ ok: true, value: null }), )).toThrow('invalid shared RPC channel') const route = routes.find(candidate => candidate.path === API_PATH)! const request: ClientRequest = { type: 'client-request', rpcId: RpcId('rpc-shared'), method: 'goals/create', payload: { args: { agentId: 'agent-1' } }, } const claimed = fakeResponse() const loopbackCookie = browserCookie(connection, '127.0.0.1:3080') await route.handler(fakePost({ host: '127.0.0.1:3080', cookie: loopbackCookie, }, '/api/goals/create', request), claimed.response) expect(JSON.parse(String(claimed.state.body))).toEqual({ type: 'server-response', rpcId: 'rpc-shared', result: { ok: true, value: { accepted: true } }, }) expect(calls).toEqual([{ endpoint: 'goals/create', payload: { args: { agentId: 'agent-1' } }, }]) const denied = fakeResponse() await route.handler(fakePost({ host: 'other.example' }, '/api/goals/create', request), denied.response) expect(denied.state).toMatchObject({ status: 403, body: 'forbidden' }) expect(calls).toHaveLength(1) const unclaimed = fakeResponse() await route.handler(fakeRequest({ host: '127.0.0.1:3080', cookie: loopbackCookie, }, '/api/session.list'), unclaimed.response) expect(unclaimed.state.status).toBe(404) await remove() const withdrawn = fakeResponse() await route.handler(fakePost({ host: '127.0.0.1:3080', cookie: loopbackCookie, }, '/api/goals/create', request), withdrawn.response) expect(withdrawn.state.status).toBe(404) expect(calls).toHaveLength(1) const removeAuthenticated = connection.rpc.intercept( '/api', endpoint => endpoint === 'goals/create', async () => ({ ok: true, value: null }), ) const declared = fakeResponse() await route.handler(fakePost({ host: 'harness.example', cookie: browserCookie(connection, 'harness.example'), }, '/api/goals/create', request), declared.response) expect(declared.state.status).toBe(200) await removeAuthenticated() await fiber.dispose() }) it('applies the configured trust fence and JSON envelope checks to generic channels', async () => { const ctx = new Context() const routes: WebRoute[] = [] provideBrowserCredentials(ctx) ctx.provide('webServer', fakeHttpServer(routes, []) as WebServer) const fiber = ctx.plugin({ inject: [...inject], apply }, { trustedHosts: ['harness.example'] }) await fiber.await() const connection = ctx.get('connection') as HostConnectionHandle const remove = connection.rpc.handle('/rpc', async (endpoint) => { if (endpoint === 'fail') throw new Error('handler broke') return { ok: true, value: null } }) const route = routes.find(candidate => candidate.path === '/rpc')! const harnessHeaders = { host: 'harness.example', cookie: browserCookie(connection, 'harness.example'), } const denied = fakeResponse() await route.handler(fakePost({ host: 'other.example' }, '/rpc/goals/create', {}), denied.response) expect(denied.state).toMatchObject({ status: 403, body: 'forbidden' }) const unauthenticated = fakeResponse() await route.handler(fakePost({ host: 'harness.example' }, '/rpc/goals/create', {}), unauthenticated.response) expect(unauthenticated.state).toMatchObject({ status: 401, body: 'unauthorized' }) const methodMismatch = fakeResponse() await route.handler(fakePost(harnessHeaders, '/rpc/goals/create', { type: 'client-request', rpcId: 'rpc-bad', method: 'other', payload: {}, }), methodMismatch.response) expect(JSON.parse(String(methodMismatch.state.body))).toMatchObject({ rpcId: 'rpc-bad', result: { ok: false, error: { code: 'bad-request' } }, }) for (const [request, status] of [ [fakeRequest(harnessHeaders, '/rpc/goals/create'), 404], [fakePost(harnessHeaders, '/outside/goals/create', {}), 404], [fakePost(harnessHeaders, '/rpc/goals//create', {}), 404], [fakeRawPost(harnessHeaders, '/rpc/goals/create', '{}'), 415], [fakeRawPost({ ...harnessHeaders, 'content-type': 'text/plain' }, '/rpc/goals/create', '{}'), 415], [fakeRawPost({ ...harnessHeaders, 'content-type': 'application/json; charset=utf-8' }, '/rpc/goals/create', '{'), 400], ] as const) { const response = fakeResponse() await route.handler(request, response.response) expect(response.state.status).toBe(status) } for (const [body, rpcId] of [ [{ rpcId: 'retained-id' }, 'retained-id'], [{ rpcId: 42 }, 'invalid-request'], [null, 'invalid-request'], ] as const) { const response = fakeResponse() await route.handler(fakePost(harnessHeaders, '/rpc/goals/create', body), response.response) expect(JSON.parse(String(response.state.body))).toMatchObject({ rpcId, result: { ok: false, error: { code: 'bad-request' } }, }) } const failed = fakeResponse() await route.handler(fakePost(harnessHeaders, '/rpc/fail', { type: 'client-request', rpcId: 'rpc-fail', method: 'fail', payload: {}, }), failed.response) expect(failed.state).toMatchObject({ status: 500, body: 'handler failure: Error: handler broke' }) expect(() => connection.rpc.handle('/api', async () => ({ ok: true, value: null }))) .toThrow('invalid or reserved RPC channel') expect(() => connection.rpc.handle('api3', async () => ({ ok: true, value: null }))) .toThrow('invalid or reserved RPC channel') await remove() await fiber.dispose() }) }) describe('connection node half over a real HTTP server', () => { /** Serve the registered prefix route from a real server and return its port. */ async function serve(routes: WebRoute[]): Promise<{ port: number; close: () => Promise }> { const server = createServer((request, response) => { void routes[0]!.handler(request, response) }) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const address = server.address() as AddressInfo return { port: address.port, close: () => new Promise((resolve, reject) => { server.close((error) => { if (error === undefined || error === null) resolve() else reject(error) }) }), } } /** One real request; `host` spoofs the authority the way a LAN client's browser would send it. */ function call(port: number, method: string, host: string, cookie?: string): Promise { return new Promise((resolve, reject) => { const request = httpRequest( { host: '127.0.0.1', port, path: `${API_PATH}/${method}`, method: 'GET', headers: { host, ...cookie === undefined ? {} : { cookie } }, }, (response) => { response.resume() response.on('end', () => { resolve(response.statusCode ?? 0) }) }, ) request.on('error', reject) request.end() }) } it('requires authentication uniformly over a real HTTP request', async () => { // A real IncomingMessage pins the exploit boundary: a client-controlled // Host naming loopback passes the rebinding fence but never authenticates. const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example'] }) const { port, close } = await serve(routes) try { const methods = [ 'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate', 'credentials.describe', 'credentials.set', 'credentials.unset', 'host.openPath', 'llm.discoverModels', 'agentPreset.openDocument', 'llm.providers', 'llm.models', ] for (const method of methods) { expect([method, await call(port, method, 'localhost')]).toEqual([method, 401]) expect([method, await call(port, method, 'harness.example')]).toEqual([method, 401]) } expect(await call(port, 'settings.describe', 'other.example')).toBe(403) const declaredCookie = browserCookie(connection, 'harness.example') for (const method of methods) { expect([method, await call(port, method, 'harness.example', declaredCookie)]).toEqual([method, 404]) } const loopbackAuthority = `127.0.0.1:${String(port)}` expect(await call( port, 'settings.describe', loopbackAuthority, browserCookie(connection, loopbackAuthority), )).toBe(404) } finally { await close() await dispose() } }) })