Files
deepseek-harness/packages/host/frontend-static/README.md
T
imccyu 73931626be feat(webserver): add resource routes and in-process readResource (#6901)
* feat(webserver): add resource routes and in-process readResource

A webserver route or the fallback seat may now answer through a resource,
a function from a Fetch Request to a Response, besides the existing
node:http handler. register accepts either route form and registerFallback
accepts a handler or { resource }; existing handler callers are unchanged.
HTTP GET and HEAD requests reach a resource; webServer.readResource(request,
peer) follows the same match order and reaches only resources, so handler
routes stay unreachable to in-process readers.

client-modules registers /plugins as the fetchBundle resource, and
frontend-static claims the fallback seat with a resource that renders the
index for a supplied Peer, or runs Connection's existing authorizeIndex for
an HTTP request and renders for the browser Peer.

* fix(webserver): flush resource headers and keep one drain listener per response

Resource responses send their status and headers before the first body
chunk, register one drain/close listener pair per response so the gzip
middleware's zlib stream does not accumulate listeners across backpressure
waits, and readResource drops the body of a HEAD read as HTTP does. HTTP
requests and readResource share one responder lookup.

* chore(scripts): prune the retired client-modules unknown-cast baseline entry

* feat(frontend-static): let browsers revalidate dist files except the index

Every dist file other than the rendered index now carries
cache-control: no-cache and a weak ETag from a SHA-1 of its bytes; a
request whose If-None-Match equals that ETag receives an empty 304. The
index keeps sending no cache headers. In-process reads through
webServer.readResource revalidate the same way.

* fix(webserver): stop reading Host for resource URLs and copy no held body

A resource request's URL is the request target resolved against the same
placeholder origin the route match uses, so a missing or unparsable Host no
longer turns a dist or /plugins request into 400. The webserver ends a
resource response itself and treats any earlier close as an abort, which
also covers the Web Worker tunnel marking an aborted response ended first.
client-modules bundle bodies and frontend-static file bodies are the held
bytes themselves instead of copies, and frontend-static no longer exports
its internal serveStatic helper.

* docs(webserver): document resource route types and the handler-or-resource fallback

Paste WebResource and WebResourceRoute beside WebRoute on the web-server
subsystem page and link the catalog signatures to it, replace the remaining
'fallback handler' wording now that the seat takes a handler or a resource,
and note why the drain listener removal is a no-op under gzip.
2026-10-22 13:41:18 +08:00

7.6 KiB

description, kind
description kind
SPA dist server for the Web shell: claims the webserver fallback seat and serves the built frontend with traversal rejection and SPA index fallback. package-reference

@deepseek-ai/dsh-host-frontend-static

English | 中文

Summary

Serve the built Web shell to browsers from its configured distribution directory. The root and configured index path render the bootstrapped index; existing assets are served directly, while missing or non-file paths return 404, traversal returns 403, and unsupported methods return 405. Index access requires a valid process token or browser cookie, but static assets remain public. Only one instance can handle unmatched routes at a time; a second activation fails, and unloading the active instance makes unmatched requests return 404.

Table of Contents


Use this package

Compose this plugin in a browser-facing host that serves the built Web shell: it claims the webserver's fallback seat and answers every request no named route matches. It needs one config value — where the built frontend's index.html lives.

Minimal configuration

- name: '@deepseek-ai/dsh-host-frontend-static'
  config:
    distIndex: /absolute/path/to/dist/index.html

distIndex is an assembly fact of the composing application: dsh-web-app resolves it through the frontend package's exports and mounts this plugin; a deployment never hardcodes it.

What the server enforces

Requests are served from the dist root (the directory containing distIndex). The dist root and the configured index path render index.html with HTTP 200; any other existing file is served directly with its MIME type, and unknown extensions ship as application/octet-stream. A path that resolves outside the root is rejected with 403, so a crafted path cannot read files above the dist. An absent or non-file target inside the dist root — a missing file, a directory, or a missing configured index — returns an empty 404. Non-GET/HEAD requests without a matching named route are answered 405. The fallback is a webserver resource. Every successful index response is rendered through the webserver's renderIndex — for connection.browserPeer, the Peer Connection's browser authentication admits, over HTTP, and for peer in a webServer.readResource(request, peer) read, which skips browser authentication — so the boot manifest and that Peer's access snapshot reach the page on / and on the configured index path.

The served HTML carries one document base, <base href="./">, ahead of every injected resource row, so it freezes the entry directory the page was loaded from: the shell's own app-directory-relative references and the Host's plugin-resource rows both resolve under the mount that served the page. The same index therefore serves the origin root and whatever mount a prefix-stripping proxy owns; this plugin renders it only for the dist root and the configured index path.

HTTP root and configured-index requests call ctx.connection.authorizeIndex before reading HTML. A valid process token receives a 303 redirect plus the persistent browser cookie; an existing valid cookie serves the index; every other index request receives the Connection-owned 401 response. Non-index files remain public static assets. Connection owns the token, cookie, expiry, and signing-record semantics.

Browsers revalidate every served file except the index. Such a response carries cache-control: no-cache and a weak ETag built from a SHA-1 of the file's bytes, so a file whose content changes gets a new ETag even when its name stays the same; a request whose If-None-Match equals the current ETag receives an empty 304. The rendered index carries no cache headers.

Observable failures

Traversal returns 403 rather than an error page. An absent or non-file target inside the dist root returns an empty 404, so a stale link or a mistyped pathname is an explicit failure rather than a silent SPA fallback. Claiming the seat twice throws, and while the seat is unclaimed the webserver answers 404 — which is what a browser sees if this plugin's fiber is disposed.


Understand the implementation

Implementation internals — click to expand

Design concept

The package is one function plugin around serveStatic, which maps a Fetch Request to a Response: apply resolves the dist root from distIndex and registers a fallback resource under an effect scope. For each request the resource passes serveStatic an authorizeIndex closure, which runs ctx.connection.authorizeIndex only when no Peer came with the request, and a renderIndex closure that runs ctx.webServer.renderIndex over the raw index.html for that Peer or the browser Peer and splices the document base in after the opening head tag (after the raw taps, so it precedes their markup too). The seat is single-owner by the webserver's contract — a second registration throws — and effect-scoped, so disposing the fiber releases the seat.

The traversal fence

serveStatic normalizes the requested pathname and joins it to the dist root, then requires the target to be the root itself or stay under it. The check uses sep rather than / because resolve() emits backslash paths on Windows, where a / suffix would reject every legitimate subpath as traversal.

Source map

File Role
src/index.ts serveStatic and apply: fallback resource, traversal rejection, index authorization and rendering, revalidation, MIME table

Further Exploration

Read these when the serving contract is not enough: the seat owner's contract, then the composition that resolves the dist and the subsystem reference.


Model Experience

None, as the SPA dist server answers browser asset requests and registers nothing model-facing.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

These limits define when a served asset class is not yet covered. They are current package constraints, not a task backlog.

  • The starter MIME table is minimal — it covers the Vite-emitted asset set plus the shipped PWA manifest; other extensions fall back to application/octet-stream until an asset class ships.
  • Pathname routing is explicit — the current client enters through the root or configured index path and has no History API pathname routes. Adding one requires an explicit server rule and real-composition coverage rather than a broad fallback for every miss.

Dev Note

Working context for maintainers — click to expand

None.