Files
deepseek-harness/packages/client/connection/tests/node-half.spec.ts
T
Yichen Jiang 191067559e feat(apiproxy): settings/credentials/llm wire domains, frames, and write guard
Eight compiler-locked methods: settings.describe/update/replace serve
redacted layered namespace views (secrets structurally absent from every
layer, write-only in the update direction) and fold seam refusals into
settings-rejected; credentials.describe/set/unset expose value-free views
with credential-rejected on shadowed writes; llm.providers merges the
configurable directory with live routes and llm.models claims the
host-scoped catalog reservation through the buildModelCatalog extraction
session.models now shares. Three HostFrame invalidations bridge the seam
events (host/settings-changed, host/credentials-changed,
host/models-changed), and the connection route generalizes the native-
dialog check into a privileged-method set covering all four writes. The
fixture and both fake clients grow the same face.
2026-07-30 00:13:12 +08:00

105 lines
4.1 KiB
TypeScript

/** Node half: registers the /api prefix route bridging to the api gateway. */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { HttpServerService, WebRoute } from '@deepseek-ai/dsh-host-webserver'
import { API_PATH, apply, inject } from '../src/index.ts'
describe('connection node half', () => {
it('registers the /api prefix route and removes it with the fiber', async () => {
const ctx = new Context()
const routes: WebRoute[] = []
// Structural fake: the plugin only touches register(); the service class
// carries private state a literal cannot (and need not) reproduce.
const httpServer: Pick<HttpServerService, 'register' | 'tapIndex' | 'port'> = {
register(route) {
routes.push(route)
return () => { routes.splice(routes.indexOf(route), 1) }
},
tapIndex: () => () => {},
port: 0,
}
ctx.provide('httpServer', httpServer as HttpServerService)
ctx.provide('apiProxy', {} as unknown as ApiProxy)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(routes).toHaveLength(1)
expect(routes[0]).toMatchObject({ kind: 'prefix', path: API_PATH })
// The privileged set: native dialogs plus every settings/credential write.
// A non-loopback peer is denied even with same-origin headers.
for (const url of [
'/api/host.pickDirectory', '/api/host.openPath',
'/api/settings.update', '/api/settings.replace',
'/api/credentials.set', '/api/credentials.unset',
]) {
let status: number | undefined
let body: unknown
const deniedRequest = {
url,
headers: {
host: 'harness.example', origin: 'http://harness.example', 'sec-fetch-site': 'same-origin',
},
socket: { remoteAddress: '192.168.1.8' },
} as unknown as IncomingMessage
const deniedResponse = {
writeHead(value: number) { status = value; return this },
end(value?: unknown) { body = value; return this },
} as unknown as ServerResponse
await routes[0]!.handler(deniedRequest, deniedResponse)
expect(status).toBe(403)
expect(body).toBe('forbidden')
}
await fiber.dispose()
expect(routes).toHaveLength(0)
})
it('leaves reads and unprivileged methods to the bridge under the same untrusted peer', async () => {
const ctx = new Context()
const routes: WebRoute[] = []
const httpServer: Pick<HttpServerService, 'register' | 'tapIndex' | 'port'> = {
register(route) {
routes.push(route)
return () => { routes.splice(routes.indexOf(route), 1) }
},
tapIndex: () => () => {},
port: 0,
}
ctx.provide('httpServer', httpServer as HttpServerService)
// The bridge parses the request before the (empty) impl is consulted; a
// carrier-level 404/parse outcome proves the guard did not intercept.
ctx.provide('apiProxy', {} as unknown as ApiProxy)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
let status: number | undefined
const request = {
url: '/api/settings.describe',
method: 'POST',
headers: {
host: 'harness.example', origin: 'http://harness.example', 'sec-fetch-site': 'same-origin',
},
socket: { remoteAddress: '192.168.1.8' },
// Minimal async-iterable face for the bridge's body assembly.
async *[Symbol.asyncIterator]() {
yield Buffer.from('not json')
},
} as unknown as IncomingMessage
const response = {
writeHead(value: number) { status = value; return this },
setHeader() { return this },
end() { return this },
write() { return true },
on() { return this },
} as unknown as ServerResponse
await routes[0]!.handler(request, response)
// 400 (body is not JSON) comes from the carrier, not the 403 guard: the
// read passed the privileged check and reached the fetch handler.
expect(status).toBe(400)
await fiber.dispose()
})
})