A plugin whose injection list names a service nobody provides is held pending by Cordis: the module loads, but the fiber never mounts, so its routes are never registered and the core static handler answers in their place. That handler allows GET and HEAD only, so every URL the plugin's own pane calls comes back as HTTP 405. Releases from 0.2.1-alpha.2 no longer provide `webRuntime`, which is how the sidebar's Files pane stopped previewing and downloading anything. lib/web-runtime.js provides that service — only when nothing else does, so a deployment that still has the real one (or @linxin666/dsh-remote-web-ui) is untouched. The value is read live from the trust source this harness actually has now: `connection.trustedHosts`, plus the listener's bind address, which the alpha.2 /api fence accepts as a separate case and a plugin carrying the older two-argument fence cannot see at all. `provideWebRuntime` turns it off. Verified against the installed 0.2.1-alpha.2, end to end: the same boot with and without the plugin gives 405 and then 400 on POST /sidebar/api/fs.tree, the 400 being the sidebar's own handler answering. 59/59 unit tests (new test/web-runtime.test.mjs), and scripts/live-probe.mjs — extended with the Files-API check — reports 11/11 on the fixed build and 10/11 with the 405 on the live one, which is the user-visible regression itself.
112 lines
5.0 KiB
JavaScript
112 lines
5.0 KiB
JavaScript
/**
|
|
* dsh-trustworthy — the `webRuntime` seam, for harness releases that stopped
|
|
* providing it.
|
|
*
|
|
* `@deepseek-ai/dsh-web-app` used to provide a `webRuntime` service whose value
|
|
* was the LAN-trust snapshot — `resolveLanTrust(ctx.webServer.host, trustedHosts)`,
|
|
* that is `{ lanAddresses, trustedHosts }`. Releases from `0.2.1-alpha.2` on no
|
|
* longer provide it: the `--trusted-host` authorities are parsed by the
|
|
* `webStartup` service and consumed by `@deepseek-ai/dsh-client-connection` into
|
|
* `connection.trustedHosts`, and the listener's own bind address is accepted by
|
|
* the `/api` fence as a separate case.
|
|
*
|
|
* Plugins written against the old shape never learn that it moved. They declare
|
|
* `inject: ['webRuntime']`, so Cordis holds their row **pending** — the module
|
|
* loads and its inject list is read, but the fiber never mounts. Their routes
|
|
* are then simply absent, and the core fallback answers in their place: a
|
|
* `POST /sidebar/api/fs.list` that matches no route falls through to the
|
|
* static-file handler, which allows `GET`/`HEAD` only and answers **405**.
|
|
*
|
|
* This module fills that seam. Same service name, same value shape, but read
|
|
* live from the trust source the harness actually has now, rather than cloned
|
|
* at assembly time — so a later `--trusted-host`, or a row that changed
|
|
* `connection`'s config, is visible to a plugin's own copy of the fence.
|
|
*
|
|
* @module dsh-trustworthy/web-runtime
|
|
*/
|
|
|
|
/** Service name sibling plugins inject, and read, for the Web runtime snapshot. */
|
|
export const WEB_RUNTIME_SERVICE = 'webRuntime'
|
|
|
|
/**
|
|
* Bind hosts that name every interface rather than one of them. A wildcard can
|
|
* be written as an authority but never *means* one, so it is not an entry.
|
|
*/
|
|
const WILDCARD_BIND_HOSTS = new Set(['0.0.0.0', '::', '::0', '*'])
|
|
|
|
/**
|
|
* The non-loopback authorities this deployment declared — the `--trusted-host`
|
|
* values, as `connection` received them. Read per call rather than captured, so
|
|
* the snapshot tracks the same list the `/api` fence is using.
|
|
*
|
|
* @param ctx - host plugin context carrying `connection`.
|
|
* @returns declared authorities, in the order they were configured.
|
|
*/
|
|
export function connectionAuthorities(ctx) {
|
|
const declared = ctx.connection?.trustedHosts
|
|
if (!Array.isArray(declared)) return []
|
|
return declared.filter((entry) => typeof entry === 'string' && entry !== '')
|
|
}
|
|
|
|
/**
|
|
* The listener's own bind address, when it names one interface. `0.2.1-alpha.2`'s
|
|
* `/api` fence accepts this address on its own (`isBindAddressAuthority`), and
|
|
* a plugin carrying a private copy of the older two-argument fence cannot: it
|
|
* only ever compares against the list it is handed. Publishing the bind address
|
|
* as an entry makes that older fence accept exactly what the core fence accepts
|
|
* — the entry is port-less, so like the core check it matches the address on any
|
|
* port, and a wildcard is not an authority: `0.2.1-alpha.2` refuses to bind one
|
|
* at all (`host "0.0.0.0" is an unspecified (wildcard) address, which is not
|
|
* supported`), so that case is unreachable rather than merely unused.
|
|
*
|
|
* @param ctx - host plugin context carrying `webServer`.
|
|
* @returns the bind host, or undefined when the surface is gone or wildcarded.
|
|
*/
|
|
export function bindAuthority(ctx) {
|
|
const host = ctx.webServer?.host
|
|
if (typeof host !== 'string' || host === '' || WILDCARD_BIND_HOSTS.has(host)) return undefined
|
|
return host
|
|
}
|
|
|
|
/**
|
|
* The authorities the removed `webRuntime.trustedHosts` would carry today.
|
|
*
|
|
* @param ctx - host plugin context carrying `connection` and `webServer`.
|
|
* @returns deduplicated authorities: the declared ones, then the bind address.
|
|
*/
|
|
export function trustedAuthorities(ctx) {
|
|
const declared = connectionAuthorities(ctx)
|
|
const bind = bindAuthority(ctx)
|
|
return bind === undefined ? [...new Set(declared)] : [...new Set([...declared, bind])]
|
|
}
|
|
|
|
/**
|
|
* The value of the `webRuntime` service, shaped as `0.2.1-alpha.1` provided it.
|
|
*
|
|
* Both members are getters: a consumer reads `trustedHosts` inside its fence, per
|
|
* request, so a snapshot that could not see a later authority would quietly
|
|
* refuse requests the `/api` gateway admits. `lanAddresses` stays empty because
|
|
* `0.2.1-alpha.2` rejects a wildcard bind — alpha.1 filled it from the host's
|
|
* interfaces only for `0.0.0.0`, so the empty list is that same function's
|
|
* answer for every bind this harness allows.
|
|
*
|
|
* When dsh-trustworthy's interceptor is installed the list is inert for the
|
|
* plugin's own routes: every request arrives carrying the loopback authority, so
|
|
* a fence written against this value admits it before reading the list. The
|
|
* populated list matters to consumers that read it for another reason, and to a
|
|
* deployment that installs this shim without the interceptor.
|
|
*
|
|
* @param ctx - host plugin context carrying `connection` and `webServer`.
|
|
* @returns the service value.
|
|
*/
|
|
export function webRuntimeShim(ctx) {
|
|
return {
|
|
get trustedHosts() {
|
|
return trustedAuthorities(ctx)
|
|
},
|
|
get lanAddresses() {
|
|
return []
|
|
},
|
|
}
|
|
}
|