Files
deepseek-harness/packages/client/connection/src/index.ts
T

132 lines
5.0 KiB
TypeScript

/** Host HTTP bridge for browser-client RPC. */
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-attachment'
import type {} from '@deepseek-ai/dsh-credentials'
// Activates the webServer Context merge used below.
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
import { API_PATH } from './api-path.ts'
import { bridge, DEFAULT_MAX_REQUEST_BODY_BYTES } from './http-bridge.ts'
import { assertTrustedAuthority } from './api-request-trust.ts'
import { BrowserAuth } from './browser-auth.ts'
import { HostConnectionService } from './rpc-host.ts'
export type {
ConnectionFetchMethod,
ConnectionFetchHandler,
ConnectionFetchRoute,
ConnectionIndexRequest,
ConnectionIndexResponse,
ConnectionRpcEndpointMatcher,
ConnectionRpcFailure,
ConnectionRpcHandler,
ConnectionRequestRejection,
ConnectionRpcResult,
ConnectionTrustRequest,
ClientRequest,
HostConnectionHandle,
HostConnectionFetch,
HostConnectionRpc,
RpcMessage,
ServerResponse,
} from './rpc.ts'
export { RpcId, transportError } from './rpc.ts'
export {
clientRequestSchema,
rpcErrorSchema,
rpcIdSchema,
rpcMessageSchema,
rpcResultSchema,
serverResponseSchema,
} from './rpc-schema.ts'
export { HostConnectionService } from './rpc-host.ts'
export { API_PATH } from './api-path.ts'
/** Stable Cordis plugin name. */
export const name = 'client-connection'
/** Headroom for RPC JSON fields around aggregate base64 image payloads. */
const REQUEST_ENVELOPE_HEADROOM_BYTES = 1024 * 1024
function assertImageBodyCapacity(ctx: Context, maxRequestBodyBytes: number): void {
const attachments = ctx.get('attachments')
if (attachments === undefined) return
const requiredImageBodyBytes = Math.ceil(
attachments.imageLimits.maxMessageImageBytes * 4 / 3,
) + REQUEST_ENVELOPE_HEADROOM_BYTES
if (maxRequestBodyBytes < requiredImageBodyBytes) {
throw new Error(
`client-connection maxRequestBodyBytes (${String(maxRequestBodyBytes)}) must be at least `
+ `${String(requiredImageBodyBytes)} for the configured aggregate image limit`,
)
}
}
/** Services required before providing Connection. */
export const inject = ['webServer', 'credentials']
/** 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 Web runtime derives LAN IP literals from an active all-interface
* bind. An entry that is not a bare, canonical authority fails plugin load.
*/
trustedHosts?: string[]
/** Absolute browser-session lifetime in days. Default: 30. */
cookieMaxAgeDays?: number
/** Maximum buffered JSON body for every `/api` request. Default: 300 MiB. */
maxRequestBodyBytes?: number
}
export const Config: z<ConnectionConfig> = z.object({
trustedHosts: z.array(String).default([]),
cookieMaxAgeDays: z.natural().min(1).default(30),
maxRequestBodyBytes: z.natural().min(1).default(DEFAULT_MAX_REQUEST_BODY_BYTES),
})
/**
* Mounts the API gateway under the browser transport prefix. Every request on
* the prefix passes the Host/Origin browser-trust fence and persistent browser
* authentication before dispatch.
* @param ctx - Host plugin context.
* @param config - resolved plugin config (schema defaults applied).
*/
export async function apply(ctx: Context, config?: ConnectionConfig): Promise<void> {
// The Loader resolves schema defaults; hand-built test contexts may pass none.
const trustedHosts = config?.trustedHosts ?? []
const cookieMaxAgeDays = config?.cookieMaxAgeDays ?? 30
const maxRequestBodyBytes = config?.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES
// 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)
assertImageBodyCapacity(ctx, maxRequestBodyBytes)
const connection = new HostConnectionService(
ctx,
trustedHosts,
await BrowserAuth.create(ctx.root, ctx.credentials, cookieMaxAgeDays),
)
const fetchHandler = connection.createSharedFetchHandler(API_PATH)
const route: WebRoute = {
kind: 'prefix',
path: API_PATH,
handler: async (req, res) => {
const rejection = connection.requestRejection(req)
if (rejection !== undefined) {
res.writeHead(rejection)
res.end(rejection === 401 ? 'unauthorized' : 'forbidden')
return
}
await bridge(req, res, fetchHandler, maxRequestBodyBytes)
},
}
ctx.effect(() => ctx.webServer.register(route), 'client-connection: /api route')
ctx.inject(['attachments'], (attachmentCtx) => {
assertImageBodyCapacity(attachmentCtx, maxRequestBodyBytes)
})
}