@anionex/dsh-eye-care 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ /** Preserve the loopback-only RPC boundary after DSH removed channel authority. */
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type {} from '@deepseek-ai/dsh-host-webserver'
5
+ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
6
+ import { EYE_CARE_RPC_CHANNEL, EYE_CARE_RPC_READ, EYE_CARE_RPC_SAVE } from './shared.ts'
7
+
8
+ export function loopbackAuthority(authority: string | undefined): boolean {
9
+ if (!authority) return false
10
+ try {
11
+ const host = new URL(`http://${authority}`).hostname
12
+ return host === 'localhost' || host === '[::1]' || /^127\.\d+\.\d+\.\d+$/.test(host)
13
+ } catch { return false }
14
+ }
15
+
16
+ /** The official Connection still owns authentication and the Host/Origin fence. */
17
+ export function installLoopbackTransport(ctx: Context, handle: ConnectionRpcHandler): void {
18
+ ctx.inject(['webServer'], web => {
19
+ web.effect(() => web.webServer.register({ kind: 'prefix', path: EYE_CARE_RPC_CHANNEL, handler: async (req: IncomingMessage, res: ServerResponse) => {
20
+ const reject = ctx.connection.requestRejection(req)
21
+ if (reject !== undefined || !loopbackAuthority(req.headers.host)) {
22
+ res.writeHead(reject ?? 403).end('forbidden')
23
+ return
24
+ }
25
+ const endpoint = new URL(req.url ?? '/', 'http://localhost').pathname.slice(EYE_CARE_RPC_CHANNEL.length + 1)
26
+ if (req.method !== 'POST' || ![EYE_CARE_RPC_READ, EYE_CARE_RPC_SAVE].includes(endpoint)) {
27
+ res.writeHead(404).end('not found'); return
28
+ }
29
+ if (req.headers['content-type']?.split(';')[0]?.trim() !== 'application/json') {
30
+ res.writeHead(415).end('content type must be application/json'); return
31
+ }
32
+ let size = 0
33
+ const chunks: Buffer[] = []
34
+ try {
35
+ for await (const chunk of req) {
36
+ const bytes = Buffer.from(chunk as Uint8Array)
37
+ size += bytes.length
38
+ if (size > 65536) { res.writeHead(413).end('request too large'); return }
39
+ chunks.push(bytes)
40
+ }
41
+ const message: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
42
+ if (message === null || typeof message !== 'object' || !('type' in message) || message.type !== 'client-request'
43
+ || !('rpcId' in message) || typeof message.rpcId !== 'string' || message.rpcId.length === 0
44
+ || !('method' in message) || message.method !== endpoint || !('payload' in message)) {
45
+ res.writeHead(400).end('invalid request'); return
46
+ }
47
+ const abort = new AbortController()
48
+ res.once('close', () => abort.abort())
49
+ const result = await handle(endpoint, message.payload, abort.signal, ctx.connection.operator)
50
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
51
+ res.end(JSON.stringify({ type: 'server-response', rpcId: message.rpcId, result }))
52
+ } catch {
53
+ if (!res.headersSent) res.writeHead(400).end('invalid request')
54
+ }
55
+ } }), 'dsh-eye-care: loopback RPC')
56
+ })
57
+ }