mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(connection): register exact Fetch routes
This commit is contained in:
@@ -13,6 +13,8 @@ import { BrowserAuth } from './browser-auth.ts'
|
||||
import { HostConnectionService } from './rpc-host.ts'
|
||||
|
||||
export type {
|
||||
ConnectionFetchMethod,
|
||||
ConnectionFetchRoute,
|
||||
ConnectionIndexRequest,
|
||||
ConnectionIndexResponse,
|
||||
ConnectionRpcEndpointMatcher,
|
||||
@@ -22,6 +24,7 @@ export type {
|
||||
ConnectionRpcResult,
|
||||
ConnectionTrustRequest,
|
||||
HostConnectionHandle,
|
||||
HostConnectionFetch,
|
||||
HostConnectionRpc,
|
||||
} from './rpc.ts'
|
||||
export { HostConnectionService } from './rpc-host.ts'
|
||||
|
||||
@@ -17,6 +17,8 @@ import type { BrowserAuth } from './browser-auth.ts'
|
||||
import type {
|
||||
ConnectionIndexRequest,
|
||||
ConnectionIndexResponse,
|
||||
ConnectionFetchRoute,
|
||||
HostConnectionFetch,
|
||||
ConnectionRpcEndpointMatcher,
|
||||
ConnectionRpcHandler,
|
||||
ConnectionRpcResult,
|
||||
@@ -35,6 +37,11 @@ interface ConnectionRpcInterceptor {
|
||||
readonly fetchHandler: FetchHandler
|
||||
}
|
||||
|
||||
interface RegisteredFetchRoute {
|
||||
readonly methods: ReadonlySet<string>
|
||||
readonly fetch: ConnectionFetchRoute['fetch']
|
||||
}
|
||||
|
||||
interface ConnectionServerResponse {
|
||||
readonly type: 'server-response'
|
||||
readonly rpcId: RpcIdType
|
||||
@@ -51,6 +58,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
/** Host Connection service whose channel registrations belong to the caller fiber. */
|
||||
export class HostConnectionService extends Service implements HostConnectionHandle {
|
||||
private readonly interceptors = new Map<string, ConnectionRpcInterceptor>()
|
||||
private readonly fetchRoutes = new Map<string, RegisteredFetchRoute>()
|
||||
|
||||
/**
|
||||
* Provide the Host half over the active HTTP server.
|
||||
@@ -76,6 +84,14 @@ export class HostConnectionService extends Service implements HostConnectionHand
|
||||
}
|
||||
}
|
||||
|
||||
/** Exact Fetch-route registry scoped to the Context reading this service. */
|
||||
get fetch(): HostConnectionFetch {
|
||||
const owner = this.ctx
|
||||
return {
|
||||
register: route => this.registerFetchRoute(owner, route),
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply the configured Host/Origin fence, then browser authentication. */
|
||||
requestRejection(request: ConnectionTrustRequest): ConnectionRequestRejection {
|
||||
if (!isTrustedApiRequest(request, this.trustedHosts)) return 403
|
||||
@@ -104,7 +120,10 @@ export class HostConnectionService extends Service implements HostConnectionHand
|
||||
): FetchHandler {
|
||||
return {
|
||||
fetch: (request) => {
|
||||
const endpoint = endpointFromPath(channel, new URL(request.url).pathname)
|
||||
const pathname = new URL(request.url).pathname
|
||||
const route = this.fetchRoutes.get(pathname)
|
||||
if (route?.methods.has(request.method) === true) return route.fetch(request)
|
||||
const endpoint = endpointFromPath(channel, pathname)
|
||||
const interceptor = this.interceptors.get(channel)
|
||||
if (endpoint === undefined || interceptor === undefined || !interceptor.matches(endpoint)) {
|
||||
return fallback.fetch(request)
|
||||
@@ -114,6 +133,24 @@ export class HostConnectionService extends Service implements HostConnectionHand
|
||||
}
|
||||
}
|
||||
|
||||
private registerFetchRoute(
|
||||
owner: Context,
|
||||
route: ConnectionFetchRoute,
|
||||
): () => Promise<void> {
|
||||
assertFetchRoute(route)
|
||||
const registered: RegisteredFetchRoute = {
|
||||
methods: new Set(route.methods),
|
||||
fetch: route.fetch,
|
||||
}
|
||||
return owner.effect(() => {
|
||||
if (this.fetchRoutes.has(route.path)) {
|
||||
throw new Error(`connection: exact Fetch route ${JSON.stringify(route.path)} is already registered`)
|
||||
}
|
||||
this.fetchRoutes.set(route.path, registered)
|
||||
return () => { this.fetchRoutes.delete(route.path) }
|
||||
}, `client-connection: ${route.path} Fetch route`)
|
||||
}
|
||||
|
||||
private register(
|
||||
owner: Context,
|
||||
channel: string,
|
||||
@@ -246,3 +283,21 @@ function assertChannel(channel: string): void {
|
||||
throw new Error(`connection: invalid or reserved RPC channel ${JSON.stringify(channel)}`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertFetchRoute(route: ConnectionFetchRoute): void {
|
||||
if (endpointFromPath(API_PATH, route.path) === undefined) {
|
||||
throw new Error(`connection: invalid exact Fetch route ${JSON.stringify(route.path)}`)
|
||||
}
|
||||
if (route.methods.length === 0) {
|
||||
throw new Error(`connection: exact Fetch route ${JSON.stringify(route.path)} declares no methods`)
|
||||
}
|
||||
const methods = new Set(route.methods)
|
||||
if (methods.size !== route.methods.length) {
|
||||
throw new Error(`connection: exact Fetch route ${JSON.stringify(route.path)} repeats a method`)
|
||||
}
|
||||
for (const method of methods) {
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
throw new Error(`connection: exact Fetch route ${JSON.stringify(route.path)} has unsupported method ${JSON.stringify(method)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,29 @@ export type ConnectionRpcHandler = (
|
||||
/** Synchronous ownership test for one endpoint on a shared RPC channel. */
|
||||
export type ConnectionRpcEndpointMatcher = (endpoint: string) => boolean
|
||||
|
||||
/** HTTP methods supported by exact Fetch routes on the shared API channel. */
|
||||
export type ConnectionFetchMethod = 'GET' | 'HEAD'
|
||||
|
||||
/** One exact, transport-independent Fetch route owned by a Host feature. */
|
||||
export interface ConnectionFetchRoute {
|
||||
/** Absolute path below `/api`; query parameters remain available on the request URL. */
|
||||
readonly path: string
|
||||
/** Methods this route owns. Other methods continue through normal shared-channel dispatch. */
|
||||
readonly methods: readonly ConnectionFetchMethod[]
|
||||
/** Handle one request after the physical carrier has applied its trust and authentication policy. */
|
||||
readonly fetch: (request: Request) => Promise<Response>
|
||||
}
|
||||
|
||||
/** Host registry for exact Fetch routes that cannot use JSON Remote invocation. */
|
||||
export interface HostConnectionFetch {
|
||||
/**
|
||||
* Register one exact route on the shared API channel.
|
||||
* @param route - path, methods, and Fetch-shaped implementation.
|
||||
* @returns asynchronous disposer removing this exact contribution.
|
||||
*/
|
||||
register(route: ConnectionFetchRoute): () => Promise<void>
|
||||
}
|
||||
|
||||
/** Host registry for logical RPC channels carried by the current transport. */
|
||||
export interface HostConnectionRpc {
|
||||
/**
|
||||
@@ -74,6 +97,8 @@ export interface HostConnectionRpc {
|
||||
export interface HostConnectionHandle {
|
||||
/** Generic RPC channel registry. */
|
||||
readonly rpc: HostConnectionRpc
|
||||
/** Exact Fetch routes for streaming or browser-native responses. */
|
||||
readonly fetch: HostConnectionFetch
|
||||
|
||||
/**
|
||||
* Apply Connection's Host/Origin checks and browser authentication to
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { BrowserAuth } from '../src/browser-auth.ts'
|
||||
import { HostConnectionService } from '../src/rpc-host.ts'
|
||||
|
||||
async function mounted(): Promise<{
|
||||
readonly connection: HostConnectionService
|
||||
readonly dispose: () => Promise<void>
|
||||
}> {
|
||||
const ctx = new Context()
|
||||
const fiber = ctx.plugin((pluginCtx) => {
|
||||
new HostConnectionService(pluginCtx, [], {} as BrowserAuth)
|
||||
})
|
||||
await fiber.await()
|
||||
return {
|
||||
connection: ctx.get('connection') as HostConnectionService,
|
||||
dispose: () => fiber.dispose(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('Connection exact Fetch routes', () => {
|
||||
it('dispatches owned methods before the transitional fallback', async () => {
|
||||
const { connection, dispose: disposeFiber } = await mounted()
|
||||
const route = vi.fn(async (request: Request) =>
|
||||
Response.json({ query: new URL(request.url).searchParams.get('sessionId') }))
|
||||
const fallback = vi.fn(async () => new Response('fallback', { status: 418 }))
|
||||
const dispose = connection.fetch.register({
|
||||
path: '/api/session.export',
|
||||
methods: ['GET', 'HEAD'],
|
||||
fetch: route,
|
||||
})
|
||||
const shared = connection.createSharedFetchHandler('/api', { fetch: fallback })
|
||||
|
||||
const response = await shared.fetch(new Request(
|
||||
'http://host/api/session.export?sessionId=session-1',
|
||||
))
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({ query: 'session-1' })
|
||||
expect(route).toHaveBeenCalledOnce()
|
||||
expect(fallback).not.toHaveBeenCalled()
|
||||
|
||||
const post = await shared.fetch(new Request('http://host/api/session.export', { method: 'POST' }))
|
||||
expect(post.status).toBe(418)
|
||||
expect(fallback).toHaveBeenCalledOnce()
|
||||
|
||||
await dispose()
|
||||
const withdrawn = await shared.fetch(new Request('http://host/api/session.export'))
|
||||
expect(withdrawn.status).toBe(418)
|
||||
expect(fallback).toHaveBeenCalledTimes(2)
|
||||
await disposeFiber()
|
||||
})
|
||||
|
||||
it('rejects invalid and duplicate registrations', async () => {
|
||||
const { connection, dispose: disposeFiber } = await mounted()
|
||||
const fetch = async (): Promise<Response> => new Response()
|
||||
|
||||
expect(() => connection.fetch.register({ path: '/outside', methods: ['GET'], fetch }))
|
||||
.toThrow('invalid exact Fetch route')
|
||||
expect(() => connection.fetch.register({ path: '/api/session.export', methods: [], fetch }))
|
||||
.toThrow('declares no methods')
|
||||
expect(() => connection.fetch.register({
|
||||
path: '/api/session.export', methods: ['GET', 'GET'], fetch,
|
||||
})).toThrow('repeats a method')
|
||||
expect(() => connection.fetch.register({
|
||||
path: '/api/session.export', methods: ['POST' as 'GET'], fetch,
|
||||
})).toThrow('unsupported method')
|
||||
|
||||
const dispose = connection.fetch.register({
|
||||
path: '/api/session.export', methods: ['GET'], fetch,
|
||||
})
|
||||
expect(() => connection.fetch.register({
|
||||
path: '/api/session.export', methods: ['HEAD'], fetch,
|
||||
})).toThrow('already registered')
|
||||
await dispose()
|
||||
expect(() => connection.fetch.register({
|
||||
path: '/api/session.export', methods: ['HEAD'], fetch,
|
||||
})).not.toThrow()
|
||||
await disposeFiber()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user