0.4.0 made the browser-session check opt-out, so a deployment whose whole
point is "no trusted host, no token" started asking for the launch token on
every new device — including a phone, where producing it is the friction the
plugin exists to remove.
The bypass itself was never in doubt and is unchanged: `disableAuth: true`
still replaces `connection.admit`, `requestRejection`, `authorizeIndex` and
`authenticatedUrl`. Measured on DSH 0.2.1-rc.2 over a non-loopback Host with
no cookie: `GET /` answers 200 and a socket upgrade to `/api/remote.mux`
answers 101. Only the default moved.
`disableAuth: false` keeps 0.4.0's posture — the launch token, the 30-day
cookie bound to the rewritten loopback authority, and the check.
0.2.1-alpha.3 moved the request gate into `connection.admit`: the `/api`
gateway and the WebSocket upgrade call it directly, and `requestRejection`
became a façade over it. The 0.3.0 bypass only replaced `requestRejection`, so
under `disableAuth: true` the index loaded and every socket upgrade answered
401. Replace `admit` too, returning the peer the harness already owns
(`browserPeer` from alpha.3 on, `operator` before).
The default posture now keeps the harness's browser-session check instead of
removing it. The plugin's Host rewrite to the loopback authority is also the
authority the one-time launch token is exchanged for, so the 30-day cookie is
minted for the rewritten authority and the rewrite presents it on every later
request. Measured on 0.2.1-rc.1, 0.2.1-alpha.4 and 0.2.1-alpha.2: 401 without
the cookie, 303 + Set-Cookie on `/?token=`, 200 index and 101 upgrade with it.
Declare `engines.dsh` (`>=0.1.7-rc.2 <0.3.0-0`) as 0.2.1-alpha.4 now prefers.
The removed session check moves to an opt-in `disableAuth: true`, which still
prints a token-free URL. Tests: 60 pass, including the operator-peer fallback
and restoring the real `admit` when the fiber is disposed.
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.
No code change from 0.2.0. This release records what was measured and how to
install so the plugin market can see future releases.
Compatibility
- Verified against 0.1.7-rc.2, 0.2.0-rc.1 and 0.2.1-alpha.1.
- dsh-host-webserver, dsh-client-connection and dsh-settings are byte-identical
between 0.2.0-rc.1 and 0.2.1-alpha.1, so no surface this plugin uses moved.
- --public-url advertises a URL in the printed/handoff/system-prompt forms and
grants no trust; resolveLanTrust() is a separate function it never reaches.
- A/B over a non-loopback peer on 0.2.1-alpha.1: 7/7 enabled, 1/7 disabled.
- This plugin declares no @deepseek-ai/dsh-* peers, so the harness peer-range
gate has nothing to reject it on; an in-place profile upgrade boots with it
installed and no exemption recorded.
Install
- The market resolves the ref the installed spec names, so a tag pin reports
"up to date" for ever. Documented the branch install (#main) that makes the
in-app Update button work, and that gitUpdateTarget() leaves the spec alone.
The plugin mutates objects the harness owns, and a reload gives those
mutations two authors. The harness does not promise an ordering between
them: in practice a successor applies before its predecessor's cleanup
runs, and a plugin switched off by editing its patch row does not always
have its disposers run at all.
lib/handover.js makes both cases safe:
- replacements are recorded through a carrier keyed on the target and
stamped with the owning generation, so a cleanup whose owner is no
longer current returns without touching anything;
- the connection replacements answer through a liveness check on the
plugin's own fiber and delegate to the true original once it is gone,
so a plugin switched off without its disposers closes rather than
leaving the gateway open;
- the interceptor is tagged, and a new one retires any predecessor
bearing the tag before prepending itself.
Losing webServer.server or the route registry is now a warning naming the
lost capability rather than a failed fiber.
Renamed from dsh-disable-untrusted-origin; the package name, the plugin
id, the manifest route, and the injection markers all follow.
A page served over plain HTTP from anything but localhost is not a
secure context, so crypto.randomUUID and navigator.clipboard are simply
absent. Code that assumes them fails, sometimes loudly and sometimes as
a button that does nothing — billion-context calls
navigator.clipboard.writeText unguarded today.
Fold in what lanmode was still doing for us, so two plugins are not
needed to make one page work:
- shimRandomUuid mints the UUID from crypto.getRandomValues, which is
not restricted to secure contexts
- shimClipboard falls back to a hidden textarea and execCommand
- shimPwa serves a manifest, links it, and widens the viewport
tag to viewport-fit=cover for standalone installs
All three default on. None of them touches request identity; they live
here so that reaching the harness from another machine is one plugin's
job rather than two.
test/shims.test.mjs evaluates the generated source in a node:vm context
that lacks both APIs, so it checks the delivered code rather than the
string that delivers it. live-probe.mjs now also asserts the shims and
the manifest reach the running harness.
Checks the three surfaces the plugin is responsible for against the process
actually serving a browser: the market's fence, the shell without a token,
and the Skill Center.
The Skill Center check connects to a non-loopback local address on purpose.
Its fence requires a loopback socket as well as a loopback Host, so a probe
over 127.0.0.1 can never reproduce a remote client and always appears to
pass.
@linxin666/dsh-client-ui-skill-explorer authorizes a request either by a
loopback peer or by a live paired device, reading that second path through
a service named remoteWebUiPairing. Nothing on a normal install provides
it, and the plugin's read of it throws: it calls
ctx.get("remoteWebUiPairing", false) intending a default, but Cordis's
signature is get(name, strict), so that call returns undefined and falls
through to ctx.remoteWebUiPairing, which Cordis answers by throwing for a
service nobody provided. The webserver turns the throw into an opaque 400,
so the plugin's own 403 never reaches the client.
The throw sits behind the loopback test, which is why the Skill Center
works on the host and fails from a LAN address or a tunnel.
grantPairing (default true) provides the service, satisfying the check the
plugin designed for it and removing the throw, without reporting a false
peer address. It defers to an existing provider and must stay off when
@linxin666/dsh-remote-web-ui is installed, since two providers for one
service name is a load error.
spoofSocketAddress remains for plugins that fence on the peer address with
no pairing path of their own.
Treat every browser window as if it were running on the DeepSeek Harness
host itself.
The harness fences its HTTP surface by request identity: the /api gateway
accepts a Host that is loopback or an operator-declared authority, and each
plugin repeats a check of its own. A window that arrives by a LAN address,
a DNS name, or a tunnel therefore authenticates normally and then fails
every mutating request with 403, which reads to the user as a button that
does nothing.
The plugin removes that distinction in two places:
- A request/upgrade listener prepended on the raw node:http server the
webserver owns, ahead of the handler it was constructed with. Every
connection is answered with the loopback authority and a matching
Origin, so one interception covers the core gateway and every plugin
that fences on Host, including versions published before the harness
grew a declared-authority list.
- The browser-side isLoopback flag, granted through the harness's own
webserver/index-inject channel with the same ownsHost marker the
packaged Desktop build publishes, rather than by string-replacing a
minified client bundle.
disableAuth (default true) also removes the browser-session check, so no
process token is needed and reachability becomes the only access control.
spoofSocketAddress (default false) additionally satisfies fences that read
request.socket.remoteAddress.