191067559e
Eight compiler-locked methods: settings.describe/update/replace serve redacted layered namespace views (secrets structurally absent from every layer, write-only in the update direction) and fold seam refusals into settings-rejected; credentials.describe/set/unset expose value-free views with credential-rejected on shadowed writes; llm.providers merges the configurable directory with live routes and llm.models claims the host-scoped catalog reservation through the buildModelCatalog extraction session.models now shares. Three HostFrame invalidations bridge the seam events (host/settings-changed, host/credentials-changed, host/models-changed), and the connection route generalizes the native- dialog check into a privileged-method set covering all four writes. The fixture and both fake clients grow the same face.
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
/** Host HTTP bridge for browser-client RPC. */
|
|
import type { Context } from 'cordis'
|
|
// Activates the httpServer Context merge used below.
|
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
|
|
import { API_PATH } from './api-path.ts'
|
|
import { bridge } from './http-bridge.ts'
|
|
import { isTrustedNativeDialogRequest } from './native-dialog-request.ts'
|
|
|
|
export { API_PATH } from './api-path.ts'
|
|
|
|
/** Stable Cordis plugin name. */
|
|
export const name = 'client-connection'
|
|
|
|
/** Services required before mounting the route. */
|
|
export const inject = ['httpServer', 'apiProxy']
|
|
|
|
/**
|
|
* Methods gated on the trusted same-origin loopback check. Native dialogs act
|
|
* on the host machine; settings and credential writes mutate the user's
|
|
* configuration and secret store. Under `--host 0.0.0.0` every other method
|
|
* is reachable LAN-wide, but these stay browser-same-origin-on-loopback until
|
|
* a real authentication layer exists.
|
|
*/
|
|
const PRIVILEGED_METHODS = new Set([
|
|
'host.pickDirectory',
|
|
'host.openPath',
|
|
'settings.update',
|
|
'settings.replace',
|
|
'credentials.set',
|
|
'credentials.unset',
|
|
])
|
|
|
|
/**
|
|
* Mounts the API gateway under the browser transport prefix.
|
|
* @param ctx - Host plugin context.
|
|
*/
|
|
export function apply(ctx: Context): void {
|
|
const apiHandler = toFetchHandler(ctx.apiProxy)
|
|
const route: WebRoute = {
|
|
kind: 'prefix',
|
|
path: API_PATH,
|
|
handler: async (req, res) => {
|
|
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
|
|
if (pathname.startsWith(`${API_PATH}/`)
|
|
&& PRIVILEGED_METHODS.has(pathname.slice(API_PATH.length + 1))
|
|
&& !isTrustedNativeDialogRequest(req)) {
|
|
res.writeHead(403)
|
|
res.end('forbidden')
|
|
return
|
|
}
|
|
await bridge(req, res, apiHandler)
|
|
},
|
|
}
|
|
ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
|
|
}
|