返回源码地图

packages/client/connection/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 15;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Host HTTP bridge for browser-client RPC. */
2import type { Context } from '@deepseek-ai/cordis'
3import type { IncomingMessage, ServerResponse } from 'node:http'
4import z from '@deepseek-ai/schemastery'
5import type {} from '@deepseek-ai/dsh-attachment'
6import type {} from '@deepseek-ai/dsh-credentials'
7// Activates the webServer Context merge used below.
8import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
9import { API_PATH } from './api-path.ts'
10import { bridge, DEFAULT_MAX_REQUEST_BODY_BYTES } from './http-bridge.ts'
11import { assertTrustedAuthority } from './api-request-trust.ts'
12import { BrowserAuth } from './browser-auth.ts'
13import { HostConnectionService } from './rpc-host.ts'
14import { ConnectionRecoveryConfigSchema, resolveConnectionConfig, type ConnectionRecoveryConfig } from './recovery-config.ts'
15
16export type {
17 PeerAdmission,
18 ConnectionFetchMethod,
19 ConnectionFetchHandler,
20 ConnectionFetchRoute,
21 ConnectionIndexRequest,
22 ConnectionIndexResponse,
23 ConnectionRpcEndpointMatcher,
24 ConnectionRpcAttachment,
25 ConnectionRpcFailure,
26 ConnectionRpcHandler,
27 ConnectionRpcHandlerResult,
28 ConnectionRequestRejection,
29 ConnectionRpcResult,
30 ConnectionRequestBodyMode,
31 ConnectionTrustRequest,
32 ClientRequest,
33 HostConnectionHandle,
34 HostConnectionFetch,
35 HostConnectionRpc,
36 RpcMessage,
37 ServerResponse,
38} from './rpc.ts'
39export type { PeerId, PeerScope, RemoteInvocation } from '@deepseek-ai/dsh-typert-protocol'
40export { RpcId, transportError } from './rpc.ts'
41export { OperatorPeer } from './operator-peer.ts'
42export {
43 clientRequestSchema,
44 rpcErrorSchema,
45 rpcIdSchema,
46 rpcMessageSchema,
47 rpcResultSchema,
48 serverResponseSchema,
49} from './rpc-schema.ts'
50export { HostConnectionService } from './rpc-host.ts'
51
52export { API_PATH } from './api-path.ts'
53
54/** Stable Cordis plugin name. */
55export const name = 'client-connection'
56
57declare module '@deepseek-ai/cordis' {
58 interface Events {
59 /**
60 * Admit or wrap an authenticated shared API request, including body transfer.
61 * Existing requests continue when a listener refuses subsequent requests.
62 * @param request - Authenticated incoming HTTP request.
63 * @param response - Response owned until the delegated bridge settles.
64 * @param next - Delegate to the next listener or the shared API bridge.
65 * @mode waterfall
66 */
67 'connection/request'(request: IncomingMessage, response: ServerResponse, next: () => Promise<void>): Promise<void>
68 }
69}
70
71/** Headroom for RPC JSON fields around aggregate base64 image payloads. */
72const REQUEST_ENVELOPE_HEADROOM_BYTES = 1024 * 1024
73
74function assertImageBodyCapacity(ctx: Context, maxRequestBodyBytes: number): void {
75 const attachments = ctx.get('attachments')
76 if (attachments === undefined) return
77 const requiredImageBodyBytes = Math.ceil(
78 attachments.imageLimits.maxMessageImageBytes * 4 / 3,
79 ) + REQUEST_ENVELOPE_HEADROOM_BYTES
80 if (maxRequestBodyBytes < requiredImageBodyBytes) {
81 throw new Error(
82 `client-connection maxRequestBodyBytes (${String(maxRequestBodyBytes)}) must be at least `
83 + `${String(requiredImageBodyBytes)} for the configured aggregate image limit`,
84 )
85 }
86}
87
88/** Services required before providing Connection. */
89export const inject = ['credentials']
90
91/** Browser authentication, request limits, and connection recovery configuration. */
92export interface ConnectionConfig {
93 /** Browser recovery timing, injected into each served page. */
94 recovery?: ConnectionRecoveryConfig
95 /**
96 * Authorities this deployment serves beyond loopback: exact `host:port`, or
97 * port-less `host` matching any port. The /api trust fence refuses any
98 * request whose Host is neither loopback nor listed here, so a
99 * non-loopback (`0.0.0.0`) deployment must declare the names it is reached
100 * by; the Web runtime derives LAN IP literals from an active all-interface
101 * bind. An entry that is not a bare, canonical authority fails plugin load.
102 */
103 trustedHosts?: string[]
104 /** Absolute browser-session lifetime in days. Default: 30. */
105 cookieMaxAgeDays?: number
106 /** Maximum buffered JSON body for every `/api` request. Default: 300 MiB. */
107 maxRequestBodyBytes?: number
108}
109
110export const Config: z<ConnectionConfig> = z.object({
111 recovery: ConnectionRecoveryConfigSchema.default({}),
112 trustedHosts: z.array(String).default([]),
113 cookieMaxAgeDays: z.natural().min(1).default(30),
114 maxRequestBodyBytes: z.natural().min(1).default(DEFAULT_MAX_REQUEST_BODY_BYTES),
115})
116
117/**
118 * Provides carrier-neutral RPC and Fetch registries. When `webServer` is
119 * present, the plugin also mounts the `/api` browser transport with Host/Origin
120 * checks and persistent browser authentication.
121 * @param ctx - Host plugin context.
122 * @param config - resolved plugin config (schema defaults applied).
123 */
124export async function apply(ctx: Context, config?: ConnectionConfig): Promise<void> {
125 const recovery = resolveConnectionConfig(config?.recovery)
126 // The Loader resolves schema defaults; hand-built test contexts may pass none.
127 const trustedHosts = config?.trustedHosts ?? []
128 const cookieMaxAgeDays = config?.cookieMaxAgeDays ?? 30
129 const maxRequestBodyBytes = config?.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES
130 // Config boundary: a malformed entry fails the load loudly here rather than
131 // silently authorizing its hostname prefix at request time.
132 for (const entry of trustedHosts) assertTrustedAuthority(entry)
133 assertImageBodyCapacity(ctx, maxRequestBodyBytes)
134 const connection = new HostConnectionService(
135 ctx,
136 trustedHosts,
137 await BrowserAuth.create(ctx.root, ctx.credentials, cookieMaxAgeDays),
138 )
139 ctx.inject(['webServer'], (webCtx) => {
140 assertImageBodyCapacity(webCtx, maxRequestBodyBytes)
141 webCtx.on('webserver/index-inject', (table) => {
142 table.push({ kind: 'global', name: '__DSH_CONNECTION_RECOVERY__', value: recovery })
143 })
144 const fetchHandler = connection.createSharedFetchHandler(API_PATH)
145 const route: WebRoute = {
146 kind: 'prefix',
147 path: API_PATH,
148 handler: async (req, res) => {
149 const admission = connection.admit(req)
150 if ('rejection' in admission) {
151 res.writeHead(admission.rejection)
152 res.end(admission.rejection === 401 ? 'unauthorized' : 'forbidden')
153 return
154 }
155 await webCtx.waterfall('connection/request', req, res, () => bridge(req, res, fetchHandler, maxRequestBodyBytes))
156 },
157 }
158 webCtx.effect(() => webCtx.webServer.register(route), 'client-connection: /api route')
159 })
160 ctx.inject(['attachments'], (attachmentCtx) => {
161 assertImageBodyCapacity(attachmentCtx, maxRequestBodyBytes)
162 })
163}