index.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. /** Host HTTP bridge for browser-client RPC. */
  2. import type { Context } from 'cordis'
  3. import z from 'schemastery'
  4. // Activates the httpServer Context merge used below.
  5. import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
  6. import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
  7. import { API_PATH } from './api-path.ts'
  8. import { bridge } from './http-bridge.ts'
  9. import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts'
  10. export { API_PATH } from './api-path.ts'
  11. /** Stable Cordis plugin name. */
  12. export const name = 'client-connection'
  13. /** Services required before mounting the route. */
  14. export const inject = ['httpServer', 'apiProxy']
  15. /** Plugin config: the deployment's non-loopback serving authorities. */
  16. export interface ConnectionConfig {
  17. /**
  18. * Authorities this deployment serves beyond loopback: exact `host:port`, or
  19. * port-less `host` matching any port. The /api trust fence refuses any
  20. * request whose Host is neither loopback nor listed here, so a
  21. * non-loopback (`0.0.0.0`) deployment must declare the names it is reached
  22. * by (the dsh CLI derives the machine's LAN IP literals itself). An entry
  23. * that is not a bare, canonical authority fails the plugin load.
  24. */
  25. trustedHosts?: string[]
  26. }
  27. export const Config: z<ConnectionConfig> = z.object({
  28. trustedHosts: z.array(String).default([]),
  29. })
  30. /**
  31. * Mounts the API gateway under the browser transport prefix. Every request on
  32. * the prefix passes the browser-trust fence first (DNS-rebinding and
  33. * cross-site defense — [api-request-trust](./api-request-trust.ts)).
  34. * @param ctx - Host plugin context.
  35. * @param config - resolved plugin config (schema defaults applied).
  36. */
  37. export function apply(ctx: Context, config?: ConnectionConfig): void {
  38. // The Loader resolves schema defaults; hand-built test contexts may pass none.
  39. const trustedHosts = config?.trustedHosts ?? []
  40. // Config boundary: a malformed entry fails the load loudly here rather than
  41. // silently authorizing its hostname prefix at request time.
  42. for (const entry of trustedHosts) assertTrustedAuthority(entry)
  43. const apiHandler = toFetchHandler(ctx.apiProxy)
  44. const route: WebRoute = {
  45. kind: 'prefix',
  46. path: API_PATH,
  47. handler: async (req, res) => {
  48. if (!isTrustedApiRequest(req, trustedHosts)) {
  49. res.writeHead(403)
  50. res.end('forbidden')
  51. return
  52. }
  53. await bridge(req, res, apiHandler)
  54. },
  55. }
  56. ctx.effect(() => ctx.httpServer.register(route), 'client-connection: /api route')
  57. }