@mzzsfy/dsh-auto-trust-all 0.1.0

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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @mzzsfy/dsh-auto-trust-all
2
+
3
+ DeepSeek Harness web 入口插件:动态信任所有实际到达的 Host,并将 web 默认绑定翻转为全部网卡——`dsh web` 零参数启动等效 `dsh web --host 0.0.0.0 --trusted-host=<任何实际到达的 host>`。
4
+
5
+ 设计文档见 [docs/design/dsh-auto-trust-all.md](../../docs/design/dsh-auto-trust-all.md)。
6
+
7
+ ## 功能
8
+
9
+ - **动态信任**:包装 webServer 全部路由(exact / prefix / upgrade / fallback,含激活前已注册与后续新注册),请求到达即提取 Host 头(与官方信任闸门同构的 WHATWG 解析:小写、去端口、IPv6 保留方括号),去重后注册进 `webRuntime.trustedHosts`,闸门每请求实时读数组,无需重启。泛域名(`*.example.com`)等无法枚举的入口不再需要改启动命令。
10
+ - **FIFO 容量**:注册域名容量 `maxHosts` 默认 100,超出按注册先后淘汰最早者;官方初始条目(部署派生的局域网 IP 等)不参与淘汰。内存总量恒定有界。容量按激活代记账:插件重载前注册的条目留存于信任清单但不参与新一代记账,重启即归零。
11
+ - **console 输出**:启动时输出一行状态横幅(绑定、容量、既有信任条目);此后每次注册输出 `auto-trust-all: registered host <域名>`、每次淘汰输出 `auto-trust-all: evicted host <域名>`,直接 grep dsh console 即可做入口审计。
12
+ - **默认绑定翻转**:bundle patch 覆盖官方 webserver 行的 host 默认值为 `0.0.0.0`;显式 `--host 127.0.0.1` 仍生效(表达式读 webStartup 服务,只翻默认值)。
13
+ - **认证层不动**:只影响官方 Host/Origin 信任闸门(官方文档明言"绝不建立身份"的可达性闸门);原生浏览器 cookie 认证与 dsh-web-startup-auth 会话闸门原样保留,安全增量趋近于零。
14
+ - **干净降级**:冷启动时 `webRuntime` 尚未就绪属预期,插件挂服务激活事件自动延迟启用;官方 web 面形态变化导致其真缺失时保持静默待命,不产生启动 pending、不影响 dsh 启动与其余功能。
15
+
16
+ ## 配置(无 GUI)
17
+
18
+ 唯一配置项 `maxHosts`(注册域名容量,自然数,默认 100)。走 cordis 行级配置,在 profile 的 `cordis.patch.yml`(或 `~/.dsh/cordis.patch.yml`)追加覆盖行,重启 dsh web 生效:
19
+
20
+ ```yaml
21
+ - id: auto-trust-all
22
+ config:
23
+ maxHosts: 200
24
+ ```
25
+
26
+ 容量按激活代记账:修改 `maxHosts` 需重启 dsh web 生效,新容量只约束其后注册的条目,插件热重载换代的遗留条目视同官方条目留存;重启 dsh web 则动态条目全部归零。
27
+
28
+ ## 安装
29
+
30
+ 任何 `dsh plugin add`(开发或发布安装)都会把 node_modules 里的既有 junction 重建为实体目录,装完必须重跑 `node scripts/dev-link.mjs all` 恢复工作副本挂载,否则改仓库源码不生效。
31
+
32
+ 发布版安装:
33
+
34
+ ```sh
35
+ dsh plugin --profile web add @mzzsfy/dsh-auto-trust-all
36
+ ```
37
+
38
+ 发布前开发安装(在仓库根目录执行——`file:` 相对路径按执行时所在目录锚定):
39
+
40
+ ```sh
41
+ dsh plugin --profile web add file:./packages/dsh-auto-trust-all
42
+ ```
43
+
44
+ bundle patch 随下次 dsh 重启生效。开发安装的依赖行是 `file:` 过渡态,发布后由 `dev-link.mjs all` 归一为 semver。
45
+
46
+ patch 覆盖官方 webserver 行的 config 是整体替换语义,本包已完整镜像官方全部键;该镜像由 `test/patch.test.mjs` 快照锁定——但该测试读不到官方安装目录,**不能**自动检出上游增删键。dsh 升级后请用 `dsh web --dump-default-config` 对照官方 webserver 行核对键集,发现新键需同步 `cordis.patch.yml`。
47
+
48
+ ## 与其他插件的关系
49
+
50
+ - **dsh-web-startup-auth**:完全共存。本插件不提供任何 cordis 服务,零服务冲突;两者都会遮蔽 webServer 注册方法包装路由,委托链在任意激活顺序下保持正确(会话检查与 Host 注册同时生效)。未认证请求至多完成 Host 登记(纯观察),首个拦截点始终是会话闸门。
51
+ - **dream-skin 等同构自守卫插件**:同样实时读 `webRuntime.trustedHosts`,本插件的注册对它们同步生效。
52
+
53
+ ## 安全边界
54
+
55
+ 放行的是可达性闸门:攻击者可让任意域名指向本服务并到达登录页(受 startup-auth IP 限速约束),拿不到任何数据。闸门内建的 `sec-fetch-site: cross-site` 拒绝与 Origin 同源检查独立于信任清单,不受本插件影响。泛解析范围控制与防火墙仍是运维责任。
56
+
57
+ 原生 cookie 会话按 `主机:端口` 绑定,每个新入口(各子域、各内网 IP 形态)都需要各自登录一次——这是 dsh 原生行为,与本插件无关。
58
+
59
+ ## 开发
60
+
61
+ ```sh
62
+ cd packages/dsh-auto-trust-all
63
+ node --test "test/*.test.mjs"
64
+ ```
65
+
66
+ 宿主半区改动经 dev-link junction 热重载;冒烟验证步骤见设计文档。
@@ -0,0 +1,24 @@
1
+ # The auto-trust-all bundle patch, two operations in order:
2
+ # 1. Override the official webserver row's config: the host default flips from
3
+ # loopback to all-interfaces, so a bare `dsh web` equals
4
+ # `dsh web --host 0.0.0.0`; an explicit `--host` still wins because the
5
+ # expression reads the webStartup service. The config object REPLACES the
6
+ # row's config wholesale (patch semantics are not a deep merge), so every
7
+ # official key is mirrored here; sync this list when upstream changes it.
8
+ # The name field guards against id collision: a mismatching row is skipped
9
+ # with a loader warning instead of being clobbered.
10
+ # 2. Insert this plugin as one entry; its host half wraps webServer routes and
11
+ # registers every arriving Host header into webRuntime.trustedHosts.
12
+
13
+ - id: webserver
14
+ name: '@deepseek-ai/dsh-host-webserver'
15
+ config:
16
+ host: !!js ctx.webStartup.host ?? '0.0.0.0'
17
+ port: !!js ctx.webStartup.port ?? 3080
18
+ compression: gzip
19
+ compressionLevel: 1
20
+ compressionThresholdBytes: 1024
21
+
22
+ - insert:
23
+ - id: auto-trust-all
24
+ name: '@mzzsfy/dsh-auto-trust-all'
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@mzzsfy/dsh-auto-trust-all",
3
+ "description": "动态信任所有实际到达的 Host:包装 webServer 路由把 Host 头实时注册进 webRuntime.trustedHosts,并将 web 默认绑定翻转为 0.0.0.0,泛域名入口免改启动命令;认证层(原生 cookie 与 startup-auth 会话)原样保留",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "dsh": {
12
+ "bundle": {
13
+ "patch": "./cordis.patch.yml"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "test": "node --test \"test/*.test.mjs\"",
18
+ "npmPublish": "npm publish --access public --provenance=false --registry=https://registry.npmjs.org"
19
+ },
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://git.1014.top:83/mzzsfy/dsh-plugin.git"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "peerDependencies": {
29
+ "@deepseek-ai/schemastery": ">=3.18.0"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "test",
34
+ "cordis.patch.yml",
35
+ "README.md"
36
+ ]
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,159 @@
1
+ // dsh-auto-trust-all Host 半区:把每个实际到达请求的 Host 头注册进
2
+ // webRuntime.trustedHosts,令官方 Host/Origin 信任闸门(可达性闸门,官方文档
3
+ // 明言"绝不建立身份")对泛域名等无法枚举的入口动态放行;原生 cookie 认证与
4
+ // startup-auth 会话闸门不受影响。注册容量 FIFO 淘汰,启动状态与每次注册、
5
+ // 淘汰均直接输出到 console,供用户确认生效与入口审计。
6
+
7
+ import z from '@deepseek-ai/schemastery'
8
+
9
+ export const name = 'dsh-auto-trust-all'
10
+
11
+ // 仅静态声明跨版本基座服务;webRuntime 版本面较新且与本插件在同一波激活中
12
+ // 就绪(strict get 要求提供方 fiber 已激活),按 AGENTS.md 兼容性规约走
13
+ // 事件延迟激活,真缺失时保持静默订阅,不产生 pending
14
+ export const inject = ['webServer']
15
+
16
+ // 注册域名容量:约束本插件注册的条目(FIFO 淘汰最早者),下界 1 防零容量死循环;
17
+ // 官方初始条目(局域网 IP 等)由部署派生,不在淘汰范围
18
+ export const Config = z.object({
19
+ maxHosts: z.number().step(1).min(1).default(100).description('注册域名容量,超出按注册先后淘汰'),
20
+ })
21
+
22
+ // 闸门同构提取:new URL('http://' + authority).hostname,小写、去端口、IPv6 保留方括号
23
+ function hostnameOf(headers) {
24
+ const header = headers ? headers.host : undefined
25
+ if (typeof header !== 'string' || header === '') return undefined
26
+ try {
27
+ return new URL('http://' + header).hostname
28
+ } catch {
29
+ return undefined
30
+ }
31
+ }
32
+
33
+ /** @param {import('@deepseek-ai/cordis').Context} ctx */
34
+ export function apply(ctx, config) {
35
+ const maxHosts = config.maxHosts
36
+ const webServer = ctx.webServer
37
+ const ready = () => Array.isArray(ctx.get('webRuntime')?.trustedHosts)
38
+ let activated = false
39
+ let unregisterFailed = false
40
+
41
+ const activate = () => {
42
+ if (activated) return
43
+ const initialHosts = ctx.get('webRuntime')?.trustedHosts
44
+ if (!Array.isArray(initialHosts)) return
45
+ activated = true
46
+
47
+ const seen = new Set()
48
+ // FIFO 队列与去重集合同进出(淘汰即双删),规模随容量封顶;
49
+ // 两者与信任数组绑定,webRuntime 重建提供新数组时一并清空,旧域名重新登记
50
+ const queue = []
51
+ let seenFor = null
52
+
53
+ const registerHost = (req) => {
54
+ const hosts = ctx.get('webRuntime')?.trustedHosts
55
+ if (!Array.isArray(hosts)) return
56
+ if (hosts !== seenFor) {
57
+ seenFor = hosts
58
+ seen.clear()
59
+ queue.length = 0
60
+ }
61
+ const hostname = hostnameOf(req && req.headers)
62
+ if (hostname === undefined || seen.has(hostname)) return
63
+ // 数组查重:激活前已存在的条目(官方初始条目)只记入去重集合,不入淘汰队列
64
+ if (hosts.includes(hostname)) {
65
+ seen.add(hostname)
66
+ return
67
+ }
68
+ while (queue.length >= maxHosts) {
69
+ const oldest = queue.shift()
70
+ seen.delete(oldest)
71
+ const index = hosts.indexOf(oldest)
72
+ if (index < 0) continue
73
+ hosts.splice(index, 1)
74
+ console.log('auto-trust-all: evicted host ' + oldest)
75
+ break
76
+ }
77
+ seen.add(hostname)
78
+ queue.push(hostname)
79
+ hosts.push(hostname)
80
+ console.log('auto-trust-all: registered host ' + hostname)
81
+ }
82
+
83
+ // 注册函数挂载为 webServer 上的共享载体:插件重载/行级 config 变更时新
84
+ // activate 覆盖它,旧代包装动态改道到最新一代
85
+ webServer.autoTrustAllRegister = registerHost
86
+
87
+ // 标记防重复包装:本插件重载后旧包装仍在路由表内,已标记的 handler 不再叠加,
88
+ // 经共享载体继续服务
89
+ const WRAPPED = 'autoTrustAllWrapped'
90
+ const wrap = (handler) => {
91
+ if (typeof handler !== 'function' || handler[WRAPPED]) return handler
92
+ const wrapped = async (...args) => {
93
+ try {
94
+ const register = webServer.autoTrustAllRegister
95
+ register(args[0])
96
+ } catch (error) {
97
+ // 注册是纯观察,失败不阻断请求;限频告警防风暴
98
+ if (!unregisterFailed) {
99
+ unregisterFailed = true
100
+ console.warn('auto-trust-all: 注册调用失败,后续不再提示: ' + (error instanceof Error ? error.message : String(error)))
101
+ }
102
+ }
103
+ return handler(...args)
104
+ }
105
+ wrapped[WRAPPED] = true
106
+ return wrapped
107
+ }
108
+ const wrapRoute = (route) => {
109
+ route.handler = wrap(route.handler)
110
+ }
111
+
112
+ // 回溯:激活前已注册的路由原地替换 handler,请求分发实时读该属性即生效
113
+ for (const route of webServer.exact.values()) wrapRoute(route)
114
+ for (const route of webServer.prefixes.values()) wrapRoute(route)
115
+ for (const route of webServer.upgrades.values()) wrapRoute(route)
116
+ if (webServer.fallback !== undefined) webServer.fallback = wrap(webServer.fallback)
117
+
118
+ // 遮蔽注册方法拦截后续注册;委托捕获时的当前值而非原型方法,与 startup-auth
119
+ // 的同类遮蔽在任意激活顺序下都保持链式,不绕过它的会话包装;影子自身带标记,
120
+ // 重复激活不叠加遮蔽层
121
+ const SHADOWED = 'autoTrustAllShadowed'
122
+ const shadowMethod = (method, delegate) => {
123
+ const previous = webServer[method]
124
+ if (typeof previous !== 'function' || previous[SHADOWED]) return
125
+ const shadow = (argument) => delegate(argument, previous)
126
+ shadow[SHADOWED] = true
127
+ webServer[method] = shadow
128
+ }
129
+ shadowMethod('register', (route, previous) => {
130
+ wrapRoute(route)
131
+ return previous.call(webServer, route)
132
+ })
133
+ shadowMethod('registerUpgrade', (route, previous) => {
134
+ wrapRoute(route)
135
+ return previous.call(webServer, route)
136
+ })
137
+ shadowMethod('registerFallback', (handler, previous) => previous.call(webServer, wrap(handler)))
138
+
139
+ // 启动横幅:绑定、容量与既有信任条目一屏可见
140
+ console.log('auto-trust-all: 动态信任已启用 (bind ' + webServer.host + ', 容量 ' + maxHosts + '), 既有信任 ' + initialHosts.length + ' 项: ' + (initialHosts.join(', ') || '无'))
141
+ }
142
+
143
+ if (ready()) {
144
+ activate()
145
+ } else {
146
+ // 冷启动时序:webRuntime 提供方与本插件在同一波激活中,其 fiber 完成激活
147
+ // (转 state 2)时会对自有服务发 internal/service,彼时 strict get 可解析
148
+ console.log('auto-trust-all: webRuntime 未就绪,待其激活后自动启用')
149
+ ctx.on('internal/service', (name) => {
150
+ if (name === 'webRuntime') activate()
151
+ })
152
+ }
153
+
154
+ // 卸载即断开注册:载体置空函数,旧代包装退化为纯透传,信任放行随插件移除停止
155
+ return () => {
156
+ activated = true
157
+ webServer.autoTrustAllRegister = () => {}
158
+ }
159
+ }
@@ -0,0 +1,30 @@
1
+ // patch 文件形态断言:覆盖 webserver 行的 config 是整体替换语义(不深合并),
2
+ // 本包必须镜像官方全部键。此测试只读本包 patch 文本,锁本地镜像形态快照——
3
+ // 它读不到官方安装目录,上游增删键不会在此失败;dsh 升级后须用
4
+ // `dsh web --dump-default-config` 对照官方 webserver 行核对键集。
5
+ import test from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { readFileSync } from 'node:fs'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { dirname, join } from 'node:path'
10
+
11
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
12
+ const patchText = readFileSync(join(PKG_ROOT, 'cordis.patch.yml'), 'utf8')
13
+
14
+ test('场景10 patch 形态: Given bundle patch Then webserver 行 name 防御且 host 默认翻转为全接口', () => {
15
+ // name 字段是 id 撞车防御:官方行改名即跳过并告警,而非误写他方行
16
+ assert.match(patchText, /- id: webserver\n name: '@deepseek-ai\/dsh-host-webserver'/)
17
+ assert.match(patchText, /host: !!js ctx\.webStartup\.host \?\? '0\.0\.0\.0'/)
18
+ })
19
+
20
+ test('场景10 镜像契约: config 整体替换要求官方行全部键在场', () => {
21
+ // 与官方 dsh-web-app bundle 的 webserver config 逐键镜像,官方增删键时此处失败
22
+ assert.match(patchText, /port: !!js ctx\.webStartup\.port \?\? 3080/)
23
+ assert.match(patchText, /compression: gzip/)
24
+ assert.match(patchText, /compressionLevel: 1/)
25
+ assert.match(patchText, /compressionThresholdBytes: 1024/)
26
+ })
27
+
28
+ test('场景10 插件行: insert 操作指向本包', () => {
29
+ assert.match(patchText, /- insert:\n - id: auto-trust-all\n name: '@mzzsfy\/dsh-auto-trust-all'/)
30
+ })
@@ -0,0 +1,414 @@
1
+ // 包装行为测试:mock webServer(实例形态与官方一致:三 Map + fallback 字段 +
2
+ // 原型注册方法)与 mock webRuntime/ctx,不起真服务器。
3
+ // Given-When-Then 场景见各用例注释,规格源 docs/design/dsh-auto-trust-all.md。
4
+ import test from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+
7
+ import { apply, Config } from '../src/index.js'
8
+
9
+ const DEFAULT_MAX_HOSTS = 100
10
+
11
+ class MockWebServer {
12
+ exact = new Map()
13
+ prefixes = new Map()
14
+ upgrades = new Map()
15
+ fallback = undefined
16
+
17
+ constructor(host = '0.0.0.0') {
18
+ this.host = host
19
+ }
20
+
21
+ register(route) {
22
+ const table = route.kind === 'exact' ? this.exact : this.prefixes
23
+ table.set(route.path, route)
24
+ return () => table.delete(route.path)
25
+ }
26
+
27
+ registerUpgrade(route) {
28
+ this.upgrades.set(route.path, route)
29
+ return () => this.upgrades.delete(route.path)
30
+ }
31
+
32
+ registerFallback(handler) {
33
+ this.fallback = handler
34
+ return () => { this.fallback = undefined }
35
+ }
36
+ }
37
+
38
+ // 构造最小插件上下文:webServer 静态注入,webRuntime 经 ctx.get 可选探测
39
+ // (与真实 cordis 语义一致),holder.current 支持测试中延迟就绪或换代;
40
+ // ctx.on 收集事件监听器供模拟触发;console 输出经 t.mock 捕获,测试结束自动还原
41
+ function createCtx({ trustedHosts = [], host = '0.0.0.0', webRuntimeReady = true } = {}) {
42
+ const webServer = new MockWebServer(host)
43
+ const holder = { current: webRuntimeReady ? { trustedHosts } : undefined }
44
+ const listeners = []
45
+ const ctx = {
46
+ webServer,
47
+ get: (name) => (name === 'webRuntime' ? holder.current : undefined),
48
+ on: (name, listener) => {
49
+ listeners.push([name, listener])
50
+ return () => {}
51
+ },
52
+ }
53
+ return { ctx, webServer, holder, trustedHosts, listeners }
54
+ }
55
+
56
+ const mockConsole = (t) => {
57
+ const output = []
58
+ t.mock.method(console, 'log', (...args) => output.push(['log', args.join(' ')]))
59
+ t.mock.method(console, 'warn', (...args) => output.push(['warn', args.join(' ')]))
60
+ return output
61
+ }
62
+
63
+ test('场景1 回溯包装: Given 激活前已注册路由 When 未注册 Host 请求到达 Then hostname 进信任清单且原 handler 透传', async (t) => {
64
+ const output = mockConsole(t)
65
+ const { ctx, webServer, trustedHosts } = createCtx()
66
+ const calls = []
67
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async (req, res) => { calls.push([req, res]) } })
68
+ webServer.register({ kind: 'prefix', path: '/api', handler: async () => { calls.push('prefix') } })
69
+ webServer.registerUpgrade({ path: '/ws', handler: async () => { calls.push('upgrade') } })
70
+
71
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
72
+
73
+ const route = webServer.exact.get('/api/x')
74
+ const req = { headers: { host: 'probe.jze100.com' } }
75
+ const res = {}
76
+ await route.handler(req, res)
77
+ await webServer.prefixes.get('/api').handler(req, res)
78
+ await webServer.upgrades.get('/ws').handler(req, {}, null)
79
+
80
+ assert.deepEqual(trustedHosts, ['probe.jze100.com'])
81
+ assert.equal(calls.length, 3)
82
+ assert.equal(calls[0][0], req)
83
+ assert.equal(calls[0][1], res)
84
+ assert.ok(output.some(([, message]) => message === 'auto-trust-all: registered host probe.jze100.com'))
85
+ })
86
+
87
+ test('场景2 去重: Given 同一 Host 重复请求 Then 数组只增一条', async (t) => {
88
+ mockConsole(t)
89
+ const { ctx, webServer, trustedHosts } = createCtx()
90
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
91
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
92
+
93
+ const route = webServer.exact.get('/api/x')
94
+ await route.handler({ headers: { host: 'probe.jze100.com' } }, {})
95
+ await route.handler({ headers: { host: 'probe.jze100.com' } }, {})
96
+ await route.handler({ headers: { host: 'probe.jze100.com:8443' } }, {})
97
+
98
+ assert.deepEqual(trustedHosts, ['probe.jze100.com'])
99
+ })
100
+
101
+ test('场景3 提取形态: Given 带端口或大小写或 IPv6 括号的 Host 头 Then 注册闸门比较形态的 hostname', async (t) => {
102
+ mockConsole(t)
103
+ const { ctx, webServer, trustedHosts } = createCtx()
104
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
105
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
106
+
107
+ const route = webServer.exact.get('/api/x')
108
+ await route.handler({ headers: { host: 'Probe.Jze100.com:8443' } }, {})
109
+ await route.handler({ headers: { host: '[::1]:3080' } }, {})
110
+
111
+ // WHATWG hostname 对 IPv6 保留方括号,与闸门 parseAuthority 的比较形态一致
112
+ assert.deepEqual(trustedHosts, ['probe.jze100.com', '[::1]'])
113
+ })
114
+
115
+ test('场景4 FIFO 容量: Given 达到 maxHosts When 新 Host 到达 Then 淘汰最早注册者且总量恒定', async (t) => {
116
+ mockConsole(t)
117
+ const { ctx, webServer, trustedHosts } = createCtx()
118
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
119
+ apply(ctx, { maxHosts: 2 })
120
+
121
+ const route = webServer.exact.get('/api/x')
122
+ await route.handler({ headers: { host: 'first.test' } }, {})
123
+ await route.handler({ headers: { host: 'second.test' } }, {})
124
+ await route.handler({ headers: { host: 'third.test' } }, {})
125
+
126
+ assert.deepEqual(trustedHosts, ['second.test', 'third.test'])
127
+ })
128
+
129
+ test('场景13 官方条目保护: Given 官方初始条目在场 When FIFO 淘汰 Then 只淘汰本插件注册的条目', async (t) => {
130
+ mockConsole(t)
131
+ const { ctx, webServer, trustedHosts } = createCtx({ trustedHosts: ['192.168.1.5'] })
132
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
133
+ apply(ctx, { maxHosts: 1 })
134
+
135
+ const route = webServer.exact.get('/api/x')
136
+ await route.handler({ headers: { host: 'first.test' } }, {})
137
+ await route.handler({ headers: { host: 'second.test' } }, {})
138
+
139
+ assert.deepEqual(trustedHosts, ['192.168.1.5', 'second.test'])
140
+ })
141
+
142
+ test('场景5 影子注册: Given 激活后新注册的路由 When 请求到达 Then 同样注册 Host 且注册语义不变', async (t) => {
143
+ mockConsole(t)
144
+ const { ctx, webServer, trustedHosts } = createCtx()
145
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
146
+
147
+ const disposer = webServer.register({ kind: 'exact', path: '/api/new', handler: async () => {} })
148
+ await webServer.exact.get('/api/new').handler({ headers: { host: 'late.jze100.com' } }, {})
149
+ disposer()
150
+
151
+ assert.deepEqual(trustedHosts, ['late.jze100.com'])
152
+ assert.equal(webServer.exact.has('/api/new'), false)
153
+ })
154
+
155
+ test('场景12 影子 upgrade: Given 激活后经影子方法注册的 upgrade 路由 When 请求到达 Then 同样注册 Host', async (t) => {
156
+ mockConsole(t)
157
+ const { ctx, webServer, trustedHosts } = createCtx()
158
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
159
+
160
+ webServer.registerUpgrade({ path: '/ws/new', handler: async () => {} })
161
+ await webServer.upgrades.get('/ws/new').handler({ headers: { host: 'ws.jze100.com' } }, {}, null)
162
+
163
+ assert.deepEqual(trustedHosts, ['ws.jze100.com'])
164
+ })
165
+
166
+ test('场景6a 影子链: Given 会话层先遮蔽注册方法 When 本插件激活后新路由注册 Then 会话包装与 Host 注册同时生效', async (t) => {
167
+ mockConsole(t)
168
+ const { ctx, webServer, trustedHosts } = createCtx()
169
+ // 模拟 startup-auth 先激活:实例属性遮蔽 register,包装 handler 模拟会话检查
170
+ const sessionChecked = []
171
+ const prototypeRegister = webServer.register
172
+ webServer.register = (route) => {
173
+ const original = route.handler
174
+ route.handler = async (req, res) => {
175
+ sessionChecked.push(req)
176
+ return original(req, res)
177
+ }
178
+ return prototypeRegister.call(webServer, route)
179
+ }
180
+
181
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
182
+ webServer.register({ kind: 'exact', path: '/api/auth', handler: async () => {} })
183
+ const req = { headers: { host: 'chained.jze100.com' } }
184
+ await webServer.exact.get('/api/auth').handler(req, {})
185
+
186
+ assert.deepEqual(trustedHosts, ['chained.jze100.com'])
187
+ assert.deepEqual(sessionChecked, [req])
188
+ })
189
+
190
+ test('场景6b 影子链: Given 本插件先激活 When 会话层后遮蔽注册方法 Then 两者包装叠加且 Host 注册在内层', async (t) => {
191
+ mockConsole(t)
192
+ const { ctx, webServer, trustedHosts } = createCtx()
193
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
194
+
195
+ // 模拟 startup-auth 后激活:遮蔽时捕获当前值(本插件的影子)并叠加会话包装
196
+ const sessionChecked = []
197
+ const previousRegister = webServer.register
198
+ webServer.register = (route) => {
199
+ const original = route.handler
200
+ route.handler = async (req, res) => {
201
+ sessionChecked.push(req)
202
+ return original(req, res)
203
+ }
204
+ return previousRegister.call(webServer, route)
205
+ }
206
+
207
+ webServer.register({ kind: 'exact', path: '/api/auth', handler: async () => {} })
208
+ const req = { headers: { host: 'stacked.jze100.com' } }
209
+ await webServer.exact.get('/api/auth').handler(req, {})
210
+
211
+ assert.deepEqual(trustedHosts, ['stacked.jze100.com'])
212
+ assert.deepEqual(sessionChecked, [req])
213
+ })
214
+
215
+ test('场景7 fallback: Given 回溯与影子注册两条路径的 fallback 请求 Then 同样注册 Host', async (t) => {
216
+ mockConsole(t)
217
+ const { ctx, webServer, trustedHosts } = createCtx()
218
+ const existing = async () => {}
219
+ webServer.fallback = existing
220
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
221
+
222
+ await webServer.fallback({ headers: { host: 'via-fallback.jze100.com' } }, {})
223
+ webServer.registerFallback(async () => {})
224
+ await webServer.fallback({ headers: { host: 'via-shadow.jze100.com' } }, {})
225
+
226
+ assert.deepEqual(trustedHosts, ['via-fallback.jze100.com', 'via-shadow.jze100.com'])
227
+ })
228
+
229
+ test('场景8 脏输入: Given 无 Host 头或非法 Host 头 When 请求到达 Then 不注册不抛错且原 handler 照常执行', async (t) => {
230
+ mockConsole(t)
231
+ const { ctx, webServer, trustedHosts } = createCtx()
232
+ const calls = []
233
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async (req) => { calls.push(req) } })
234
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
235
+
236
+ const route = webServer.exact.get('/api/x')
237
+ await route.handler({ headers: {} }, {})
238
+ // 空格与超范围端口都是 WHATWG 解析拒绝的形态,闸门 parseAuthority 同样返回 undefined
239
+ await route.handler({ headers: { host: 'bad host' } }, {})
240
+ await route.handler({ headers: { host: 'host:99999' } }, {})
241
+ await route.handler({}, {})
242
+
243
+ assert.deepEqual(trustedHosts, [])
244
+ assert.equal(calls.length, 4)
245
+ })
246
+
247
+ test('场景9 幂等重载: Given 本插件重复激活(HMR 重载模拟) When 再次激活 Then 路由 handler 与影子方法都不叠加', async (t) => {
248
+ mockConsole(t)
249
+ const { ctx, webServer, trustedHosts } = createCtx()
250
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
251
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
252
+ const wrappedOnce = webServer.exact.get('/api/x').handler
253
+ const shadowOnce = webServer.register
254
+ const fallbackShadowOnce = webServer.registerFallback
255
+
256
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
257
+
258
+ assert.equal(webServer.exact.get('/api/x').handler, wrappedOnce)
259
+ assert.equal(webServer.register, shadowOnce)
260
+ assert.equal(webServer.registerFallback, fallbackShadowOnce)
261
+ await webServer.exact.get('/api/x').handler({ headers: { host: 'reload.jze100.com' } }, {})
262
+ assert.deepEqual(trustedHosts, ['reload.jze100.com'])
263
+ })
264
+
265
+ test('场景11 启动横幅: Given 插件激活 Then console 输出绑定、容量与既有信任条目', (t) => {
266
+ const output = mockConsole(t)
267
+ const { ctx } = createCtx({ trustedHosts: ['192.168.1.5', 'lan.example.com'] })
268
+
269
+ apply(ctx, { maxHosts: 50 })
270
+
271
+ const banner = output.map(([, message]) => message).find((message) => message.includes('动态信任已启用'))
272
+ assert.ok(banner !== undefined)
273
+ assert.ok(output.some(([, message]) => message.includes('bind 0.0.0.0')))
274
+ assert.ok(output.some(([, message]) => message.includes('容量 50')))
275
+ assert.ok(output.some(([, message]) => message.includes('192.168.1.5, lan.example.com')))
276
+ })
277
+
278
+ test('场景11b 启动横幅空清单: Given 无既有条目 Then 输出"无"占位', (t) => {
279
+ const output = mockConsole(t)
280
+ const { ctx } = createCtx({ trustedHosts: [] })
281
+
282
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
283
+
284
+ assert.ok(output.some(([, message]) => message.includes('既有信任 0 项: 无')))
285
+ })
286
+
287
+ test('场景14 数组重建: Given webRuntime 提供新数组 When 已注册域名再次到达 Then 重新登记进新数组且队列随代重置', async (t) => {
288
+ mockConsole(t)
289
+ const { ctx, webServer, holder, trustedHosts } = createCtx()
290
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
291
+ apply(ctx, { maxHosts: 1 })
292
+
293
+ const route = webServer.exact.get('/api/x')
294
+ await route.handler({ headers: { host: 'first.test' } }, {})
295
+ assert.deepEqual(trustedHosts, ['first.test'])
296
+
297
+ // 模拟 web-app 行重载:webRuntime 重建,新数组只含官方初始条目
298
+ const rebuilt = ['10.0.0.2']
299
+ holder.current = { trustedHosts: rebuilt }
300
+ await route.handler({ headers: { host: 'first.test' } }, {})
301
+ assert.deepEqual(rebuilt, ['10.0.0.2', 'first.test'])
302
+
303
+ // 新一代容量 1 已满:second.test 到达淘汰 first.test,证明队列已随代重置
304
+ await route.handler({ headers: { host: 'second.test' } }, {})
305
+ assert.deepEqual(rebuilt, ['10.0.0.2', 'second.test'])
306
+ })
307
+
308
+ test('场景15 容量下界: Given maxHosts 小于 1 Then schema 校验拒绝,缺省解出默认 100', () => {
309
+ assert.ok(Config['~standard'].validate({ maxHosts: 0 }).issues)
310
+ assert.ok(Config['~standard'].validate({ maxHosts: -1 }).issues)
311
+ assert.ok(Config['~standard'].validate({ maxHosts: 1.5 }).issues)
312
+ assert.equal(Config['~standard'].validate({}).value.maxHosts, 100)
313
+ })
314
+
315
+ test('场景16 淘汰重访: Given 条目被 FIFO 淘汰 When 该域名再次到达 Then 重新注册(不被去重记忆拉黑)', async (t) => {
316
+ mockConsole(t)
317
+ const { ctx, webServer, trustedHosts } = createCtx()
318
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
319
+ apply(ctx, { maxHosts: 1 })
320
+
321
+ const route = webServer.exact.get('/api/x')
322
+ await route.handler({ headers: { host: 'first.test' } }, {})
323
+ await route.handler({ headers: { host: 'second.test' } }, {})
324
+ assert.deepEqual(trustedHosts, ['second.test'])
325
+
326
+ await route.handler({ headers: { host: 'first.test' } }, {})
327
+ assert.deepEqual(trustedHosts, ['first.test'])
328
+ })
329
+
330
+ test('场景17 换代移交: Given 行级 config 变更触发重跑 apply When 请求到达已包装路由 Then 注册走新一代容量', async (t) => {
331
+ mockConsole(t)
332
+ const { ctx, webServer, trustedHosts } = createCtx()
333
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
334
+ const dispose1 = apply(ctx, { maxHosts: 1 })
335
+
336
+ const route = webServer.exact.get('/api/x')
337
+ const wrappedOnce = route.handler
338
+ await route.handler({ headers: { host: 'first.test' } }, {})
339
+
340
+ // 模拟行级 config 变更:先卸载(carrier 置空)再重跑 apply,handler 带标记跳过
341
+ // 重包装,但共享载体被新代覆盖,旧包装经载体用新容量注册
342
+ dispose1()
343
+ assert.equal(webServer.autoTrustAllRegister('x'), undefined)
344
+ apply(ctx, { maxHosts: 2 })
345
+ assert.equal(route.handler, wrappedOnce)
346
+
347
+ await route.handler({ headers: { host: 'second.test' } }, {})
348
+ await route.handler({ headers: { host: 'third.test' } }, {})
349
+ // 新代只记自身条目:second/third 共 2 条未超容量,first 属上代遗留视同官方条目
350
+ assert.deepEqual(trustedHosts, ['first.test', 'second.test', 'third.test'])
351
+
352
+ await route.handler({ headers: { host: 'fourth.test' } }, {})
353
+ // 新代容量 2 生效:fourth 淘汰新代最早的 second,遗留条目不参与记账
354
+ assert.deepEqual(trustedHosts, ['first.test', 'third.test', 'fourth.test'])
355
+ })
356
+
357
+ test('场景18 延迟激活: Given 激活时 webRuntime 未就绪 When 挂事件监听且服务就绪事件到达 Then 自动完成激活', async (t) => {
358
+ const output = mockConsole(t)
359
+ const { ctx, webServer, holder, trustedHosts, listeners } = createCtx({ webRuntimeReady: false })
360
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
361
+ const handlerBefore = webServer.exact.get('/api/x').handler
362
+
363
+ const dispose = apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
364
+
365
+ // 未就绪路径:不包装、不挂载体,挂 internal/service 监听并输出等待行
366
+ assert.equal(webServer.exact.get('/api/x').handler, handlerBefore)
367
+ assert.equal(webServer.autoTrustAllRegister, undefined)
368
+ assert.deepEqual(listeners.map(([name]) => name), ['internal/service'])
369
+ assert.ok(output.some(([, message]) => message.includes('webRuntime 未就绪')))
370
+
371
+ // 模拟 web-app fiber 激活完成:runtime 就绪并发出服务事件
372
+ holder.current = { trustedHosts: [] }
373
+ const [, listener] = listeners[0]
374
+ listener('webRuntime')
375
+ assert.notEqual(webServer.exact.get('/api/x').handler, handlerBefore)
376
+ assert.equal(typeof webServer.autoTrustAllRegister, 'function')
377
+ assert.ok(output.some(([, message]) => message.includes('动态信任已启用')))
378
+
379
+ await webServer.exact.get('/api/x').handler({ headers: { host: 'late.jze100.com' } }, {})
380
+ assert.deepEqual(holder.current.trustedHosts, ['late.jze100.com'])
381
+ dispose()
382
+ assert.equal(webServer.autoTrustAllRegister('x'), undefined)
383
+ })
384
+
385
+ test('场景20 卸载断开: Given 已激活 When 卸载 Then 载体置空,后续请求纯透传不再注册', async (t) => {
386
+ mockConsole(t)
387
+ const { ctx, webServer, trustedHosts } = createCtx()
388
+ webServer.register({ kind: 'exact', path: '/api/x', handler: async () => {} })
389
+ const dispose = apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
390
+
391
+ const route = webServer.exact.get('/api/x')
392
+ await route.handler({ headers: { host: 'before.test' } }, {})
393
+ assert.deepEqual(trustedHosts, ['before.test'])
394
+
395
+ dispose()
396
+ await route.handler({ headers: { host: 'after.test' } }, {})
397
+ assert.deepEqual(trustedHosts, ['before.test'])
398
+ })
399
+
400
+ test('场景21 激活幂等: Given 事件路径激活后服务再次发事件 When 重复触发 Then 不重复激活(横幅与载体不换代)', (t) => {
401
+ const output = mockConsole(t)
402
+ const { ctx, webServer, holder, listeners } = createCtx({ webRuntimeReady: false })
403
+ apply(ctx, { maxHosts: DEFAULT_MAX_HOSTS })
404
+
405
+ holder.current = { trustedHosts: ['10.0.0.9'] }
406
+ const [, listener] = listeners[0]
407
+ listener('webRuntime')
408
+ const carrier = webServer.autoTrustAllRegister
409
+
410
+ listener('webRuntime')
411
+
412
+ assert.equal(webServer.autoTrustAllRegister, carrier)
413
+ assert.equal(output.filter(([, message]) => message.includes('动态信任已启用')).length, 1)
414
+ })