@deepseek-ai/dsh-host-webserver 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +22 -0
- package/README.zh.md +22 -0
- package/lib/index.js +217 -0
- package/lib/invariant.js +50 -0
- package/lib/types/index.d.ts +109 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/host/webserver/README.md
|
|
5
|
+
README.md: c41001fba3a69bfd7c00550d0be602e3fc2e0474
|
|
6
|
+
README.zh.md: 061bed977e456ba6c3cd38f5ad3d30fe0c9354ab
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-host-webserver
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Web HTTP and upgrade-route registration plugin (default-exported `HttpServerService`, config `{host, port}`): a `node:http` server that listens on activation and provides `ctx.httpServer`. `register(route)` adds a named `exact`/`prefix` HTTP route; `registerUpgrade(route)` adds an upgrade route for an exact pathname. A duplicate path within either table throws because route patterns are a composition-level contract and a collision is a misconfiguration; both methods return a disposer that removes the registration. `registerFallback(handler)` registers the one handler for requests that match no named route. A second registration throws; the SPA dist server [`dsh-frontend-static`](../frontend-static/README.md) is the shipped owner, and the server returns 404 while none is registered. `tapIndex(transform)` adds an index.html transform, and `applyIndexTaps(html)` runs a body through the registered transforms in order; the fallback handler calls it on every index response. `port` reads the listening port (the OS-assigned value when `port` is 0), and `host` reads the configured bind host (composition-time facts other plugins adapt to, e.g. the directory-picker chooser). HTTP match order is fixed: exact over the whole table, then longest prefix, then the fallback handler. Upgrades match exactly and unmatched connections are closed; registration order carries no request-facing semantics.
|
|
6
|
+
|
|
7
|
+
The package knows no harness concepts and serves no files: the `/api` HTTP bridge and downlink WebSockets are routes owned by the connection plugin, plugin bundles and the HMR event stream are routes owned by the modules/hmr plugins, and dist serving belongs to the fallback owner. The upgrade handler owns the protocol handshake and connection contents; the webserver only delivers the raw socket and request. `host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure). This server serves browsers only; Electron loads dist over `file://` and carries fetch over an IPC bridge. This package never prints; the URL line belongs to the shell.
|
|
8
|
+
|
|
9
|
+
A listen failure (EADDRINUSE…) throws out of activation and rejects Loader composition with the bind diagnostic; the failed candidate fiber is disposed. An HTTP request whose handling throws (a fallback owner's `decodeURIComponent` on a malformed %-escape, a client dropping mid-body) is answered 400 — or the socket destroyed when headers are already out — and logged as a warning; it never exits the process. An upgrade-handler exception or upgraded-socket transport error is logged as a warning and destroys its socket. Disposal starts `close()` and `closeAllConnections()`, destroys every tracked upgraded socket, and returns only after the HTTP server and those sockets have closed.
|
|
10
|
+
|
|
11
|
+
## Model Experience
|
|
12
|
+
|
|
13
|
+
None, as the package is a Web carrier between the browser and the HTTP/upgrade routes other plugins register; nothing here reaches a model request.
|
|
14
|
+
|
|
15
|
+
#### KV Cache effect
|
|
16
|
+
|
|
17
|
+
None; this package neither assembles nor sends a provider request.
|
|
18
|
+
|
|
19
|
+
## Known Limitations and Deferred Work
|
|
20
|
+
|
|
21
|
+
- **No TLS, auth, or origin policy** — binding a non-loopback address exposes the server to that network; deployment hardening (or fronting it with a real reverse proxy) is deliberately out of scope for the dev-facing v1.
|
|
22
|
+
- **Socket options are fixed** — config selects the bind host and port, while backlog and other socket settings remain internal until a deployment needs them.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-host-webserver
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
Web HTTP 与 upgrade route 注册插件(默认导出 `HttpServerService`,配置为 `{host, port}`):一个在激活时开始监听的 `node:http` 服务器,提供 `ctx.httpServer`。`register(route)` 添加具名的 `exact`/`prefix` HTTP route;`registerUpgrade(route)` 添加精确 pathname 的 upgrade route;同一张表内的重复路径会抛错,因为 route 模式是组合层约定,冲突即配置错误;两者返回的 disposer 都会移除注册。`registerFallback(handler)` 注册一个 handler,处理所有未被具名 route 命中的请求。第二次注册会抛错;随附的 SPA dist 服务器 [`dsh-frontend-static`](../frontend-static/README.md) 是该 handler 的所有者,没有注册 handler 时服务器返回 404。`tapIndex(transform)` 添加一个 index.html 转换,`applyIndexTaps(html)` 按注册顺序对一段响应体运行已注册的转换;fallback handler 在每次 index 响应时调用它。`port` 读取正在监听的端口(当 `port` 为 0 时读取 OS 分配的值),`host` 读取配置的绑定宿主(这些是其他插件据以自适应的组合期事实,例如 directory-picker 选择器)。HTTP 匹配顺序固定不变:先在整张表中匹配精确 route,再匹配最长前缀,最后交给 fallback handler。upgrade 只做精确匹配,未命中连接直接关闭;注册顺序不影响请求处理。
|
|
6
|
+
|
|
7
|
+
该包不了解任何 harness 概念,也不提供任何文件服务:`/api` HTTP 桥接与下行 WebSocket 是 connection 插件的 route,插件 bundle 与 HMR(热模块替换)事件流是 modules/hmr 插件的 route,dist 服务则属于 fallback 持有者。upgrade handler 拥有协议握手与连接内容;webserver 只交付原始 socket 与 request。`host` 只接受 `127.0.0.1`(默认值)和 `0.0.0.0`(有意向网络开放)。该服务器只服务浏览器;Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch。该包从不打印内容;URL 行属于 shell。
|
|
8
|
+
|
|
9
|
+
监听失败(EADDRINUSE……)会从激活过程抛出,以 bind 诊断使 Loader 组合 reject;失败的候选 fiber 会被 dispose(资源释放)。处理 HTTP 请求时抛错(例如 fallback 持有者的 `decodeURIComponent` 收到格式错误的百分号转义,或客户端在请求体传输中途断开)时,服务器会响应 400;若响应头已经发出,则销毁 socket,并记录 warning,但绝不会退出进程。upgrade handler 抛错或升级 socket 出现传输错误时,会记录 warning 并销毁对应 socket。资源释放会启动 `close()` 与 `closeAllConnections()`,销毁所有受跟踪的升级 socket,并仅在 HTTP server 与这些 socket 均已关闭后返回。
|
|
10
|
+
|
|
11
|
+
## 模型体验
|
|
12
|
+
|
|
13
|
+
无。该包只是浏览器与其他插件所注册 HTTP/upgrade route 之间的 Web 载体,其中没有任何内容会进入模型请求。
|
|
14
|
+
|
|
15
|
+
#### KV 缓存影响
|
|
16
|
+
|
|
17
|
+
无;该包既不组装也不发送提供方请求。
|
|
18
|
+
|
|
19
|
+
## 已知限制与延期工作
|
|
20
|
+
|
|
21
|
+
- **不提供 TLS、认证或来源策略**:绑定非回环地址会向对应网络公开服务器;面向部署的加固措施(或在前方放置真正的反向代理)有意不纳入面向开发环境的 v1。
|
|
22
|
+
- **Socket 选项固定不变**:配置只选择绑定宿主与端口;在具体部署产生需求前,backlog 和其他 socket 设置仍保持内部实现。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
//#region lib/types/index.js
|
|
5
|
+
/**
|
|
6
|
+
* @deepseek-ai/dsh-host-webserver — Web route-registration plugin: a node:http
|
|
7
|
+
* server plus the `httpServer` service (HTTP and upgrade route registries,
|
|
8
|
+
* index transform taps, and the single fallback seat for everything no route
|
|
9
|
+
* claims). Knows no harness concepts and serves no files; the composing
|
|
10
|
+
* application's frontend plugin owns dist serving through the fallback hook.
|
|
11
|
+
* Web shape only — Electron loads dist over file:// and carries fetch over an
|
|
12
|
+
* IPC bridge. This package never prints: the URL line belongs to the shell.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The browser HTTP carrier service. Activation listens immediately. Route
|
|
16
|
+
* registration order does not affect requests because configured named routes
|
|
17
|
+
* must be distinct, and the fallback handler answers anything not yet claimed
|
|
18
|
+
* during startup with 404 until its owner registers. A listen failure rejects
|
|
19
|
+
* initialization, and the boot process reports the failed fiber.
|
|
20
|
+
*/
|
|
21
|
+
var HttpServerService = class extends Service {
|
|
22
|
+
config;
|
|
23
|
+
static Config = z.object({
|
|
24
|
+
host: z.union([z.const("127.0.0.1"), z.const("0.0.0.0")]).required(),
|
|
25
|
+
port: z.natural().max(65535).required()
|
|
26
|
+
});
|
|
27
|
+
exact = /* @__PURE__ */ new Map();
|
|
28
|
+
prefixes = /* @__PURE__ */ new Map();
|
|
29
|
+
upgrades = /* @__PURE__ */ new Map();
|
|
30
|
+
upgradedSockets = /* @__PURE__ */ new Set();
|
|
31
|
+
indexTaps = [];
|
|
32
|
+
fallback;
|
|
33
|
+
server;
|
|
34
|
+
listenedPort;
|
|
35
|
+
constructor(ctx, config) {
|
|
36
|
+
super(ctx, "httpServer");
|
|
37
|
+
this.config = config;
|
|
38
|
+
}
|
|
39
|
+
/** The listening port (the OS-assigned value when config.port is 0). */
|
|
40
|
+
get port() {
|
|
41
|
+
return this.listenedPort;
|
|
42
|
+
}
|
|
43
|
+
/** The configured bind host (the loopback or all-interfaces literal). */
|
|
44
|
+
get host() {
|
|
45
|
+
return this.config.host;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Register a named route. Duplicate (kind, path) throws — route patterns are
|
|
49
|
+
* a composition-level contract, so a collision is a misconfiguration.
|
|
50
|
+
* @param route - kind, path, and the owning handler.
|
|
51
|
+
* @returns the disposer removing the route.
|
|
52
|
+
*/
|
|
53
|
+
register(route) {
|
|
54
|
+
const table = route.kind === "exact" ? this.exact : this.prefixes;
|
|
55
|
+
if (table.has(route.path)) throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`);
|
|
56
|
+
table.set(route.path, route);
|
|
57
|
+
return () => {
|
|
58
|
+
table.delete(route.path);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
|
|
63
|
+
* one socket can have only one protocol owner.
|
|
64
|
+
* @param route - pathname and handler owning negotiation plus socket use.
|
|
65
|
+
* @returns the disposer removing the route.
|
|
66
|
+
*/
|
|
67
|
+
registerUpgrade(route) {
|
|
68
|
+
if (this.upgrades.has(route.path)) throw new Error(`webserver: duplicate upgrade route "${route.path}"`);
|
|
69
|
+
this.upgrades.set(route.path, route);
|
|
70
|
+
return () => {
|
|
71
|
+
this.upgrades.delete(route.path);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Claim the fallback seat: the handler answering every request no named
|
|
76
|
+
* route matches (the SPA dist server in the shipped Web composition). One
|
|
77
|
+
* owner only — a second registration throws, because two fallbacks cannot
|
|
78
|
+
* compose.
|
|
79
|
+
* @param handler - owns the full response lifecycle of unmatched requests.
|
|
80
|
+
* @returns the disposer releasing the seat.
|
|
81
|
+
*/
|
|
82
|
+
registerFallback(handler) {
|
|
83
|
+
if (this.fallback !== void 0) throw new Error("webserver: fallback already registered");
|
|
84
|
+
this.fallback = handler;
|
|
85
|
+
return () => {
|
|
86
|
+
this.fallback = void 0;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Register an index.html transform, applied by the fallback owner to every
|
|
91
|
+
* index response ({@link applyIndexTaps}) in registration order.
|
|
92
|
+
* @param transform - pure html-to-html function.
|
|
93
|
+
* @returns the disposer removing the transform.
|
|
94
|
+
*/
|
|
95
|
+
tapIndex(transform) {
|
|
96
|
+
this.indexTaps.push(transform);
|
|
97
|
+
return () => {
|
|
98
|
+
const at = this.indexTaps.indexOf(transform);
|
|
99
|
+
if (at !== -1) this.indexTaps.splice(at, 1);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Listen; resolves once the socket is bound (rejection = FAILED fiber). */
|
|
103
|
+
async [Service.init]() {
|
|
104
|
+
const handle = async (req, res) => {
|
|
105
|
+
/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server
|
|
106
|
+
requests; the field is only optional on the client-side IncomingMessage type */
|
|
107
|
+
const rawPath = new URL(req.url ?? "/", "http://x").pathname;
|
|
108
|
+
const route = this.match(rawPath);
|
|
109
|
+
if (route !== void 0) {
|
|
110
|
+
await route.handler(req, res);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const fallback = this.fallback;
|
|
114
|
+
if (fallback === void 0) {
|
|
115
|
+
res.writeHead(404);
|
|
116
|
+
res.end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await fallback(req, res);
|
|
120
|
+
};
|
|
121
|
+
this.server = createServer((req, res) => {
|
|
122
|
+
handle(req, res).catch((err) => {
|
|
123
|
+
this.ctx.logger.warn(err instanceof Error ? err : new Error(String(err)));
|
|
124
|
+
if (res.headersSent) {
|
|
125
|
+
res.destroy();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
res.writeHead(400);
|
|
129
|
+
res.end();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
this.server.on("upgrade", (req, socket, head) => {
|
|
133
|
+
const onError = (error) => {
|
|
134
|
+
this.ctx.logger.warn(error);
|
|
135
|
+
socket.destroy();
|
|
136
|
+
};
|
|
137
|
+
socket.on("error", onError);
|
|
138
|
+
socket.once("close", () => {
|
|
139
|
+
socket.off("error", onError);
|
|
140
|
+
this.upgradedSockets.delete(socket);
|
|
141
|
+
});
|
|
142
|
+
let route;
|
|
143
|
+
try {
|
|
144
|
+
/* v8 ignore next -- node:http always sets url on server requests. */
|
|
145
|
+
route = this.upgrades.get(new URL(req.url ?? "/", "http://x").pathname);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
148
|
+
socket.destroy();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (route === void 0) {
|
|
152
|
+
socket.destroy();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.upgradedSockets.add(socket);
|
|
156
|
+
try {
|
|
157
|
+
Promise.resolve(route.handler(req, socket, head)).catch((error) => {
|
|
158
|
+
this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
159
|
+
socket.destroy();
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
163
|
+
socket.destroy();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
await new Promise((resolve, reject) => {
|
|
167
|
+
this.server.once("error", reject);
|
|
168
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
169
|
+
this.server.off("error", reject);
|
|
170
|
+
this.server.on("error", (err) => {
|
|
171
|
+
this.ctx.logger.error(err);
|
|
172
|
+
});
|
|
173
|
+
this.listenedPort = this.server.address().port;
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
this.ctx.effect(() => async () => {
|
|
178
|
+
const serverClosed = new Promise((resolve) => {
|
|
179
|
+
this.server.close(() => {
|
|
180
|
+
resolve();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
this.server.closeAllConnections();
|
|
184
|
+
const upgradedClosed = [...this.upgradedSockets].map((socket) => new Promise((resolve) => {
|
|
185
|
+
socket.once("close", () => {
|
|
186
|
+
resolve();
|
|
187
|
+
});
|
|
188
|
+
socket.destroy();
|
|
189
|
+
}));
|
|
190
|
+
await Promise.all([serverClosed, ...upgradedClosed]);
|
|
191
|
+
}, "httpServer.listen");
|
|
192
|
+
}
|
|
193
|
+
/** Longest-prefix-wins over the prefix table after an exact-table miss. */
|
|
194
|
+
match(pathname) {
|
|
195
|
+
const exact = this.exact.get(pathname);
|
|
196
|
+
if (exact !== void 0) return exact;
|
|
197
|
+
let best;
|
|
198
|
+
for (const [prefix, route] of this.prefixes) {
|
|
199
|
+
if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) continue;
|
|
200
|
+
if (best === void 0 || prefix.length > best.path.length) best = route;
|
|
201
|
+
}
|
|
202
|
+
return best;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Run an index.html body through the registered taps in registration order
|
|
206
|
+
* — called by the fallback owner on every index response it renders.
|
|
207
|
+
* @param html - the raw index.html body.
|
|
208
|
+
* @returns the transformed body.
|
|
209
|
+
*/
|
|
210
|
+
applyIndexTaps(html) {
|
|
211
|
+
let out = html;
|
|
212
|
+
for (const transform of this.indexTaps) out = transform(out);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
export { HttpServerService, HttpServerService as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-host-webserver`.
|
|
4
|
+
* @module @deepseek-ai/dsh-host-webserver/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-host-webserver";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "host-webserver-invariant";
|
|
9
|
+
/** Service required before the companion can register. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* Owned relation: HTTP and upgrade route registrations and their disposers must stay
|
|
13
|
+
* symmetric — after the owning fiber of a registered route unloads, the
|
|
14
|
+
* route table must no longer answer for its path (a stale route would keep
|
|
15
|
+
* serving a disposed plugin's handler). Checked on every fiber teardown
|
|
16
|
+
* (cordis 'internal/plugin'): the service's own registry state is compared
|
|
17
|
+
* against the set of live fibers' registrations indirectly, by probing that
|
|
18
|
+
* dispose really removed the entry — the register() disposer contract.
|
|
19
|
+
*/
|
|
20
|
+
const install = (ctx, fail) => {
|
|
21
|
+
ctx.on("internal/plugin", () => {
|
|
22
|
+
const server = ctx.get("httpServer");
|
|
23
|
+
if (server === void 0) return;
|
|
24
|
+
const probe = {
|
|
25
|
+
kind: "exact",
|
|
26
|
+
path: "/__dsh_invariant_probe__",
|
|
27
|
+
handler: () => {}
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
server.register(probe)();
|
|
31
|
+
server.register(probe)();
|
|
32
|
+
const upgradeProbe = {
|
|
33
|
+
path: "/__dsh_invariant_upgrade_probe__",
|
|
34
|
+
handler: () => {}
|
|
35
|
+
};
|
|
36
|
+
server.registerUpgrade(upgradeProbe)();
|
|
37
|
+
server.registerUpgrade(upgradeProbe)();
|
|
38
|
+
} catch {
|
|
39
|
+
fail("httpServer route disposer left a route registered — route tables and fiber lifecycles diverged");
|
|
40
|
+
}
|
|
41
|
+
}, { global: true });
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Register this package's invariant companion.
|
|
45
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
46
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
47
|
+
*/
|
|
48
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
49
|
+
//#endregion
|
|
50
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepseek-ai/dsh-host-webserver — Web route-registration plugin: a node:http
|
|
3
|
+
* server plus the `httpServer` service (HTTP and upgrade route registries,
|
|
4
|
+
* index transform taps, and the single fallback seat for everything no route
|
|
5
|
+
* claims). Knows no harness concepts and serves no files; the composing
|
|
6
|
+
* application's frontend plugin owns dist serving through the fallback hook.
|
|
7
|
+
* Web shape only — Electron loads dist over file:// and carries fetch over an
|
|
8
|
+
* IPC bridge. This package never prints: the URL line belongs to the shell.
|
|
9
|
+
*/
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
|
+
import type { Duplex } from 'node:stream';
|
|
12
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
declare module '@deepseek-ai/cordis' {
|
|
15
|
+
interface Context {
|
|
16
|
+
httpServer: HttpServerService;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
|
|
20
|
+
export type WebRouteKind = 'exact' | 'prefix';
|
|
21
|
+
/** One named route registration. */
|
|
22
|
+
export interface WebRoute {
|
|
23
|
+
kind: WebRouteKind;
|
|
24
|
+
/** Absolute pathname, no trailing slash. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Owns the full response lifecycle (may hold the response open, e.g. SSE). */
|
|
27
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** One exact-path HTTP upgrade registration. */
|
|
30
|
+
export interface WebUpgradeRoute {
|
|
31
|
+
/** Absolute pathname, no trailing slash. */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Owns protocol negotiation and the upgraded socket after dispatch. */
|
|
34
|
+
handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Gateway config: the listen address. */
|
|
37
|
+
export interface Config {
|
|
38
|
+
/** Listen host; the two supported values are loopback and all-interfaces. */
|
|
39
|
+
host: '127.0.0.1' | '0.0.0.0';
|
|
40
|
+
/** Listen port; zero requests an OS-assigned port. */
|
|
41
|
+
port: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The browser HTTP carrier service. Activation listens immediately. Route
|
|
45
|
+
* registration order does not affect requests because configured named routes
|
|
46
|
+
* must be distinct, and the fallback handler answers anything not yet claimed
|
|
47
|
+
* during startup with 404 until its owner registers. A listen failure rejects
|
|
48
|
+
* initialization, and the boot process reports the failed fiber.
|
|
49
|
+
*/
|
|
50
|
+
export declare class HttpServerService extends Service {
|
|
51
|
+
private config;
|
|
52
|
+
static Config: z<Config>;
|
|
53
|
+
private readonly exact;
|
|
54
|
+
private readonly prefixes;
|
|
55
|
+
private readonly upgrades;
|
|
56
|
+
private readonly upgradedSockets;
|
|
57
|
+
private readonly indexTaps;
|
|
58
|
+
private fallback;
|
|
59
|
+
private server;
|
|
60
|
+
private listenedPort;
|
|
61
|
+
constructor(ctx: Context, config: Config);
|
|
62
|
+
/** The listening port (the OS-assigned value when config.port is 0). */
|
|
63
|
+
get port(): number;
|
|
64
|
+
/** The configured bind host (the loopback or all-interfaces literal). */
|
|
65
|
+
get host(): Config['host'];
|
|
66
|
+
/**
|
|
67
|
+
* Register a named route. Duplicate (kind, path) throws — route patterns are
|
|
68
|
+
* a composition-level contract, so a collision is a misconfiguration.
|
|
69
|
+
* @param route - kind, path, and the owning handler.
|
|
70
|
+
* @returns the disposer removing the route.
|
|
71
|
+
*/
|
|
72
|
+
register(route: WebRoute): () => void;
|
|
73
|
+
/**
|
|
74
|
+
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
|
|
75
|
+
* one socket can have only one protocol owner.
|
|
76
|
+
* @param route - pathname and handler owning negotiation plus socket use.
|
|
77
|
+
* @returns the disposer removing the route.
|
|
78
|
+
*/
|
|
79
|
+
registerUpgrade(route: WebUpgradeRoute): () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Claim the fallback seat: the handler answering every request no named
|
|
82
|
+
* route matches (the SPA dist server in the shipped Web composition). One
|
|
83
|
+
* owner only — a second registration throws, because two fallbacks cannot
|
|
84
|
+
* compose.
|
|
85
|
+
* @param handler - owns the full response lifecycle of unmatched requests.
|
|
86
|
+
* @returns the disposer releasing the seat.
|
|
87
|
+
*/
|
|
88
|
+
registerFallback(handler: WebRoute['handler']): () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Register an index.html transform, applied by the fallback owner to every
|
|
91
|
+
* index response ({@link applyIndexTaps}) in registration order.
|
|
92
|
+
* @param transform - pure html-to-html function.
|
|
93
|
+
* @returns the disposer removing the transform.
|
|
94
|
+
*/
|
|
95
|
+
tapIndex(transform: (html: string) => string): () => void;
|
|
96
|
+
/** Listen; resolves once the socket is bound (rejection = FAILED fiber). */
|
|
97
|
+
[Service.init](): Promise<void>;
|
|
98
|
+
/** Longest-prefix-wins over the prefix table after an exact-table miss. */
|
|
99
|
+
private match;
|
|
100
|
+
/**
|
|
101
|
+
* Run an index.html body through the registered taps in registration order
|
|
102
|
+
* — called by the fallback owner on every index response it renders.
|
|
103
|
+
* @param html - the raw index.html body.
|
|
104
|
+
* @returns the transformed body.
|
|
105
|
+
*/
|
|
106
|
+
applyIndexTaps(html: string): string;
|
|
107
|
+
}
|
|
108
|
+
export default HttpServerService;
|
|
109
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-host-webserver`.
|
|
3
|
+
* @module @deepseek-ai/dsh-host-webserver/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "host-webserver-invariant";
|
|
8
|
+
/** Service required before the companion can register. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-host-webserver",
|
|
3
|
+
"description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/host/webserver"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
43
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1"
|
|
44
|
+
}
|
|
45
|
+
}
|