DSH / Atlas
2026-09-17implementedarchitecturearchived 2026-09-30

Feature routes follow the document base, and a gate keeps them there

功能路由遵循文档 base,并由门禁维持

[Web app-owned routes are document-relative](2026-09-14-web-document-relative-app-routes.md) makes the shell, its plugin bundles, and its streams reach one listener under any mount. Routes owned by feature packages still addressed the origin root: `/open-in-app/...`, the deliverables `present.*` and `changes.*` routes, the session-log export download, the upload worker's `/api/session/uploadFileBinary` post, and the

English

Problem

Web app-owned routes are document-relative makes the shell, its plugin bundles, and its streams reach one listener under any mount. Routes owned by feature packages still addressed the origin root: /open-in-app/..., the deliverables present.* and changes.* routes, the session-log export download, the upload worker's /api/session/uploadFileBinary post, and the /api/file route behind local markdown image paths. Under a prefix-stripping proxy each of those requests missed. Nothing stopped a new browser-face reference from binding the bundle to one mount again.

Decision

Every feature producer keeps its absolute registration key and derives the browser form beside it: OPEN_IN_APP_*_PATH/*_ROUTE, PRESENT_*_PATH/*_ROUTE, CHANGED_FILES_PATH and CHANGES_*_PATH/*_ROUTE, SESSION_LOG_EXPORT_PATH/_ROUTE, FILE_UPLOAD_PATH/_ROUTE. Browser code addresses only the *_ROUTE form, and a Fetch-shaped carrier a page installs (FileUploadFetch, RpcFetch) receives that relative route and resolves it against its own base.

Two consumers need an absolute URL and resolve against document.baseURI: the upload worker, whose own base is a blob: URL, and the markdown image vocabulary, which emits only absolute http(s), blob, or data destinations.

verify-client-route-resolution runs in hygiene and CI static checks over every browser source the client compiler face compiles. It governs request targets — the first argument of a request constructor, a fetch-shaped call, or a dynamic import; an assigned resource property; or a JSX src/href attribute — and rejects a root-absolute, protocol-relative, or absolute app route (api, plugins, open-in-app prefixes), and a relative app route resolved against a location read. A shared *_PATH/*_ENDPOINT key used as a request target must be stripped. The same pass checks the Host producers of browser references: a url, src, or href field stamped with a root-absolute app route is rejected, while route keys and response-table keys stay absolute and out of scope.

Alternatives considered

Banning every location read in browser code. Rejected: identity comparisons, the preview's own query read, and the worker tunnel's host-relative mapping are all legitimate. The gate governs request targets, which is where a mount-bound route appears.

Rewriting feature responses in the proxy. Rejected: it duplicates the mount in deployment configuration for each route the proxy does not know about, and the document already resolves relative references.

Consequences

The gate reads inline literal fragments only, so a composed reference (comboReference(...), artifact.url.slice(1)) is not traced; packages/client/modules/tests/node-half.client.spec.ts owns the regression signal for composed graph rows, batch descriptors, and the source-map trailer. Preview pages do not expose their worker's file API at the document URL, so markdown local-file image URLs cannot reach that Host. Session prose local media display owns the image vocabulary.

Testing

The gate's own spec pins a negative control per rule: interpolated targets (${origin}/api/file), protocol-relative and absolute forms, an unstripped shared key, a relative route resolved against a location read, and root-absolute producer fields. Each feature package's specs assert the relative route its browser half sends; markdown-images.e2e.ts keeps the local-image path covered end to end. The discovery spec pins which client projects contribute browser sources: a DOM project the Host aggregate also compiles contributes its plain src/ unless it has a src/client half.

中文

问题

Web 应用自有路由相对文档寻址让外壳、插件 bundle 与流在任一挂载下到达同一监听端口。功能包自有的路由仍寻址源站根:/open-in-app/...、deliverables 的 present.* 与 changes.* 路由、会话日志导出下载、上传 worker 的 /api/session/uploadFileBinary 提交,以及本地 markdown 图片路径背后的 /api/file 路由。在剥离前缀的代理之后,这些请求全部落空。也没有任何机制阻止新的浏览器侧引用再次把 bundle 绑定到单一挂载。

决策

每个功能生产者保留其绝对注册键,并在旁边派生浏览器形式:OPEN_IN_APP_*_PATH/*_ROUTE、PRESENT_*_PATH/*_ROUTE、CHANGED_FILES_PATH 与 CHANGES_*_PATH/*_ROUTE、SESSION_LOG_EXPORT_PATH/_ROUTE、FILE_UPLOAD_PATH/_ROUTE。浏览器代码只寻址 *_ROUTE 形式;页面安装的 Fetch 形载体(FileUploadFetch、RpcFetch)收到该相对路由,并相对自身基准解析。

两个消费方需要绝对 URL,并相对 document.baseURI 解析:上传 worker(其自身基准是 blob: URL)与 markdown 图片词汇表(只输出绝对的 http(s)、blob 或 data 目标)。

verify-client-route-resolution 在 hygiene 与 CI 静态检查中运行,覆盖 client 编译面编译的每个浏览器源文件。它治理请求目标——请求构造器、fetch 形调用或动态 import 的第一个参数;赋值的资源属性;JSX 的 src/href 属性——拒绝根绝对、协议相对或绝对的应用路由(api、plugins、open-in-app 前缀),以及相对 location 读取解析的相对应用路由。作为请求目标使用的共享 *_PATH/*_ENDPOINT 键必须先剥去前导斜杠。同一遍检查还覆盖宿主侧浏览器引用的生产者:以根绝对应用路由写入的 url、src 或 href 字段被拒绝,而路由键与响应表键保持绝对,不在范围内。

曾考虑的替代方案

在浏览器代码中禁止一切 location 读取。 否决:身份比较、preview 自身查询读取、worker tunnel 的宿主相对映射都属正当。门禁治理请求目标——绑定挂载点的路由真正出现的位置。

在代理中改写功能响应。 否决:这会为代理并不知晓的每条路由在部署配置里重复挂载点,而文档本就能解析相对引用。

后果

门禁只读取内联字面片段,因此组合出来的引用(comboReference(...)、artifact.url.slice(1))不被追踪;组合而成的启动图 row、批次描述符与 source-map trailer 的回归信号由 packages/client/modules/tests/node-half.client.spec.ts 持有。Preview 页面不会在文档 URL 下暴露其 worker 的文件 API,因此 markdown 本地文件图片 URL 无法到达该 Host。会话正文本地媒体展示持有图片词汇表。

测试

门禁自身的 spec 为每条规则固定了负向对照:插值目标(${origin}/api/file)、协议相对与绝对形式、未剥前导斜杠的共享键、相对路由相对 location 读取解析,以及生产者中根绝对的引用字段。各功能包的 spec 断言其浏览器半侧发出的相对路由;markdown-images.e2e.ts 端到端维持本地图片路径的覆盖。发现逻辑的 spec 固定了哪些 client 项目贡献浏览器源文件:同时被 Host 聚合编译的 DOM 项目,若没有 src/client 半边,则其整个 src/ 都计入。