3 Commits
Author SHA1 Message Date
mward4 b7fa429b50 v0.2.1: validated against DSH 0.2.1-alpha.1
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.
2026-10-04 19:11:52 -04:00
mward4 80fb6b6f4f Note that the frontend ships its own manifest and this one precedes it 2026-09-27 19:42:17 -04:00
mward4 c6ae15f829 Rename to dsh-trustworthy, add a generation handover for reload safety
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.
2026-09-27 05:59:39 -04:00
10 changed files with 926 additions and 126 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 dsh-disable-untrusted-origin contributors
Copyright (c) 2026 dsh-trustworthy contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+202 -19
View File
@@ -1,13 +1,17 @@
# dsh-disable-untrusted-origin
# dsh-trustworthy
Treat every browser window as if it were running on the DeepSeek Harness host itself.
**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.
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. No token, no
`--trusted-host`, no per-plugin configuration.
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.
---
@@ -91,10 +95,10 @@ exactly like a window on this machine.
```bash
# from a checkout or tarball on disk
dsh plugin --profile web add file:/path/to/dsh-disable-untrusted-origin
dsh plugin --profile web add file:/path/to/dsh-trustworthy
# or from a published package
dsh plugin --profile web add dsh-disable-untrusted-origin
dsh plugin --profile web add dsh-trustworthy
```
Then restart the host so the new bundle is loaded.
@@ -102,9 +106,35 @@ Then restart the host so the new bundle is loaded.
The bundle patch inserts the row automatically. Verify it loaded:
```bash
dsh plugin --profile web list | grep disable-untrusted-origin
dsh plugin --profile web list | grep trustworthy
```
### Installing so that the market can update it
The plugin market decides whether a git-sourced install has an update by
resolving **the ref the installed spec names** — `checkUpdates()` in
`dshmarket` calls `resolveGitRemoteHead(spec, gitRefOfTarget(spec))`. A spec
pinned to a tag therefore reports "up to date" for ever, however many releases
are published: the check resolves tag `v0.2.0` and compares it with the very
commit it already installed. Publishing a new tag does not make a button
appear.
To receive in-app updates, install from the branch:
```bash
dsh plugin --profile web add 'git+https://gitea.milesward.dev/mward4/dsh-trustworthy.git#main'
```
Releases are still tagged (`v0.2.1`, `v0.2.0`, …) so you can pin to one
deliberately and move by hand; the branch ref is what makes the market's
**Update** button work. Applying an update leaves the spec alone —
`gitUpdateTarget()` strips a fragment only when it is a 40-hex commit pin — so
it stays on the branch and keeps following it.
`dshmarket` caches its update verdicts in memory for 30 minutes; the market page
forces a fresh check when it opens, and `GET /dsh-market/updates?force=1` does
the same on demand.
---
## Configuration
@@ -112,7 +142,7 @@ dsh plugin --profile web list | grep disable-untrusted-origin
Override any key in your profile's `cordis.patch.yml`:
```yaml
- id: disable-untrusted-origin
- id: trustworthy
config:
enabled: true
disableAuth: true
@@ -187,10 +217,20 @@ context**, and the browser withholds two APIs from it:
The UUID is minted from `crypto.getRandomValues`, which is *not* restricted to
secure contexts; the clipboard falls back to a hidden textarea and
`document.execCommand('copy')`. `shimPwa` additionally serves a manifest at
`/dsh-disable-untrusted-origin/manifest.webmanifest`, links it, and widens the
`/dsh-trustworthy/manifest.webmanifest`, links it, and widens the
viewport tag to `viewport-fit=cover` so Add to Home Screen produces a
standalone app rather than a bookmark.
Note that the harness frontend **already ships a manifest** — a static
`manifest.webmanifest` with `display: "fullscreen"`, linked relatively from its
own `index.html` and served from the fallback seat. So a page with `shimPwa` on
carries two `rel="manifest"` links. That is harmless but worth knowing: the
browser takes the **first** link in document order, and index injections land at
the top of `<head>`, so this plugin's manifest is the one that wins — reporting
`display: "standalone"`, which keeps the status bar that `fullscreen` hides.
Nothing is string-patched to achieve it; the added link simply precedes the
shipped one.
The harness's own code no longer needs either shim — `@deepseek-ai/dsh-util-crypto`
mints UUIDs from `getRandomValues`, and its lint rules point callers there — but
third-party plugin bundles are not obliged to, and one that shipped recently
@@ -201,9 +241,9 @@ three are `true` by default; serving over HTTPS makes the first two redundant.
## Compatibility
Verified against **DSH 0.1.7-rc.2**. The plugin depends on three 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**, and **0.2.1-alpha.1**. The
plugin depends on three things that are internal rather than public API, so a
harness release that renames any of them will break it:
| Depends on | Where |
|---|---|
@@ -212,8 +252,106 @@ them will break it:
| `connection.requestRejection` / `authorizeIndex` / `authenticatedUrl` | `@deepseek-ai/dsh-client-connection` |
| the `webserver/index-inject` channel and the `ownsHost` marker | webserver + connection client |
If `webServer.server` is ever absent the plugin throws at load with a clear
message rather than failing quietly.
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
raw server is the one that matters — it is reported as such:
```
trustworthy: the webserver exposes no raw server, so the request-identity
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-alpha.1
The three packages this plugin reaches into are **byte-identical** between
0.2.0-rc.1 and 0.2.1-alpha.1 — `dsh-host-webserver/lib/index.js`
(`48605e6f4fc88362c07569686df1417b`), `dsh-client-connection/lib/index.js`
(`39d2d6d64f3ffb91ec623932a15a068b`), and `dsh-settings/lib/index.js`
(`5024d014ad19237fb91fbf8bb4370903`) — so nothing in the table above has moved,
`ownsHost` is still read by the client bundle, and the `webserver/index-inject`
channel is still emitted and still typed.
The release does add `--public-url`, and it is worth being explicit about what
it is not: it advertises an HTTP root in the printed URL, the system prompt, and
`DSH_WEB_URL`, and it **grants no trust** — the CLI's own help says so. The trust
surface is a separate function, `resolveLanTrust()`, and `publicUrl` never
reaches it. `--public-url` does not replace this plugin.
Measured on 0.2.1-alpha.1 over a non-loopback peer, plugin enabled vs.
`enabled: false`, same harness and same profile: **7/7 with it on, 1/7 with it
off**. Switched off, the shell answers 401, the Skill Center answers 400 (the
fence's own throw), the shims are absent, and the ready URL carries a process
token again. That difference is the whole of what this plugin carries.
An in-place upgrade of a real 0.2.0-rc.1 profile to 0.2.1-alpha.1 boots and
serves with this plugin installed and no version exemption recorded. Two other
plugins are refused by the harness's peer-range gate on that release —
`@memtensor/memos-local-plugin` and `@michengai/dsh-btw`, both because of an
absolute `<0.2.0` / `… || 0.2.0-rc.2` upper bound in their `@deepseek-ai/dsh-*`
peers. This plugin declares **no `@deepseek-ai/dsh-*` peers at all**
(`peerDependencies` is `@deepseek-ai/schemastery` only), so the gate has nothing
to reject it on and it skips nothing at boot.
### Why it never writes the trusted-host lists
The obvious fix for this problem is to add the arriving host to
`connection.trustedHosts`. This plugin deliberately does not, for two reasons.
The harness assembles **two** trusted-host arrays — one on `webRuntime`, one on
`connection` — and they are separate objects cloned at assembly time. A host
declared into one is invisible to the other, so half the fix silently does
nothing. More importantly, a fence that compares against a **literal loopback
value** rather than against the list is not moved by any declaration at all.
Plugin markets published before the list existed hardcode exactly that check, and
they are the ones producing the 403 users actually see. Rewriting the request
identity satisfies every fence, of every shape and every age; a list entry
satisfies only the fences that read the list.
### Reload safety
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
the **successor applies before the predecessor's cleanup runs** — and it does not
always run disposers at all when a plugin is switched off by editing its patch
row.
Three guards follow from that, and each has a test:
- Replacements are recorded through a **handover carrier** keyed on the target
itself, with the installing generation stamped as its owner. A cleanup whose
owner is no longer current returns without touching anything, so a
predecessor cannot restore the fence underneath a live successor. The carrier
key is `Symbol.for('dsh-trustworthy.handover')` — a registry symbol, not a
module-local one, so a reload that re-imports this file still shares it.
- The service methods answer through a **liveness check on the plugin's own
fiber**. If the fiber is gone the replacement stops admitting and delegates to
the true original instead, read from the carrier rather than captured at
install time. A plugin switched off without its disposers therefore closes
rather than leaving the gateway open — the failure mode is a working fence, not
a silent hole.
- The interceptor is tagged `Symbol.for('dsh-trustworthy.interceptor')`, and a new
one **retires any predecessor bearing the tag** before prepending itself. Two
live interceptors would both run, and the older one is a closure over a dead
generation's configuration — it would keep spoofing the peer address after
`spoofSocketAddress` was turned off.
### Known limits
- `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`.
- `Referer` is read by no layer of the harness, core or plugin. It is rewritten
only for third-party code that might read it.
### Relationship to `@goodandready/dsh-lanmode`
@@ -240,6 +378,33 @@ So the two overlap almost entirely. Running both is redundant, and
`spoofSocketAddress` in particular will confuse lanmode's device tracking — but
neither will break the other if you do.
`shimClipboard`, `shimRandomUuid`, and `shimPwa` exist so that lanmode is not
needed for the browser side either; with them on, the only things it still owns
are its mobile navigation UI, its QR tool, and its device registry.
### Relationship to `@mzzsfy/dsh-auto-trust-all`
The other published approach to this problem, and a good one — it solves it by
**declaring** the arriving host, wrapping the webserver's route-registration
methods and adding each `Host` it sees to both trusted-host lists, under an LRU
cap.
The difference is reach. A declaration only helps a fence that consults the
list, and the fences users actually hit do not: they compare against a literal
loopback value, in versions published before the list existed. Declaring the
host leaves those untouched; rewriting the request identity satisfies them. This
plugin also has nothing to cap, because it declares nothing.
The reload guards here — a generation-owned carrier, identity-checked teardown,
per-request liveness, and a tagged interceptor that retires its predecessor —
come from the same problem being solved there. That project's convention is also
worth stating, because it predicts what is coming: its own review standard
declares *any* audit finding of "missing Host validation / DNS-rebinding defence
/ token validation" **invalid** in its packages. Third-party loopback-only
fences are therefore a deliberate ecosystem position, not an oversight, and more
of them will appear. `sec-fetch-site` and the cookie authority above are the two
limits inherited from the harness side.
### What it does not fix
- Anything gating on TLS state, or on a proxy's own authentication.
@@ -256,25 +421,43 @@ neither will break the other if you do.
## Verification
```bash
node test/fence.test.mjs # drives real requests past the real plugin-market fence
node test/apply.test.mjs # wiring, including that unload restores every original
node test/shims.test.mjs # executes the generated shim in a context without the APIs
node test/fence.test.mjs # drives real requests past the real plugin-market fence
node test/apply.test.mjs # wiring, including that unload restores every original
node test/handover.test.mjs # the reload orderings: predecessor vs. successor, liveness
node test/shims.test.mjs # executes the generated shim in a context without the APIs
node scripts/live-probe.mjs # against the running harness, over a non-loopback peer
```
All four suites together — **52 tests** (fence 8, apply 20, handover 15,
shims 9) — 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.
`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`
evaluates the shim source in a `node:vm` context that lacks `crypto.randomUUID`
and `navigator.clipboard`, so it checks the delivered code rather than the string
that delivers it. The suites need no dependency tree beyond a resolvable
`@deepseek-ai/schemastery` for the second.
that delivers it. `test/handover.test.mjs` exercises the orderings directly,
including the one the harness does not promise — a predecessor's cleanup running
*after* its successor installed. The suites need no dependency tree beyond a
resolvable `@deepseek-ai/schemastery` for the second.
`scripts/live-probe.mjs` is the one that matters after a change: it connects to a
**non-loopback** local address, because the Skill Center fences on the socket
address as well as the `Host`, so a probe over `127.0.0.1` can never reproduce a
remote client and always looks like it passes.
## Releases
| Version | What changed |
|---|---|
| **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. |
| 0.1.2 | Restored `crypto.randomUUID` and `navigator.clipboard` on plain HTTP, added the PWA manifest, and widened the viewport for notched screens. |
| 0.1.1 | Provided the paired-device service, so the Skill Center works over a remote address instead of answering 400. |
| 0.1.0 | First release: one interceptor on the raw server, rewriting request identity to the loopback authority. |
## License
MIT
+4 -4
View File
@@ -1,4 +1,4 @@
# dsh-disable-untrusted-origin — bundle layer
# dsh-trustworthy — bundle layer
#
# Applied automatically when the package is installed as a profile bundle
# (package.json declares dsh.bundle.patch).
@@ -8,7 +8,7 @@
# host's own isLoopback flag, and the APIs a non-secure page is denied are
# restored. Override any key in your profile patch:
#
# - id: disable-untrusted-origin
# - id: trustworthy
# config:
# disableAuth: false # keep the process token, disarm only the fence
# grantPairing: false # when dsh-remote-web-ui provides the pairing service itself
@@ -19,8 +19,8 @@
#
# Set `enabled: false` to suspend the plugin without removing the bundle.
- insert:
- id: disable-untrusted-origin
name: 'dsh-disable-untrusted-origin'
- id: trustworthy
name: 'dsh-trustworthy'
config:
enabled: true
disableAuth: true
+191
View File
@@ -0,0 +1,191 @@
/**
* Handover between generations of the same plugin.
*
* Everything this plugin installs is a mutation of an object somebody else
* owns: methods on the `connection` service, and listeners on the raw HTTP
* server. A reload — the plugin market switching it off and on, a line-level
* config change, a bundle patch reloaded — runs `apply` again and, sooner or
* later, the previous generation's cleanup. Those two events have no
* guaranteed order, and the unordered case is destructive:
*
* generation 1 captures the true originals, installs A
* generation 2 captures A as its "originals", installs B
* generation 1 cleans up, restoring the true originals
*
* Generation 2 is now silently inert while believing it is installed, because
* the value it displaced was itself a replacement. The fix is to record the
* originals once, on the shared object, together with the identity of the
* generation that currently owns them; a cleanup runs only while it still owns
* the installation. That is what {@link installReplacements} does, and the
* interceptor needs the mirror of it — a listener that survives its author
* must not be joined by its successor.
*
* Deliberately free of imports, like `authority.js` and `shims.js`, so the
* tests can load it with no resolvable dependency tree.
*
* The sharing channels are `Symbol.for` keys, not module-local state: a reload
* can arrive as a fresh module instance with a fresh set of module bindings, and
* only the global symbol registry is common to both.
*/
/** Carrier for the recorded originals and the owning generation. */
export const HANDOVER = Symbol.for('dsh-trustworthy.handover')
/** Marks a listener as this plugin's request interceptor. */
export const INTERCEPTOR = Symbol.for('dsh-trustworthy.interceptor')
/** Fallback carrier for a target that refuses a symbol property. */
const carriers = new WeakMap()
/**
* Read the carrier on a shared object.
*
* @param target - the object the installation lives on.
* @param create - whether to make one when it is absent.
* @returns the carrier, or `undefined` when absent and not creating.
*/
function carrierFor(target, create) {
const existing = target[HANDOVER]
if (existing !== undefined) return existing
const mapped = carriers.get(target)
if (mapped !== undefined) return mapped
if (!create) return undefined
const carrier = { originals: new Map(), owner: null }
try {
Object.defineProperty(target, HANDOVER, {
value: carrier,
configurable: true,
writable: true,
enumerable: false,
})
} catch {
// A frozen or exotic host object still gets coordination, just only within
// this module instance rather than across a fresh one.
carriers.set(target, carrier)
}
return carrier
}
/**
* Install replacement properties, with a handover between generations.
*
* The originals are captured exactly once and kept on the target, so a
* successor inherits them rather than treating its predecessor's replacements
* as the state to return to. A cleanup restores only while its own generation
* still owns the installation, and only for keys still holding its own value —
* so a third party that replaced one of them in the meantime keeps it.
*
* @param target - the object whose properties are replaced.
* @param replacements - a map of property name to replacement value.
* @returns a cleanup function, safe to call more than once.
*/
export function installReplacements(target, replacements) {
const carrier = carrierFor(target, true)
const keys = Object.keys(replacements)
// Recorded before the first overwrite, and never again: from the second
// generation on, the live values are this plugin's own replacements.
for (const key of keys) {
if (!carrier.originals.has(key)) carrier.originals.set(key, target[key])
}
const token = {}
carrier.owner = token
for (const key of keys) target[key] = replacements[key]
let restored = false
return () => {
if (restored) return
restored = true
// A newer generation has taken the installation over; its values are the
// live ones and this cleanup must not touch them.
if (carrier.owner !== token) return
for (const key of keys) {
if (target[key] !== replacements[key]) continue
const original = carrier.originals.get(key)
if (original === undefined) delete target[key]
else target[key] = original
}
carrier.owner = null
if (target[HANDOVER] === carrier) {
try {
delete target[HANDOVER]
} catch {
/* Non-configurable on an exotic host; the carrier is inert anyway. */
}
}
}
}
/**
* The value a key held before this plugin first replaced it.
*
* Read from the carrier rather than captured in the caller, so a replacement
* can delegate to the true original even when it was installed by a later
* generation — and can do so lazily, at call time, when the carrier is known to
* exist.
*
* @param target - the object the installation lives on.
* @param key - the property name.
* @returns the original value, or `undefined` when there is no carrier or the
* property did not exist before this plugin was installed.
*/
export function originalOf(target, key) {
return carrierFor(target, false)?.originals.get(key)
}
/**
* Install the request-identity interceptor ahead of every other listener.
*
* `prependListener`, not `on`: the webserver registers its own request handler
* when it constructs the server, so appending would run this after every route
* lookup and change nothing.
*
* Any predecessor bearing {@link INTERCEPTOR} is removed first. A reload can
* add the successor before the predecessor's cleanup runs, and two identical
* interceptors are not merely wasteful — the older one is a live closure over a
* dead generation's config, which would keep spoofing the peer address after
* `spoofSocketAddress` was turned off.
*
* @param server - the raw `node:http` server.
* @param listener - the interceptor, called with the request or the socket.
* @param events - the server events to cover, `request` and `upgrade`.
* @returns a cleanup function, safe to call more than once.
*/
export function installInterceptor(server, listener, events = ['request', 'upgrade']) {
listener[INTERCEPTOR] = true
for (const event of events) {
for (const existing of server.listeners(event)) {
if (existing[INTERCEPTOR] === true) server.removeListener(event, existing)
}
server.prependListener(event, listener)
}
let removed = false
return () => {
if (removed) return
removed = true
for (const event of events) server.removeListener(event, listener)
}
}
/**
* Whether a fiber is still the live installation.
*
* Cordis nulls a fiber's `uid` at the very start of disposal — before its
* disposers run, and before the state machine moves to `UNLOADING` — so a
* closure left behind by a generation whose cleanup never ran can recognise
* that it no longer owns the process. The alternative signal, `fiber.state`, is
* a numeric enum whose members are not part of the plugin-facing API; `uid` is
* documented as "`null` once disposed".
*
* Conservative by construction: only a positive signal of disposal answers
* false. A host that has reshaped the fiber, or a mock without one, keeps the
* installation working rather than silently disarming it.
*
* @param fiber - the plugin fiber, usually `ctx.fiber`.
* @returns false only when the fiber is known to have been disposed.
*/
export function fiberLive(fiber) {
if (fiber === undefined || fiber === null) return true
return fiber.uid !== null
}
+120 -70
View File
@@ -1,5 +1,5 @@
/**
* dsh-disable-untrusted-origin — make every browser window behave like a
* dsh-trustworthy — make every browser window behave like a
* window running on the harness machine itself.
*
* DeepSeek Harness fences its HTTP surface by request identity. The `/api`
@@ -25,6 +25,22 @@
* `navigator.clipboard`, both of which exist only in a secure context, plus
* the metadata that makes the page installable on a phone.
*
* What it deliberately does not do is write the trusted-host lists.
* `webRuntime.trustedHosts` and the `connection` service's own `trustedHosts`
* are separate arrays cloned at assembly time, and only the second is what the
* `/api` fence reads — declaring an authority in the first, or in a config
* patch that reaches neither, produces a name the gate never sees. Rewriting
* the request needs no declaration at all, and reaches fences a declaration
* cannot: a plugin comparing the header against a hardcoded loopback literal is
* unmoved by any entry in a list it never consults.
*
* Reload safety: everything installed here is a mutation of an object the
* harness owns, and a reload can run a successor's installation before its
* predecessor's cleanup. `handover.js` records the originals once, on the
* shared object, and restores them only while the generation that wrote them
* 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
@@ -32,7 +48,7 @@
* turning it on, and keep the listening port behind something that
* authenticates on your behalf.
*
* @module dsh-disable-untrusted-origin
* @module dsh-trustworthy
*/
import z from '@deepseek-ai/schemastery'
@@ -42,6 +58,14 @@ import {
loopbackise,
spoofPeerAddress,
} from './authority.js'
import {
HANDOVER,
INTERCEPTOR,
fiberLive,
installInterceptor,
installReplacements,
originalOf,
} from './handover.js'
import {
MANIFEST_PATH,
manifest,
@@ -51,10 +75,11 @@ import {
} from './shims.js'
export { CLIENT_LOOPBACK_GRANT, loopbackAuthority, loopbackise, spoofPeerAddress }
export { HANDOVER, INTERCEPTOR, fiberLive, installInterceptor, installReplacements, originalOf }
export { MANIFEST_PATH, manifest, pwaMarkup, shimSource, withCoverViewport }
/** Stable Cordis plugin name. */
export const name = 'disable-untrusted-origin'
export const name = 'trustworthy'
/** The webserver owns the routes; the connection service owns the fence. */
export const inject = ['webServer', 'connection']
@@ -145,7 +170,7 @@ export const Config = z.object({
*/
export function apply(ctx, config) {
if (!config.enabled) {
ctx.logger?.info('disable-untrusted-origin: disabled by config; no fence was changed')
ctx.logger?.info('trustworthy: disabled by config; no fence was changed')
return
}
@@ -155,54 +180,63 @@ export function apply(ctx, config) {
const authorityNow = () => (authority ??= loopbackAuthority(ctx.webServer.port))
const onRequest = (request) => {
// A listener can outlive its own cleanup when a host tears a fiber down
// without running disposers. Cordis nulls the fiber's `uid` at the start of
// disposal, so a superseded interceptor recognises itself and stops acting
// rather than going on spoofing an identity nobody is asking it to spoof.
if (!fiberLive(ctx.fiber)) return
loopbackise(request.headers, authorityNow())
if (config.spoofSocketAddress) spoofPeerAddress(request)
}
const server = ctx.webServer.server
if (server === undefined) {
throw new Error(
'disable-untrusted-origin: the webserver exposes no raw server; the interceptor cannot be installed',
// The host surface is probed rather than assumed. A harness release that
// stops exposing the raw server should cost the user this plugin's effect and
// a log line, not a failed fiber whose error takes the tree down with it.
const server = ctx.webServer?.server
if (server === undefined || typeof server.prependListener !== 'function') {
ctx.logger?.warn(
'trustworthy: the webserver exposes no raw server, so the request-identity '
+ 'interceptor was not installed and a Host fence will still refuse a '
+ 'remote window; the harness surface may have changed',
)
} else {
// `prependListener`, not `on`: the webserver registers its own request
// handler when it constructs the server, so appending would run this after
// every route lookup and change nothing.
const detach = installInterceptor(server, onRequest)
ctx.effect(() => detach, 'trustworthy: request identity interceptor')
}
// `prependListener`, not `on`: the webserver registers its own request
// handler when it constructs the server, so appending would run this after
// every route lookup and change nothing.
server.prependListener('request', onRequest)
server.prependListener('upgrade', onRequest)
ctx.effect(
() => () => {
server.removeListener('request', onRequest)
server.removeListener('upgrade', onRequest)
},
'disable-untrusted-origin: request identity interceptor',
)
if (config.disableAuth) {
const connection = ctx.connection
const originals = {
requestRejection: connection.requestRejection,
authorizeIndex: connection.authorizeIndex,
authenticatedUrl: connection.authenticatedUrl,
}
// 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.
connection.requestRejection = () => undefined
// The shell is served by the frontend's fallback seat, which asks the
// connection service before reading index.html. Affirming unconditionally
// serves the application on a bare URL, with no process token and no
// cookie.
connection.authorizeIndex = () => true
// The startup URL and the remote-access handoff both come from here;
// returning the base URL keeps the token out of the printed link.
connection.authenticatedUrl = (baseUrl) => baseUrl
ctx.effect(
() => () => {
for (const [key, value] of Object.entries(originals)) connection[key] = value
},
'disable-untrusted-origin: authentication bypass',
)
//
// `authenticatedUrl` is the startup URL and the remote-access handoff;
// returning the base URL keeps the process token out of the printed link.
//
// Each replacement is live-aware rather than unconditional. A host that
// takes a plugin out of service by editing its patch row does not always
// run the plugin's disposers, and a service method that goes on answering
// "admitted" after the plugin was switched off is the worst possible leak:
// the fence would look restored while the gateway stayed open. When the
// fiber is gone these delegate to the true original instead — read from the
// handover carrier, so a later generation still finds the real one rather
// than a predecessor's replacement.
const live = () => fiberLive(ctx.fiber)
const delegate = (key, admitted) =>
function (...args) {
if (live()) return admitted(...args)
const original = originalOf(connection, key)
return original === undefined ? undefined : original.apply(this, args)
}
const restore = installReplacements(connection, {
requestRejection: delegate('requestRejection', () => undefined),
authorizeIndex: delegate('authorizeIndex', () => true),
authenticatedUrl: delegate('authenticatedUrl', (baseUrl) => baseUrl),
})
ctx.effect(() => restore, 'trustworthy: authentication bypass')
}
if (config.grantPairing) {
@@ -221,12 +255,15 @@ export function apply(ctx, config) {
// test, it only happens for a remote window, which is why the Skill Center
// works on the host and fails everywhere else.
//
// Registered on this plugin's own fiber, so unload withdraws it.
// Registered on this plugin's own fiber, so unload withdraws it. The answer
// is gated on the fiber being live as well: if a host switches the plugin
// off without running its disposers, the registration would otherwise go on
// vouching for every device.
if (ctx.get(PAIRING_SERVICE) === undefined) {
ctx.provide(PAIRING_SERVICE, { isPairedDevice: () => true })
ctx.provide(PAIRING_SERVICE, { isPairedDevice: () => fiberLive(ctx.fiber) })
} else {
ctx.logger?.info(
`disable-untrusted-origin: ${PAIRING_SERVICE} is already provided; leaving that provider in place`,
`trustworthy: ${PAIRING_SERVICE} is already provided; leaving that provider in place`,
)
}
}
@@ -262,47 +299,60 @@ export function apply(ctx, config) {
}
if (config.shimPwa) {
// Served from the webserver rather than injected inline, so the browser
// can fetch it under the URL the manifest link advertises.
dispose.push(ctx.webServer.register({
kind: 'exact',
path: MANIFEST_PATH,
handler: (request, response) => {
const body = JSON.stringify(manifest('DeepSeek Harness'))
response.writeHead(200, {
'content-type': 'application/manifest+json; charset=utf-8',
'content-length': Buffer.byteLength(body),
'cache-control': 'no-cache',
})
response.end(body)
},
}))
// Both seats are probed, for the same reason the raw server is: the
// harness reshapes this surface between releases, and a missing method
// should cost the user a log line rather than a failed fiber.
if (
typeof ctx.webServer?.register !== 'function'
|| typeof ctx.webServer?.tapIndex !== 'function'
) {
ctx.logger?.warn(
'trustworthy: the webserver exposes no route registry or index '
+ 'transform, so the installable-app metadata was not added',
)
} else {
// Served from the webserver rather than injected inline, so the browser
// can fetch it under the URL the manifest link advertises.
dispose.push(ctx.webServer.register({
kind: 'exact',
path: MANIFEST_PATH,
handler: (request, response) => {
const body = JSON.stringify(manifest('DeepSeek Harness'))
response.writeHead(200, {
'content-type': 'application/manifest+json; charset=utf-8',
'content-length': Buffer.byteLength(body),
'cache-control': 'no-cache',
})
response.end(body)
},
}))
ctx.inject(['webServer'], (webCtx) => {
webCtx.on('webserver/index-inject', (table) => {
table.push({
kind: 'html',
placement: 'head',
html: pwaMarkup({ manifestPath: MANIFEST_PATH, includeManifestLink: true }),
ctx.inject(['webServer'], (webCtx) => {
webCtx.on('webserver/index-inject', (table) => {
table.push({
kind: 'html',
placement: 'head',
html: pwaMarkup({ manifestPath: MANIFEST_PATH, includeManifestLink: true }),
})
})
})
})
// The viewport tag already exists, so it is rewritten in place rather
// than joined by a second one whose precedence the browser decides.
dispose.push(ctx.webServer.tapIndex(withCoverViewport))
// The viewport tag already exists, so it is rewritten in place rather
// than joined by a second one whose precedence the browser decides.
dispose.push(ctx.webServer.tapIndex(withCoverViewport))
}
}
ctx.effect(
() => () => {
for (const undo of dispose) undo()
},
'disable-untrusted-origin: browser shims',
'trustworthy: browser shims',
)
}
ctx.logger?.info(
`disable-untrusted-origin: every window is treated as local; authentication is ${
`trustworthy: every window is treated as local; authentication is ${
config.disableAuth ? 'disabled' : 'kept'
}`,
)
+3 -3
View File
@@ -13,13 +13,13 @@
*/
/** Attribute marking the injected shim script, so a second injection is a no-op. */
export const SHIM_MARKER = 'data-dsh-duo-shims'
export const SHIM_MARKER = 'data-dsh-trustworthy-shims'
/** Attribute marking the injected PWA metadata block. */
export const PWA_MARKER = 'data-dsh-duo-pwa'
export const PWA_MARKER = 'data-dsh-trustworthy-pwa'
/** Where the plugin serves its PWA manifest from. */
export const MANIFEST_PATH = '/dsh-disable-untrusted-origin/manifest.webmanifest'
export const MANIFEST_PATH = '/dsh-trustworthy/manifest.webmanifest'
/**
* The page-side shim, as a single IIFE.
+4 -3
View File
@@ -1,12 +1,13 @@
{
"name": "dsh-disable-untrusted-origin",
"version": "0.1.2",
"description": "Treat every browser window as if it were running on the DeepSeek Harness host itself: answers each request with the loopback authority so no Host/Origin fence refuses it, optionally removes the browser-session check, and restores the browser APIs a non-secure page is denied.",
"name": "dsh-trustworthy",
"version": "0.2.1",
"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.",
"type": "module",
"main": "lib/index.js",
"exports": {
".": "./lib/index.js",
"./authority": "./lib/authority.js",
"./handover": "./lib/handover.js",
"./package.json": "./package.json",
"./shims": "./lib/shims.js"
},
+151 -23
View File
@@ -17,9 +17,10 @@ import test from 'node:test'
const PLUGIN = '../lib/index.js'
/** A host context carrying only what the plugin touches. */
async function mockHost() {
async function mockHost({ rawServer = true, registry = true } = {}) {
const disposers = []
const hooks = new Map()
const warnings = []
const originals = {
requestRejection: function requestRejection() {
return 403
@@ -40,33 +41,47 @@ async function mockHost() {
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
// The webserver is a route registry plus an index transform list, and both
// refuse duplicates or release cleanly exactly as the real one does.
// refuse duplicates or release cleanly exactly as the real one does. Either
// seat can be withheld, so the tests can prove the plugin degrades to a
// warning rather than to a failed fiber when a release reshapes the surface.
const exact = new Map()
const taps = []
const webServer = {
port: server.address().port,
server,
host: '0.0.0.0',
register(route) {
if (exact.has(route.path)) {
throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`)
}
exact.set(route.path, route)
return () => {
exact.delete(route.path)
}
},
tapIndex(transform) {
taps.push(transform)
return () => {
const at = taps.indexOf(transform)
if (at !== -1) taps.splice(at, 1)
}
},
...(rawServer ? { server } : {}),
...(registry
? {
register(route) {
if (exact.has(route.path)) {
throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`)
}
exact.set(route.path, route)
return () => {
exact.delete(route.path)
}
},
tapIndex(transform) {
taps.push(transform)
return () => {
const at = taps.indexOf(transform)
if (at !== -1) taps.splice(at, 1)
}
},
}
: {}),
}
const ctx = {
logger: { info() {} },
// Cordis nulls a fiber's uid at the start of disposal, and leaves it
// non-null for as long as the plugin is mounted.
fiber: { uid: 1 },
logger: {
info() {},
warn(message) {
warnings.push(message)
},
},
webServer,
connection,
effect(callback) {
@@ -105,6 +120,7 @@ async function mockHost() {
provided,
exact,
taps,
warnings,
/** Apply the registered index transforms, as `renderIndex` does. */
renderIndex(html) {
return taps.reduce((current, tap) => tap(current), html)
@@ -299,7 +315,7 @@ test('the shims reach the index and the manifest answers', async () => {
try {
const rows = host.rows()
const script = rows.find((row) => row.kind === 'script' && row.text?.includes('data-dsh-duo-shims'))
const script = rows.find((row) => row.kind === 'script' && row.text?.includes('data-dsh-trustworthy-shims'))
assert.ok(script, 'the shim script row is present')
assert.equal(script.placement, 'head', 'the shim runs before any bundle')
assert.match(script.text, /getRandomValues/, 'the UUID polyfill mints from the unrestricted call')
@@ -353,7 +369,7 @@ test('turning the shims off leaves the index alone', async () => {
apply(host.ctx, { ...CONFIG, shimClipboard: false, shimRandomUuid: false, shimPwa: false })
try {
const rows = host.rows()
assert.equal(rows.filter((row) => row.text?.includes('data-dsh-duo-shims')).length, 0, 'no shim script')
assert.equal(rows.filter((row) => row.text?.includes('data-dsh-trustworthy-shims')).length, 0, 'no shim script')
assert.equal(rows.filter((row) => row.kind === 'html').length, 0, 'no PWA metadata')
assert.equal(rows.filter((row) => row.text?.includes('ownsHost')).length, 1, 'the identity grant is unaffected')
assert.equal(host.exact.size, 0, 'no manifest route')
@@ -370,7 +386,7 @@ test('the PWA shim alone does not drag the browser shims in', async () => {
apply(host.ctx, { ...CONFIG, shimClipboard: false, shimRandomUuid: false })
try {
const rows = host.rows()
assert.equal(rows.filter((row) => row.kind === 'script' && row.text?.includes('data-dsh-duo-shims')).length, 0)
assert.equal(rows.filter((row) => row.kind === 'script' && row.text?.includes('data-dsh-trustworthy-shims')).length, 0)
assert.equal(rows.filter((row) => row.kind === 'html').length, 1)
assert.equal(host.exact.size, 1)
} finally {
@@ -388,3 +404,115 @@ test('unload withdraws the manifest route and the index transform', async () =>
assert.equal(host.taps.length, 0, 'the viewport transform is released')
await new Promise((resolve) => host.server.close(resolve))
})
// The rest of the suite is about being switched off by a host that does not
// run disposers, and about a reload that gives the installation two authors.
test('a disposed fiber stops admitting requests', async () => {
// A host can take a plugin out of service by editing its patch row, and the
// harness does not always run the plugin's disposers when it does. An
// installed-but-switched-off plugin whose service methods still answer
// "admitted" is the worst case: the interface reports the plugin as off while
// the gateway stands open.
const host = await mockHost()
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
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.equal(host.connection.requestRejection(), 403, 'the real fence answers again')
assert.equal(host.connection.authorizeIndex(), false)
assert.equal(
host.connection.authenticatedUrl('/base'),
'/base?token=launch-secret',
'the launch token comes back',
)
await new Promise((resolve) => host.server.close(resolve))
})
test('a disposed fiber stops rewriting request identity', async () => {
const host = await mockHost()
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
const interceptor = host.server.listeners('request')[0]
const mounted = { headers: { host: '192.168.1.49:3080' } }
interceptor(mounted)
assert.equal(mounted.headers.host, `127.0.0.1:${host.ctx.webServer.port}`)
host.ctx.fiber.uid = null
const after = { headers: { host: '192.168.1.49:3080' } }
interceptor(after)
assert.equal(after.headers.host, '192.168.1.49:3080', 'a dead generation rewrites nothing')
await new Promise((resolve) => host.server.close(resolve))
})
test('a disposed fiber stops vouching for paired devices', async () => {
const host = await mockHost()
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
const pairing = host.provided.get('remoteWebUiPairing')
assert.equal(pairing.isPairedDevice(), true)
host.ctx.fiber.uid = null
assert.equal(pairing.isPairedDevice(), false, 'an off plugin vouches for nobody')
await new Promise((resolve) => host.server.close(resolve))
})
test('a late cleanup from the previous generation leaves the reload in charge', async () => {
// The ordering the harness does not promise: on reload the successor applies
// before the predecessor's disposers run. The two generations share one
// connection service, so the predecessor's cleanup is the dangerous one — an
// unconditional restore would put the fence back while the successor still
// believed it was mounted.
const previous = await mockHost()
const current = await mockHost()
previous.ctx.connection = current.ctx.connection
const { apply } = await import(PLUGIN)
apply(previous.ctx, CONFIG)
apply(current.ctx, CONFIG)
previous.unload()
assert.equal(
current.connection.requestRejection(),
undefined,
'the successor is still admitting after the predecessor was cleaned up',
)
current.unload()
assert.equal(current.connection.requestRejection(), 403, 'the original survives both generations')
assert.equal(current.connection.authenticatedUrl('/base'), '/base?token=launch-secret')
await new Promise((resolve) => previous.server.close(resolve))
await new Promise((resolve) => current.server.close(resolve))
})
test('a webserver with no raw server is reported, not fatal', async () => {
// A release may reshape the surface the plugin reaches through. Losing an
// optional capability is a warning; throwing would fail the fiber and take
// the parts that still work down with it.
const host = await mockHost({ rawServer: false })
const { apply } = await import(PLUGIN)
assert.doesNotThrow(() => apply(host.ctx, CONFIG))
assert.equal(host.warnings.length, 1, 'the loss is reported')
assert.match(host.warnings[0], /no raw server/)
assert.equal(host.connection.requestRejection(), undefined, 'the rest still works')
host.unload()
await new Promise((resolve) => host.server.close(resolve))
})
test('a webserver with no route registry is reported, not fatal', async () => {
const host = await mockHost({ registry: false })
const { apply } = await import(PLUGIN)
assert.doesNotThrow(() => apply(host.ctx, CONFIG))
assert.equal(host.warnings.length, 1)
assert.match(host.warnings[0], /no route registry/)
assert.equal(host.connection.requestRejection(), undefined)
host.unload()
await new Promise((resolve) => host.server.close(resolve))
})
+247
View File
@@ -0,0 +1,247 @@
/**
* Behavioural check for the generation handover.
*
* The mechanism this plugin installs is a mutation of objects the harness
* owns, and a reload gives those mutations two authors. These tests are about
* the orderings — which the harness does not promise — and about the two ways
* a reload goes wrong: a predecessor's cleanup tearing down its successor, and
* a superseded installation going on acting after it was replaced.
*
* node test/handover.test.mjs
*/
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import test from 'node:test'
import {
HANDOVER,
INTERCEPTOR,
fiberLive,
installInterceptor,
installReplacements,
originalOf,
} from '../lib/handover.js'
/** A stand-in for a service whose methods the plugin replaces. */
function service() {
return {
requestRejection: () => 403,
authorizeIndex: () => false,
authenticatedUrl: (baseUrl) => `${baseUrl}?token=secret`,
}
}
/** The replacements one generation installs, distinguishable from another's. */
function generation(tag, { live = () => true } = {}) {
const admitted = () => `admitted-by-${tag}`
return {
requestRejection: () => (live() ? admitted() : 'delegated'),
authorizeIndex: () => live(),
authenticatedUrl: (baseUrl) => (live() ? baseUrl : `${baseUrl}?token=secret`),
}
}
test('the originals are recorded once and restored on cleanup', () => {
const target = service()
const first = generation('one')
const restore = installReplacements(target, first)
assert.equal(target.requestRejection(), 'admitted-by-one')
assert.equal(target.authenticatedUrl('/base'), '/base', 'the token is gone while installed')
restore()
assert.equal(target.requestRejection(), 403, 'the fence is back')
assert.equal(target.authorizeIndex(), false)
assert.equal(target.authenticatedUrl('/base'), '/base?token=secret')
})
test('a predecessor cleanup cannot tear down its successor', () => {
// The ordering the harness does not promise: the successor installs first,
// and only then does the predecessor's disposer run. Restoring
// unconditionally here would put the true originals back while the successor
// still believed it was mounted — the plugin inert, reporting itself live.
const target = service()
const first = installReplacements(target, generation('one'))
const second = installReplacements(target, generation('two'))
first()
assert.equal(target.requestRejection(), 'admitted-by-two', 'the successor still owns the service')
assert.equal(target.authenticatedUrl('/base'), '/base')
second()
assert.equal(target.requestRejection(), 403, 'the true original survives both generations')
assert.equal(target.authenticatedUrl('/base'), '/base?token=secret')
})
test('a cleanup before the next install leaves the original intact', () => {
// The ordering the harness usually produces, asserted so that the guard is
// not accidentally relying on the unordered case.
const target = service()
const first = installReplacements(target, generation('one'))
first()
const second = installReplacements(target, generation('two'))
assert.equal(target.requestRejection(), 'admitted-by-two')
second()
assert.equal(target.requestRejection(), 403, 'not a predecessor replacement')
})
test('the original is readable from the carrier across generations', () => {
const target = service()
const first = installReplacements(target, generation('one'))
assert.equal(originalOf(target, 'requestRejection')(), 403)
installReplacements(target, generation('two'))
assert.equal(
originalOf(target, 'requestRejection')(),
403,
'a later generation must still find the true original, not the predecessor',
)
first()
assert.equal(target.requestRejection(), 'admitted-by-two')
})
test('cleanup is idempotent and a third party keeps what it replaced', () => {
const target = service()
const restore = installReplacements(target, generation('one'))
target.authorizeIndex = () => 'someone-else'
restore()
restore()
assert.equal(target.authorizeIndex(), 'someone-else', 'a foreign replacement is left alone')
assert.equal(target.requestRejection(), 403, 'and the untouched keys are restored')
})
test('a property that did not exist is removed rather than set to undefined', () => {
const target = {}
const restore = installReplacements(target, { requestRejection: () => 'admitted' })
assert.equal(target.requestRejection(), 'admitted')
restore()
assert.equal('requestRejection' in target, false)
})
test('the carrier is a shared, non-enumerable symbol property', () => {
const target = service()
const restore = installReplacements(target, generation('one'))
assert.equal(HANDOVER, Symbol.for('dsh-trustworthy.handover'), 'shared across module instances')
const descriptor = Object.getOwnPropertyDescriptor(target, HANDOVER)
assert.equal(descriptor.enumerable, false, 'invisible to a serializer walking the service')
restore()
assert.equal(target[HANDOVER], undefined, 'withdrawn once nothing is installed')
})
test('a target that refuses the carrier still coordinates', () => {
// Some hosts freeze their service objects. Coordination then only covers this
// module instance, which is still the common reload path, because a reload
// that re-imports the module usually reuses the cached one.
const target = Object.preventExtensions(service())
const first = installReplacements(target, generation('one'))
assert.equal(target.requestRejection(), 'admitted-by-one')
first()
assert.equal(target.requestRejection(), 403)
})
test('the interceptor is prepended ahead of an existing handler', () => {
const server = new EventEmitter()
const order = []
server.on('request', () => order.push('route'))
const detach = installInterceptor(server, () => order.push('interceptor'))
server.emit('request')
assert.deepEqual(order, ['interceptor', 'route'], 'the rewrite runs before any route lookup')
detach()
order.length = 0
server.emit('request')
assert.deepEqual(order, ['route'])
})
test('the interceptor covers both request and upgrade', () => {
const server = new EventEmitter()
const seen = []
installInterceptor(server, (event) => seen.push(event))
server.emit('request', 'request')
server.emit('upgrade', 'upgrade')
assert.deepEqual(seen, ['request', 'upgrade'], 'a websocket upgrade is a request too')
})
test('a superseded interceptor is retired rather than joined', () => {
// A reload can add the successor while the predecessor is still attached.
// Two listeners would both run, and the older one is a closure over a dead
// generation's configuration.
const server = new EventEmitter()
const seen = []
const first = () => seen.push('old')
const detachFirst = installInterceptor(server, first)
const second = () => seen.push('new')
const detachSecond = installInterceptor(server, second)
assert.equal(server.listenerCount('request'), 1, 'only the successor remains')
server.emit('request')
assert.deepEqual(seen, ['new'])
detachFirst()
server.emit('request')
assert.deepEqual(seen, ['new', 'new'], "the predecessor's cleanup is a harmless no-op")
detachSecond()
assert.equal(server.listenerCount('request'), 0)
detachSecond()
assert.equal(server.listenerCount('request'), 0, 'cleanup is idempotent')
})
test('the interceptor is marked so a successor can find it', () => {
const server = new EventEmitter()
const listener = () => {}
installInterceptor(server, listener)
assert.equal(listener[INTERCEPTOR], true)
assert.equal(INTERCEPTOR, Symbol.for('dsh-trustworthy.interceptor'))
})
test('a foreign listener is left in place', () => {
const server = new EventEmitter()
const foreign = () => {}
server.on('request', foreign)
const detach = installInterceptor(server, () => {})
detach()
assert.equal(server.listenerCount('request'), 1)
assert.equal(server.listeners('request')[0], foreign)
})
test('liveness answers false only on a positive signal of disposal', () => {
assert.equal(fiberLive(undefined), true, 'a host without a fiber is not a reason to disarm')
assert.equal(fiberLive(null), true)
assert.equal(fiberLive({}), true, 'an unrecognised shape is not a reason to disarm')
assert.equal(fiberLive({ uid: 0 }), true, 'the root fiber is live')
assert.equal(fiberLive({ uid: 12 }), true)
assert.equal(fiberLive({ uid: null }), false, 'cordis nulls uid at the start of disposal')
})
test('a dead generation delegates instead of admitting', () => {
// The replacement a live plugin installs on the connection service. Once its
// fiber is disposed it must behave as if the plugin were never mounted —
// otherwise switching the plugin off would leave the gateway open.
const target = service()
const fiber = { uid: 1 }
const live = () => fiberLive(fiber)
const delegate = (key, admitted) =>
function delegated(...args) {
if (live()) return admitted(...args)
const original = originalOf(target, key)
return original === undefined ? undefined : original.apply(this, args)
}
installReplacements(target, {
requestRejection: delegate('requestRejection', () => undefined),
authorizeIndex: delegate('authorizeIndex', () => true),
})
assert.equal(target.requestRejection(), undefined, 'admitted while the fiber is live')
assert.equal(target.authorizeIndex(), true)
fiber.uid = null
assert.equal(target.requestRejection(), 403, 'the fence is answered by the original again')
assert.equal(target.authorizeIndex(), false)
})
+3 -3
View File
@@ -89,7 +89,7 @@ test('the UUID polyfill mints a v4 UUID from the unrestricted call', async () =>
assert.equal(minted, '00010203-0405-4607-8809-0a0b0c0d0e0f')
assert.notEqual(sandbox.crypto.randomUUID(), minted, 'each call reads fresh bytes')
assert.equal(calls.draws, 2, 'one random draw per minted UUID')
assert.deepEqual(calls.attributes, [['data-dsh-duo-shims', '1']], 'the script is stamped')
assert.deepEqual(calls.attributes, [['data-dsh-trustworthy-shims', '1']], 'the script is stamped')
})
test('the UUID polyfill leaves an existing implementation alone', async () => {
@@ -148,12 +148,12 @@ test('nothing is emitted when every shim is off', async () => {
test('the PWA block carries the manifest link and the app metadata', async () => {
const { pwaMarkup, MANIFEST_PATH } = await import(SHIMS)
const markup = pwaMarkup({ manifestPath: MANIFEST_PATH, includeManifestLink: true })
assert.match(markup, /rel="manifest" href="\/dsh-disable-untrusted-origin\/manifest\.webmanifest"/)
assert.match(markup, /rel="manifest" href="\/dsh-trustworthy\/manifest\.webmanifest"/)
assert.match(markup, /apple-mobile-web-app-capable/)
assert.match(markup, /apple-mobile-web-app-status-bar-style/)
assert.match(markup, /name="theme-color"/)
// Every tag is marked, so a probe can find the block in the served document.
assert.equal((markup.match(/data-dsh-duo-pwa/g) ?? []).length, 4)
assert.equal((markup.match(/data-dsh-trustworthy-pwa/g) ?? []).length, 4)
assert.doesNotMatch(
pwaMarkup({ manifestPath: MANIFEST_PATH, includeManifestLink: false }),
/rel="manifest"/,