Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2700854a9 |
No files matched your search
@@ -1,17 +1,20 @@
|
||||
# dsh-trustworthy
|
||||
|
||||
**Removes the untrusted-origin restriction from DeepSeek Harness.** Every browser
|
||||
window is treated as if it were running on the harness machine itself: no process
|
||||
token, no declared trusted host, and no per-plugin origin fence, because one
|
||||
interceptor answers every request with the loopback authority.
|
||||
**Removes the untrusted-origin restriction from DeepSeek Harness without
|
||||
removing its authentication.** Every browser window is treated as if it were
|
||||
running on the harness machine itself: no declared trusted host and no
|
||||
per-plugin origin fence, because one interceptor answers every request with the
|
||||
loopback authority. The harness's own browser-session check stays in place —
|
||||
open the one-time launch URL from the process log once, and the 30-day cookie it
|
||||
exchanges covers the rewritten requests too.
|
||||
|
||||
A window that reaches the harness by a LAN address, a DNS name, or a tunnel
|
||||
authenticates normally and then fails every mutating request with
|
||||
`403 {"error":"untrusted origin"}`. Cards render, but Install, Update, toggle,
|
||||
and "Update all" do nothing that the user can see. This plugin removes the
|
||||
distinction between a local window and a remote one — with no token, no
|
||||
`--trusted-host`, no per-plugin configuration, and no second listener on a
|
||||
second port.
|
||||
distinction between a local window and a remote one — with no declared trusted
|
||||
host, no `--trusted-host`, and no per-plugin configuration. Removing the session
|
||||
check as well (`disableAuth: true`) is available, but it is now opt-in.
|
||||
|
||||
---
|
||||
|
||||
@@ -19,6 +22,7 @@ second port.
|
||||
|
||||
**This plugin is a deliberate and total removal of the harness's request-identity
|
||||
fence. It is not a bug fix and it is not a workaround for one specific plugin.**
|
||||
The session check is a separate matter: it stays unless you ask for it to go.
|
||||
|
||||
The harness fences requests by their `Host` and `Origin` headers for one
|
||||
reason: those are the only headers a page on another origin cannot forge, so
|
||||
@@ -26,28 +30,55 @@ they are what stops a web page you happen to visit from driving your agent API
|
||||
while the harness is reachable. With this plugin enabled:
|
||||
|
||||
- **DNS rebinding** against the harness API is no longer defended.
|
||||
- **A page on any other origin** can issue mutating requests to the API.
|
||||
- **A page on any other origin** that carries a session cookie can issue
|
||||
mutating requests to the API.
|
||||
- Those requests can **run commands on your machine** through the agent.
|
||||
|
||||
With `disableAuth: true` (the default) there is no session check either, so
|
||||
**anyone who can reach the port has full control of the agent, including code
|
||||
execution.** Reachability becomes the only access control.
|
||||
The default posture keeps the browser-session check, so the additional exposure
|
||||
is limited to a page holding a live session cookie. Set `disableAuth: true` and
|
||||
there is no check either: **anyone who can reach the port has full control of
|
||||
the agent, including code execution.** Reachability becomes the only access
|
||||
control.
|
||||
|
||||
That is a reasonable posture when the port is behind something that
|
||||
That second posture is reasonable when the port is behind something that
|
||||
authenticates on your behalf, or when it is bound to a network you fully
|
||||
control. It is a poor posture for a `0.0.0.0` bind fronted by a public tunnel
|
||||
with no authentication of its own. Decide accordingly, and re-read this
|
||||
section if you change how the port is exposed.
|
||||
with no authentication of its own. Decide accordingly, and re-read this section
|
||||
if you change how the port is exposed.
|
||||
|
||||
If you want the errors gone but not the exposure, set `disableAuth: false`.
|
||||
That keeps the process token as the access control while still answering the
|
||||
identity fence, which is what actually produces the 403.
|
||||
### The launch token, and why the default changed
|
||||
|
||||
The Web UI prints a one-time launch token in the process log —
|
||||
`dsh web: http://…/?token=…` — and from `0.2.1-alpha.4` the container entrypoint
|
||||
writes that line into the Docker log as well. The token rotates on every
|
||||
restart. Open the URL once on each origin you use, and the harness exchanges it
|
||||
for a **30-day cookie** bound to the authority it was minted under; until then
|
||||
every page answers `401`.
|
||||
|
||||
The exchange, the cookie, and the authority check are all present in
|
||||
`0.2.1-alpha.2` and later; what changed is that the bypass (0.3.0 and the
|
||||
`disableAuth: true` default it shipped) discarded all three. The default now
|
||||
keeps them.
|
||||
|
||||
Because this plugin rewrites `Host` to the loopback authority before any route
|
||||
looks at it, the cookie is minted for the loopback authority too, and the
|
||||
rewrite presents that same authority on every request afterwards. One exchange
|
||||
therefore covers every entry domain, and the token never has to be pasted into
|
||||
the address bar by hand again. That is why the plugin now keeps the check by
|
||||
default instead of removing it: the token flow is the access control that makes
|
||||
an internet-facing port defensible.
|
||||
|
||||
If you want the errors gone but not the exposure, leave `disableAuth` at its
|
||||
default `false`. Only set it `true` when something else already authenticates
|
||||
callers, and accept that the harness then has no second line of defence.
|
||||
|
||||
---
|
||||
|
||||
## What it changes
|
||||
|
||||
Five things, each one reachable only by a different kind of fence.
|
||||
Five things, each one reachable only by a different kind of fence. None of them
|
||||
touches the browser-session check; removing that is the separate `disableAuth`
|
||||
key described at the end of this section.
|
||||
|
||||
**1. Request identity, at the server.** The plugin installs `request` and
|
||||
`upgrade` listeners with `prependListener` on the raw `node:http` server the
|
||||
@@ -153,7 +184,7 @@ Override any key in your profile's `cordis.patch.yml`:
|
||||
- id: trustworthy
|
||||
config:
|
||||
enabled: true
|
||||
disableAuth: true
|
||||
disableAuth: false
|
||||
forceClientLoopback: true
|
||||
grantPairing: true
|
||||
provideWebRuntime: true
|
||||
@@ -166,7 +197,7 @@ Override any key in your profile's `cordis.patch.yml`:
|
||||
| Key | Default | Effect |
|
||||
|---|---|---|
|
||||
| `enabled` | `true` | Master switch. `false` loads the plugin and changes nothing. |
|
||||
| `disableAuth` | `true` | Also removes the browser-session check, so no process token is needed. **This is the one that removes access control.** |
|
||||
| `disableAuth` | `false` | **Also** removes the browser-session check, so no launch token is needed at all. The default keeps it: exchange the token once and the 30-day cookie covers every rewritten request. `true` is the one that removes access control. |
|
||||
| `forceClientLoopback` | `true` | Grants the browser-side `isLoopback` flag so the settings and model surfaces act as they do on the host. |
|
||||
| `grantPairing` | `true` | Provides the paired-device service that plugins like the Skill Center accept instead of a loopback peer. See below. |
|
||||
| `provideWebRuntime` | `true` | Restores the `webRuntime` trust snapshot the harness stopped publishing in 0.2.1-alpha.2, so plugins that inject it (the sidebar) mount instead of answering 405. See below. |
|
||||
@@ -175,6 +206,31 @@ Override any key in your profile's `cordis.patch.yml`:
|
||||
| `shimRandomUuid` | `true` | Restores `crypto.randomUUID` on plain HTTP. See below. |
|
||||
| `shimPwa` | `true` | Serves a manifest and adds the installable-app metadata. See below. |
|
||||
|
||||
### `disableAuth` — the two postures
|
||||
|
||||
Every key above only answers fences. `disableAuth` is the one that changes who
|
||||
may talk to the harness at all.
|
||||
|
||||
- **`false` (default).** The request-identity fence is rewritten away, and the
|
||||
browser-session check is left exactly as the harness built it. A new browser
|
||||
shows `401` until the launch URL from the process log is opened once; that
|
||||
exchange sets the 30-day cookie, and every later request — including the ones
|
||||
this plugin rewrote the `Host` on — carries it. This is the posture to use on a
|
||||
port that faces anything you do not fully control.
|
||||
- **`true`.** The session check is replaced too: `connection.admit` returns the
|
||||
owner's own peer for every request, `authorizeIndex` admits the index, and
|
||||
`authenticatedUrl` stops appending the token to the printed URL. No token, no
|
||||
cookie, no login — anyone who reaches the port controls the agent. Use it only
|
||||
behind an authenticating proxy or on a network that is already trusted.
|
||||
|
||||
The implementation replaces `admit` because that is where every shape of the
|
||||
gate ends up: releases up to `0.2.1-alpha.2` call `requestRejection` from inside
|
||||
`admit` and wrap its answer, while `0.2.1-alpha.3` and later apply the fence and
|
||||
session check in `admit` itself. `requestRejection` is replaced as well so the
|
||||
older shape cannot short-circuit around the newer one, and every replacement is
|
||||
liveness-aware, so a host that switches the plugin off without running its
|
||||
disposers gets the real gate back.
|
||||
|
||||
### `provideWebRuntime` — the Files pane, and HTTP 405
|
||||
|
||||
`0.2.1-alpha.2` stopped providing a service named `webRuntime`. Its value was the
|
||||
@@ -304,9 +360,25 @@ three are `true` by default; serving over HTTPS makes the first two redundant.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Verified against **DSH 0.1.7-rc.2**, **0.2.0-rc.1**, **0.2.1-alpha.1**, and
|
||||
**0.2.1-alpha.2**. The plugin depends on several things that are internal rather
|
||||
than public API, so a harness release that renames any of them will break it:
|
||||
Verified against **DSH 0.1.7-rc.2**, **0.2.0-rc.1**, **0.2.1-alpha.1**,
|
||||
**0.2.1-alpha.2**, **0.2.1-alpha.4**, and **0.2.1-rc.1**. From 0.4.0 the
|
||||
manifest also declares the range:
|
||||
|
||||
```json
|
||||
"engines": { "dsh": ">=0.1.7-rc.2 <0.3.0-0" }
|
||||
```
|
||||
|
||||
`0.2.1-alpha.4` made `engines.dsh` the compatibility declaration a plugin should
|
||||
provide — the harness uses it, when present, in place of guessing from
|
||||
`@deepseek-ai/dsh-*` peer ranges. This plugin has no dsh peers at all, so the
|
||||
declaration is the only statement of support it makes, and it is bounded above
|
||||
at the next minor release, excluding its prereleases, rather than left open. `0.2.1-rc.2` is not listed as a
|
||||
separate version because `dsh-client-connection`, `dsh-host-webserver`,
|
||||
`dsh-web-app`, and the frontend static bundle are byte-identical between rc.1
|
||||
and rc.2.
|
||||
|
||||
The plugin depends on several things that are internal rather than public API,
|
||||
so a harness release that renames any of them will break it:
|
||||
|
||||
| Depends on | Where |
|
||||
|---|---|
|
||||
@@ -314,9 +386,13 @@ than public API, so a harness release that renames any of them will break it:
|
||||
| `webServer.host` — the listener's own bind address | `@deepseek-ai/dsh-host-webserver` |
|
||||
| `webServer.register` / `webServer.tapIndex` — the route registry and index transforms | `@deepseek-ai/dsh-host-webserver` |
|
||||
| `connection.trustedHosts` — the authorities the `/api` fence reads | `@deepseek-ai/dsh-client-connection` |
|
||||
| `connection.admit` — the fence and session check, alone from 0.2.1-alpha.3 | `@deepseek-ai/dsh-client-connection` |
|
||||
| `connection.requestRejection` / `authorizeIndex` / `authenticatedUrl` | `@deepseek-ai/dsh-client-connection` |
|
||||
| the `webserver/index-inject` channel and the `ownsHost` marker | webserver + connection client |
|
||||
|
||||
The only one used conditionally is `connection.admit`: the bypass path replaces
|
||||
it when `disableAuth: true`. The default posture does not touch it at all.
|
||||
|
||||
Every one of those is probed before use. If a release reshapes the surface, the
|
||||
plugin logs which capability it lost and carries on with the rest, rather than
|
||||
failing the fiber and taking the parts that still work down with it. Losing the
|
||||
@@ -328,6 +404,49 @@ interceptor was not installed and a Host fence will still refuse a remote
|
||||
window; the harness surface may have changed
|
||||
```
|
||||
|
||||
### What was checked on 0.2.1-rc.1
|
||||
|
||||
This is the release the 0.4.0 change answers. Two things moved.
|
||||
|
||||
**The gate moved into `admit`.** `requestRejection` is now a façade over
|
||||
`admit`, and the `/api` route and the WebSocket upgrade call `admit` directly.
|
||||
A plugin that only answered `requestRejection` — which is all 0.3.0 did when
|
||||
`disableAuth: true` — left the index reachable and every WebSocket refused with
|
||||
`401`, so the page loaded and then showed a disconnected shell. The bypass path
|
||||
now replaces `admit` itself and returns the owner's peer (`browserPeer` on this
|
||||
release, `operator` before alpha.3); measured on a sandboxed rc.1 with a
|
||||
non-loopback `Host`, the upgrade answered `101` with no cookie.
|
||||
|
||||
**The session check is now exercised by the default posture.** The plugin
|
||||
rewrites `Host`/`Origin` to `127.0.0.1:<port>`, which is also the authority the
|
||||
harness mints the session cookie for. Measured on the same sandbox, in order:
|
||||
|
||||
| Request (`Host: ai.milesward.dev`) | Result |
|
||||
|---|---|
|
||||
| `GET /` with no cookie | `401` |
|
||||
| `GET /?token=<launch token>` | `303`, `location: ./`, `Set-Cookie: dsh-auth-…` |
|
||||
| `GET /` with that cookie | `200` index |
|
||||
| `Upgrade /api/remote.mux` with that cookie | `101` |
|
||||
|
||||
The cookie payload's `authority` is `127.0.0.1:<port>` — the rewritten one — so
|
||||
the browser keeps the cookie for the public origin it opened, and the plugin's
|
||||
rewrite presents the matching authority on every request afterwards. This is
|
||||
what lets the 30-day session survive a Host rewrite, and it is why the default
|
||||
no longer needs to remove the check.
|
||||
|
||||
The token rotates whenever the process restarts (`dsh web: …?token=…` in the
|
||||
process log; the container entrypoint prints that line into the Docker log from
|
||||
0.2.1-alpha.4). The safety property the old bypass traded away is exactly this
|
||||
one: after `disableAuth: true` the printed URL has no token and every session is
|
||||
unauthenticated.
|
||||
|
||||
Also verified on this release: `webServer.server` / `register` / `tapIndex` and
|
||||
the `webserver/index-inject` channel are unchanged, so the interceptor, the
|
||||
browser grant, the shims, and the PWA route need no adjustment. The
|
||||
`dsh-client-connection`, `dsh-host-webserver`, `dsh-web-app`, and
|
||||
`dsh-host-frontend-static` bundles are byte-identical between 0.2.1-rc.1 and
|
||||
0.2.1-rc.2.
|
||||
|
||||
### What was checked on 0.2.1-alpha.2
|
||||
|
||||
Everything in the table above is still present and still of the shape this plugin
|
||||
@@ -445,14 +564,17 @@ Three guards follow from that, and each has a test:
|
||||
- `sec-fetch-site: cross-site` is refused **unconditionally** by
|
||||
`isTrustedApiRequest`, before the `Origin` check and regardless of the
|
||||
trusted-host list. No header rewrite can defeat it, and this plugin does not
|
||||
try. It is inert with the default `disableAuth: true`, because the gateway
|
||||
never reaches the fence. It is the one thing that can still refuse a request
|
||||
with `disableAuth: false`.
|
||||
- Rewriting `Host` changes the **cookie authority** the harness derives from it
|
||||
(`dsh-auth-<sha256(authority)>`). With `disableAuth: false` that invalidates
|
||||
sessions minted under the hostname you used before — one re-login — and
|
||||
afterwards a single login covers every entry domain instead of one per domain.
|
||||
Inert with `disableAuth: true`.
|
||||
try. It only guards requests a browser itself marks as cross-site, so it has
|
||||
no effect on normal navigation or on the WebSocket. Set `disableAuth: true`
|
||||
and the fence is skipped entirely.
|
||||
- Rewriting `Host` decides the **cookie authority** the harness derives from it
|
||||
(`dsh-auth-<sha256(authority)>`). The launch token is exchanged for the
|
||||
rewritten authority, so the resulting 30-day cookie matches every request the
|
||||
plugin intercepts — that is what makes the default posture work. The
|
||||
authority is the loopback bind (`127.0.0.1:<port>`), so changing `--port`
|
||||
mints a new one and requires one new exchange. A session cookie you already
|
||||
hold under the public hostname is not reused; expect one re-login on enabling
|
||||
the plugin.
|
||||
- `Referer` is read by no layer of the harness, core or plugin. It is rewritten
|
||||
only for third-party code that might read it.
|
||||
|
||||
@@ -532,11 +654,16 @@ node test/web-runtime.test.mjs # the restored webRuntime value, and when it is
|
||||
node scripts/live-probe.mjs # against the running harness, over a non-loopback peer
|
||||
```
|
||||
|
||||
All five suites together — **59 tests** (fence 8, apply 20, handover 15,
|
||||
All five suites together — **60 tests** (fence 8, apply 21, handover 15,
|
||||
shims 9, web-runtime 7) — are `node --test test/*.test.mjs` from the plugin
|
||||
directory. The shell has to expand that glob: `node --test test/` fails with `MODULE_NOT_FOUND`
|
||||
on Node 22.
|
||||
|
||||
The manifest pins what it supports: `engines.dsh` is
|
||||
`>=0.1.7-rc.2 <0.3.0-0`, so a harness from a future minor release skips this
|
||||
plugin instead of loading it against a surface it was never checked on. Bump the
|
||||
range only after re-running the probe matrix above.
|
||||
|
||||
`test/fence.test.mjs` loads `sameOrigin()` from the installed `dshmarket` when a
|
||||
profile is present and says which source it used, so a run against a bundled
|
||||
copy is never mistaken for a run against the real thing. `test/shims.test.mjs`
|
||||
@@ -556,6 +683,7 @@ remote client and always looks like it passes.
|
||||
|
||||
| Version | What changed |
|
||||
|---|---|
|
||||
| **0.4.0** | Keeps the browser-session check by default (`disableAuth: false`) and makes the 0.2.1-alpha.3 launch-token → 30-day-cookie flow survive the Host rewrite, including on a remote origin; the no-check posture moves to `disableAuth: true` and now replaces `connection.admit` as well, because the gate moved there. Declares `engines.dsh` (the 0.2.1-alpha.4 recommendation) and is validated on 0.2.1-alpha.4 and 0.2.1-rc.1. |
|
||||
| **0.3.0** | Provides the `webRuntime` trust snapshot that `0.2.1-alpha.2` stopped publishing (`lib/web-runtime.js`, `provideWebRuntime`), so a plugin that injects it — the sidebar among them — mounts instead of answering 405 on every request. The value is read live from `connection.trustedHosts` plus the listener's bind address, and an existing provider is left alone. |
|
||||
| 0.2.1 | Documentation and compatibility only — **no code change from 0.2.0**. Records the 0.2.1-alpha.1 validation above, and how to install so the market can update it. |
|
||||
| 0.2.0 | Renamed from `dsh-disable-untrusted-origin`. Added `lib/handover.js`: originals recorded once on a `Symbol.for` carrier, a liveness check (`fiber.uid === null`) on every delegated path, and an interceptor that retires a tagged predecessor. Capabilities now degrade with a warning instead of failing the fiber. |
|
||||
|
||||
+7
-5
@@ -4,13 +4,15 @@
|
||||
# (package.json declares dsh.bundle.patch).
|
||||
#
|
||||
# The defaults below are the plugin's stated posture: the identity fence is
|
||||
# disarmed, the browser-session check is removed, the browser is granted the
|
||||
# host's own isLoopback flag, and the APIs a non-secure page is denied are
|
||||
# restored. Override any key in your profile patch:
|
||||
# disarmed, the browser is granted the host's own isLoopback flag, and the APIs
|
||||
# a non-secure page is denied are restored. The browser-session check stays in
|
||||
# place: the first visit is the one-time launch token printed in the process log
|
||||
# (the Docker log from 0.2.1-alpha.4 on), exchanged for a 30-day cookie.
|
||||
# Override any key in your profile patch:
|
||||
#
|
||||
# - id: trustworthy
|
||||
# config:
|
||||
# disableAuth: false # keep the process token, disarm only the fence
|
||||
# disableAuth: true # ALSO remove the session check; no token needed
|
||||
# grantPairing: false # when dsh-remote-web-ui provides the pairing service itself
|
||||
# spoofSocketAddress: true # also satisfy socket-based fences with no pairing path
|
||||
# shimClipboard: false # unnecessary when you reach the harness over HTTPS
|
||||
@@ -23,7 +25,7 @@
|
||||
name: 'dsh-trustworthy'
|
||||
config:
|
||||
enabled: true
|
||||
disableAuth: true
|
||||
disableAuth: false
|
||||
forceClientLoopback: true
|
||||
grantPairing: true
|
||||
spoofSocketAddress: false
|
||||
|
||||
+38
-13
@@ -11,7 +11,13 @@
|
||||
* request with `403`, which reads to the user as a button that does nothing.
|
||||
*
|
||||
* This plugin removes the distinction in two places, and separately restores
|
||||
* the browser capabilities a non-secure page is denied.
|
||||
* the browser capabilities a non-secure page is denied. It removes only the
|
||||
* origin fence by default: the harness's own browser-session check stays in
|
||||
* place, so the first visit is the one-time launch token from the process log
|
||||
* (`dsh web: …?token=…`; the container entrypoint prints it into the Docker log
|
||||
* from `0.2.1-alpha.4`), which is exchanged for a 30-day cookie bound to the
|
||||
* authority it was minted under. Set `disableAuth: true` to remove that check
|
||||
* as well.
|
||||
*
|
||||
* 1. A `request`/`upgrade` listener installed ahead of every route answers
|
||||
* each connection with the loopback authority. One interception covers the
|
||||
@@ -47,12 +53,13 @@
|
||||
* still owns the installation — so a reload cannot leave the plugin inert while
|
||||
* it believes it is mounted, nor let a superseded interceptor keep acting.
|
||||
*
|
||||
* Security: this is a deliberate and total removal of the request-identity
|
||||
* fence across the whole HTTP surface. It defends against DNS rebinding and
|
||||
* against another page in the same browser driving this API, and both
|
||||
* defences stop applying once this plugin is enabled. Read the README before
|
||||
* turning it on, and keep the listening port behind something that
|
||||
* authenticates on your behalf.
|
||||
* Security: removing the request-identity fence is deliberate and total for
|
||||
* every window that reaches the port. It defends against DNS rebinding and
|
||||
* against another page in the same browser driving this API, and both defences
|
||||
* stop applying once this plugin is enabled. The default keeps the session
|
||||
* check, so the exposure is a page that carries a live cookie; `disableAuth:
|
||||
* true` removes the check too, and then reachability is the only access
|
||||
* control. Read the README before turning that on.
|
||||
*
|
||||
* @module dsh-trustworthy
|
||||
*/
|
||||
@@ -121,12 +128,15 @@ export const Config = z.object({
|
||||
),
|
||||
disableAuth: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.default(false)
|
||||
.description(
|
||||
'Also remove the browser-session check, so no process token is needed. '
|
||||
+ 'Anyone who can reach the port then has full control of the agent, '
|
||||
+ 'including code execution. Keep this true only behind a port or '
|
||||
+ 'tunnel that authenticates on its own.',
|
||||
+ 'including code execution. The default leaves the check in place: '
|
||||
+ 'open the launch URL from the process log (in the Docker log on the '
|
||||
+ 'container image) once, and the 30-day session cookie it exchanges '
|
||||
+ 'covers this plugin\'s rewritten requests too. Set this true only '
|
||||
+ 'behind a port or tunnel that authenticates on its own.',
|
||||
),
|
||||
forceClientLoopback: z
|
||||
.boolean()
|
||||
@@ -240,9 +250,17 @@ export function apply(ctx, config) {
|
||||
|
||||
if (config.disableAuth) {
|
||||
const connection = ctx.connection
|
||||
// The `/api` gateway asks one method whether the request may proceed; it
|
||||
// applies the identity fence and then the session check. Answering
|
||||
// `undefined` admits both.
|
||||
// The gate is one method. Releases up to `0.2.1-alpha.2` asked
|
||||
// `requestRejection` first and `admit` wrapped its answer; from
|
||||
// `0.2.1-alpha.3` on `admit` applies the fence and the session check
|
||||
// itself, and `requestRejection` is a façade over it. Replacing `admit`
|
||||
// therefore satisfies both shapes, and replacing `requestRejection` keeps
|
||||
// the older shape from short-circuiting around it.
|
||||
//
|
||||
// The peer that comes back is one the harness already owns: `operator` up
|
||||
// to `0.2.1-alpha.2`, `browserPeer` from `0.2.1-alpha.3` on. There is no
|
||||
// synthetic identity to invent, and returning the owner's peer is exactly
|
||||
// what the session check would have produced for a logged-in browser.
|
||||
//
|
||||
// `authenticatedUrl` is the startup URL and the remote-access handoff;
|
||||
// returning the base URL keeps the process token out of the printed link.
|
||||
@@ -262,7 +280,14 @@ export function apply(ctx, config) {
|
||||
const original = originalOf(connection, key)
|
||||
return original === undefined ? undefined : original.apply(this, args)
|
||||
}
|
||||
const admit = (request) => {
|
||||
const peer = connection.browserPeer ?? connection.operator
|
||||
if (peer !== undefined) return { peer }
|
||||
const original = originalOf(connection, 'admit')
|
||||
return original === undefined ? undefined : original.call(connection, request)
|
||||
}
|
||||
const restore = installReplacements(connection, {
|
||||
admit: delegate('admit', admit),
|
||||
requestRejection: delegate('requestRejection', () => undefined),
|
||||
authorizeIndex: delegate('authorizeIndex', () => true),
|
||||
authenticatedUrl: delegate('authenticatedUrl', (baseUrl) => baseUrl),
|
||||
|
||||
+6
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-trustworthy",
|
||||
"version": "0.3.0",
|
||||
"description": "Removes the untrusted-origin restriction from DeepSeek Harness. Every browser window is treated as if it were running on the harness machine itself: no process token, no declared trusted host, and no per-plugin origin fence, because one interceptor answers every request with the loopback authority.",
|
||||
"version": "0.4.0",
|
||||
"description": "Removes the untrusted-origin restriction from DeepSeek Harness while keeping its own session check: every browser window is treated as if it were running on the harness machine itself, so no declared trusted host and no per-plugin origin fence refuse it. One interceptor answers every request with the loopback authority, and the one-time launch token is exchanged for a 30-day cookie exactly as on the loopback host.",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
@@ -19,6 +19,9 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"dsh": ">=0.1.7-rc.2 <0.3.0-0"
|
||||
},
|
||||
"keywords": [
|
||||
"dsh",
|
||||
"dsh-plugin",
|
||||
@@ -33,7 +36,7 @@
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"compatibility": {
|
||||
"dsh": ">=0.1.7-rc.2"
|
||||
"dsh": ">=0.1.7-rc.2 <0.3.0-0"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
+29
-1
@@ -25,6 +25,9 @@ async function mockHost({ rawServer = true, registry = true } = {}) {
|
||||
requestRejection: function requestRejection() {
|
||||
return 403
|
||||
},
|
||||
admit: function admit() {
|
||||
return { rejection: 401 }
|
||||
},
|
||||
authorizeIndex: function authorizeIndex() {
|
||||
return false
|
||||
},
|
||||
@@ -32,7 +35,12 @@ async function mockHost({ rawServer = true, registry = true } = {}) {
|
||||
return `${baseUrl}?token=launch-secret`
|
||||
},
|
||||
}
|
||||
const connection = { ...originals }
|
||||
const connection = {
|
||||
...originals,
|
||||
// Releases up to 0.2.1-alpha.2 named the owner peer `operator`; from
|
||||
// 0.2.1-alpha.3 on it is `browserPeer`. A real host has one or the other.
|
||||
browserPeer: { id: 'browser-peer' },
|
||||
}
|
||||
const provided = new Map()
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200)
|
||||
@@ -195,21 +203,39 @@ test('disableAuth replaces the session methods and unload restores them', async
|
||||
const host = await mockHost()
|
||||
const { apply } = await import(PLUGIN)
|
||||
apply(host.ctx, CONFIG)
|
||||
assert.deepEqual(host.connection.admit({}), { peer: host.connection.browserPeer })
|
||||
assert.equal(host.connection.requestRejection({}), undefined)
|
||||
assert.equal(host.connection.authorizeIndex({}, {}), true)
|
||||
assert.equal(host.connection.authenticatedUrl('http://box:3080'), 'http://box:3080')
|
||||
host.unload()
|
||||
assert.deepEqual(host.connection.admit({}), { rejection: 401 })
|
||||
assert.equal(host.connection.requestRejection({}), 403)
|
||||
assert.equal(host.connection.authorizeIndex({}, {}), false)
|
||||
assert.equal(host.connection.authenticatedUrl('http://box:3080'), 'http://box:3080?token=launch-secret')
|
||||
await new Promise((resolve) => host.server.close(resolve))
|
||||
})
|
||||
|
||||
test('disableAuth admits through the operator peer on 0.2.1-alpha.2 and older', async () => {
|
||||
const host = await mockHost()
|
||||
const operator = { id: 'operator-peer' }
|
||||
delete host.connection.browserPeer
|
||||
host.connection.operator = operator
|
||||
const { apply } = await import(PLUGIN)
|
||||
apply(host.ctx, CONFIG)
|
||||
try {
|
||||
assert.deepEqual(host.connection.admit({}), { peer: operator })
|
||||
} finally {
|
||||
host.unload()
|
||||
await new Promise((resolve) => host.server.close(resolve))
|
||||
}
|
||||
})
|
||||
|
||||
test('disableAuth false leaves the session methods untouched', async () => {
|
||||
const host = await mockHost()
|
||||
const { apply } = await import(PLUGIN)
|
||||
apply(host.ctx, { ...CONFIG, disableAuth: false })
|
||||
try {
|
||||
assert.deepEqual(host.connection.admit({}), { rejection: 401 })
|
||||
assert.equal(host.connection.requestRejection({}), 403)
|
||||
assert.equal(host.connection.authorizeIndex({}, {}), false)
|
||||
} finally {
|
||||
@@ -418,11 +444,13 @@ test('a disposed fiber stops admitting requests', async () => {
|
||||
const { apply } = await import(PLUGIN)
|
||||
apply(host.ctx, CONFIG)
|
||||
|
||||
assert.deepEqual(host.connection.admit({}), { peer: host.connection.browserPeer }, 'admitting while mounted')
|
||||
assert.equal(host.connection.requestRejection(), undefined, 'admitted while mounted')
|
||||
assert.equal(host.connection.authorizeIndex(), true)
|
||||
assert.equal(host.connection.authenticatedUrl('/base'), '/base')
|
||||
|
||||
host.ctx.fiber.uid = null
|
||||
assert.deepEqual(host.connection.admit({}), { rejection: 401 }, 'the real gate answers again')
|
||||
assert.equal(host.connection.requestRejection(), 403, 'the real fence answers again')
|
||||
assert.equal(host.connection.authorizeIndex(), false)
|
||||
assert.equal(
|
||||
|
||||
Reference in new issue
Block a user