mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
132 lines
5.0 KiB
TypeScript
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)
|
|
})
|
|
}
|