@lvxianchao/dsh-web-guard 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.1 — 2026-11-10
4
+
5
+ ### Fixed
6
+
7
+ - **The proxy no longer leaks upstream sockets when a browser disconnects.** Closing a tab, reloading or dropping the link mid-response left the upstream request alive; for a response that never ends — an SSE stream, a long poll — the socket stayed allocated in the shared keep-alive agent forever. Once `maxSockets` (128) of them had accumulated, every later request through the proxy queued until the reverse proxy in front gave up with `504` — while `/__guard/session` kept answering 200, which made it look like the harness was up. The guard now destroys the upstream request when the client closes and treats the resulting error as expected instead of logging it.
8
+ - **The launch-token exchange is bounded to 10 seconds.** An unanswered exchange used to stay `inflight` forever, stalling every request that awaited it.
9
+
10
+ ### Upgrade
11
+
12
+ 1. `dsh plugin --profile web add @lvxianchao/dsh-web-guard@3.0.1` (or reinstall from the same source you use).
13
+ 2. `sudo systemctl restart dsh-web` (or however the harness is supervised). Sessions survive the restart; open pages reconnect on their own.
14
+
3
15
  ## 3.0.0 — 2026-11-06
4
16
 
5
17
  ### Breaking
package/lib/index.js CHANGED
@@ -235,7 +235,9 @@ export function apply(ctx, config = {}) {
235
235
  const pending = (async () => {
236
236
  try {
237
237
  const url = ctx.connection.authenticatedUrl(`${targetOrigin}/`);
238
- const res = await fetch(url, { redirect: 'manual' });
238
+ // Bound the exchange: an unanswered fetch would otherwise stay
239
+ // `inflight` forever and stall every request that awaits it.
240
+ const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(10_000) });
239
241
  const parts = res.headers.getSetCookie();
240
242
  const value = parts.map((line) => line.split(';')[0]).filter(Boolean).join('; ');
241
243
  if (!value)
package/lib/proxy.js CHANGED
@@ -110,7 +110,19 @@ export function forward(req, res, options) {
110
110
  const proxyReq = secure
111
111
  ? https.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log))
112
112
  : http.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log));
113
+ // A browser can walk away mid-response (tab closed, reload, flaky link).
114
+ // Without this the upstream request survives the client, and a response that
115
+ // never ends — an SSE stream, a long poll — keeps its socket allocated in the
116
+ // shared keep-alive agent forever. Once `maxSockets` of those pile up every
117
+ // later request queues until the proxy in front gives up (504).
118
+ res.on('close', () => {
119
+ if (!res.writableEnded)
120
+ proxyReq.destroy();
121
+ });
113
122
  proxyReq.on('error', (err) => {
123
+ // Destroying the upstream is the expected outcome of a client hangup.
124
+ if (res.destroyed || res.writableEnded)
125
+ return;
114
126
  log(`upstream error ${req.method} ${req.url}: ${err.message}`);
115
127
  if (!res.headersSent) {
116
128
  res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
@@ -123,10 +135,15 @@ export function forward(req, res, options) {
123
135
  req.pipe(proxyReq);
124
136
  }
125
137
  function forwardResponse(proxyRes, res, req, started, log) {
138
+ if (res.destroyed) {
139
+ proxyRes.destroy();
140
+ return;
141
+ }
126
142
  const headers = { ...proxyRes.headers };
127
143
  // Never leak the harness cookie to the browser: the guard owns it.
128
144
  delete headers['set-cookie'];
129
145
  res.writeHead(proxyRes.statusCode ?? 502, headers);
146
+ proxyRes.on('error', () => res.destroy());
130
147
  proxyRes.pipe(res);
131
148
  // Quiet by default: the Web UI polls constantly. Log failures and slow
132
149
  // requests only, so the journal does not fill with 200s.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvxianchao/dsh-web-guard",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Password login for the DeepSeek Harness Web UI, so it can be exposed on the internet: sign in from anywhere instead of chasing one-off tokens, with reverse-proxy quirks handled. Ships a native DSH settings page.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",