@iasiv5/dsh-skip-browser-auth 0.1.2 → 0.2.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/LICENSE CHANGED
@@ -1,7 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 DeepSeek
4
- Copyright (c) 2026 iasi (@iasiv5/dsh-skip-browser-auth 适配与新增部分)
3
+ Copyright (c) 2026 iasi
5
4
 
6
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
6
  of this software and associated documentation files (the "Software"), to deal
@@ -13,11 +12,6 @@ furnished to do so, subject to the following conditions:
13
12
  The above copyright notice and this permission notice shall be included in all
14
13
  copies or substantial portions of the Software.
15
14
 
16
- 本插件包含或移植 DeepSeek Harness(@deepseek-ai/dsh-client-connection 等)的
17
- MIT 许可代码:lib/client.js 为官方发布产物的不可变 re-ID 副本,src/trust-fence.ts
18
- 与 src/bridge.ts 移植自官方源码,src/index.ts、src/trusted-auth.ts 基于官方
19
- Connection 实现语义。上游版权与许可声明如上保留。
20
-
21
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
package/NOTICE ADDED
@@ -0,0 +1,40 @@
1
+ Third-party notices
2
+ ===================
3
+
4
+ This package includes or adapts MIT-licensed code from DeepSeek Harness
5
+ (@deepseek-ai/dsh-client-connection and related packages, the "Upstream
6
+ Code"):
7
+
8
+ - lib/client.js is an unmodified copy of the official published client
9
+ bundle, re-identified only by its registration id;
10
+ - src/trust-fence.ts and src/bridge.ts are ported from the official
11
+ source;
12
+ - src/index.ts and src/trusted-auth.ts follow the semantics of the
13
+ official Connection implementation.
14
+
15
+ The Upstream Code is Copyright (c) 2026 DeepSeek. It is distributed in
16
+ this package under the MIT License, whose full text follows and applies
17
+ to the Upstream Code in its entirety.
18
+
19
+ ---
20
+ MIT License
21
+
22
+ Copyright (c) 2026 DeepSeek
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # @iasiv5/dsh-skip-browser-auth
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@iasiv5/dsh-skip-browser-auth?label=npm&color=cb3837)](https://www.npmjs.com/package/@iasiv5/dsh-skip-browser-auth)
4
- [![DSH Web](https://img.shields.io/badge/DSH_Web-0.1.2--rc.1_verified-blue)](#兼容版本)
4
+ [![DSH Web](https://img.shields.io/badge/DSH_Web-0.1.2--rc.1_%7C_0.1.5--rc.1_verified-blue)](#兼容版本)
5
5
  [![License](https://img.shields.io/github/license/iasiv5/dsh-skip-browser-auth?color=green)](./LICENSE)
6
6
 
7
- 个人自用的 DSH Web 插件:在 **DSH 0.1.2-rc.1** 上自动跳过 BrowserAuth——打开 Web 地址即可直接使用,**不必每次把启动 URL 里那串随机 token 抄进浏览器**。
7
+ 个人自用的 DSH Web 插件:在已验证的 DSH compatibility profile 上自动跳过 BrowserAuth,打开 Web 地址即可直接使用,**不必每次把启动 URL 里那串随机 token 抄进浏览器**。
8
8
 
9
- - **只在适配版本激活**:当前仅 `0.1.2-rc.1`。在 `0.1.1-rc.2` 及其它所有未测试版本上**自动休眠**,官方行为分毫不变。
9
+ - **精确 profile 门控**:当前支持 `0.1.2-rc.1` 与 `0.1.5-rc.1`,但两者使用不同的 host/client 兼容代际;不是把两个版本塞进同一个实现。
10
+ - **严格成对匹配**:runtime `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-client-connection` 必须组成已知版本对。混合版本、未知版本、profile 未实现或解析失败都会自动休眠。
11
+ - **分代适配**:`0.1.2-rc.1` 使用 `legacy-web-v1`;`0.1.5-rc.1` 使用 `carrier-neutral-v2`,包含 request body mode、RPC carrier 和对应浏览器 client variant 适配。
10
12
  - **不修改 DSH 本体与任何机制**:仅通过 DSH 官方插件 patch 机制替换 Connection 行;卸载或禁用插件并重启,即完全恢复官方行为。
11
- - **提醒**:跳过认证后,Web 对本机/可信网络内的访问不再有身份层,请勿把 DSH 端口暴露给不可信网络。
13
+ - **安全提醒**:跳过认证后,Web 对本机/可信网络内的访问不再有身份层,请勿把 DSH 端口暴露给不可信网络。
14
+
15
+ > 0.1.5 的官方 Connection 在 carrier-neutral 重构后出现过 `connection.rpc.handle()` 返回 HTTP 405 的回归。本插件对 0.1.5 使用独立 adapter,并通过真实 npm fixture 覆盖 dedicated RPC 与 exact Fetch route;GitHub 审计与根因见 [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md)。
12
16
 
13
17
  ## 安装
14
18
 
@@ -27,22 +31,23 @@ sudo systemctl restart deepseek-harness.service # 按你的部署方式重启
27
31
 
28
32
  ### 复制给 agent 的安装提示词
29
33
 
30
- 把下面整段贴给你机器上的 agent,它就能完成安装与验证:
31
-
32
34
  ```text
33
- 请给我的 DSH Web 安装 @iasiv5/dsh-skip-browser-auth 插件(DSH 0.1.2-rc.1 专用:
34
- 自动跳过 BrowserAuth,免每次在启动 URL 后输入随机 token;其它版本会自动休眠,
35
- 不影响任何行为)。
35
+ 请给我的 DSH Web 安装 @iasiv5/dsh-skip-browser-auth 插件。
36
+ 它支持两个精确 compatibility profile:
37
+ - DSH 0.1.2-rc.1:legacy-web-v1
38
+ - DSH 0.1.5-rc.1:carrier-neutral-v2
39
+ 其它版本或 runtime/connection 混合版本会自动休眠,不要强行处理。
40
+
36
41
  1. 先执行 dsh --version 告诉我当前 DSH 版本,然后照常安装:
37
42
  dsh plugin --profile web add @iasiv5/dsh-skip-browser-auth
38
43
  2. 重启 DSH Web 并轮询 http://127.0.0.1:3080 直到恢复 200
39
44
  (systemd 系统级部署:sudo systemctl restart deepseek-harness.service;
40
45
  user 级部署:systemctl --user restart dsh-web.service;端口以实际部署为准)。
41
- 3. 若版本是 0.1.2-rc.1:确认 journal/日志中出现固定警告
42
- "BrowserAuth has been skipped",且浏览器直接打开 Web 地址不再要求 token,
43
- 即激活成功。
44
- 4. 若版本不是 0.1.2-rc.1:插件会自动休眠(官方行为不变),直接告诉我即可,
45
- 不要做任何强行处理。
46
+ 3. 若版本是 0.1.2-rc.1 或 0.1.5-rc.1:
47
+ 确认 journal/日志中出现固定警告
48
+ "BrowserAuth has been skipped",并验证浏览器直接打开 Web 地址不再要求 token。
49
+ 4. 若版本不是精确白名单版本,或 profile/依赖契约不匹配:插件应自动休眠,
50
+ 保持官方行为,不要修改 DSH 本体或强行覆盖版本检测。
46
51
  ```
47
52
 
48
53
  ## 卸载
@@ -54,21 +59,63 @@ sudo systemctl restart deepseek-harness.service
54
59
 
55
60
  市场安装的用户在「插件市场 → 已装」里点卸载即可。卸载或禁用并重启后,官方 BrowserAuth 完全恢复。
56
61
 
57
- ## 兼容版本
62
+ ## 兼容版本与 profile
63
+
64
+ | DSH runtime / connection 版本对 | Profile | Host/client 适配 | 状态 |
65
+ | --- | --- | --- | --- |
66
+ | `0.1.2-rc.1` + `0.1.2-rc.1` | `legacy-web-v1` | legacy `/api` prefix + 0.1.2 client | ✅ active |
67
+ | `0.1.5-rc.1` + `0.1.5-rc.1` | `carrier-neutral-v2` | carrier-neutral HostConnectionService + 0.1.5 client | ✅ active(代码/真实 npm fixture 已验证;真实部署仍需按清单确认) |
68
+ | 其它版本或混合版本对 | — | 😴 自动休眠:官方行为分毫不变 | dormant |
69
+
70
+ 白名单匹配的是**完整版本对**,不是两个独立的 `includes()`:
71
+
72
+ ```text
73
+ runtime=0.1.2-rc.1 + connection=0.1.5-rc.1 → dormant
74
+ runtime=0.1.5-rc.1 + connection=0.1.5-rc.1 → carrier-neutral-v2
75
+ ```
76
+
77
+ 未来的新版本如果保持同一契约,可以加入对应 profile 的精确版本对;如果发生 API、DI、route、body 或 client bundle 契约变化,必须新增 profile、host adapter 和 client variant,不能 fallback 到旧代际。详细流程见 [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md)。
78
+
79
+ ## 机制与安全
58
80
 
59
- | DSH 版本 | 状态 |
60
- | --- | --- |
61
- | `0.1.2-rc.1` | ✅ 激活:跳过 BrowserAuth(pnpm 与 npm 扁平安装布局均可识别) |
62
- | `0.1.1-rc.2` 及其它未测试版本 | 😴 自动休眠:官方行为分毫不变 |
81
+ - 组合期版本探针使用 runtime 本体与官方 connection 包双锚点,并返回 profile id;路径段和 npm flat manifest 两种布局均支持。任何异常都 fail-closed 到 dormant。
82
+ - `apply()` backstop 再读取两个 manifest,验证 active profile、patch 传入的 profile id、官方 connection sibling 行的 id/name/disabled/fiber 状态;不满足时在创建服务、注册 route、输出 warning 前 fail loud。
83
+ - `legacy-web-v1` 保留 0.1.2 的 `/api` prefix 与 buffered bridge。
84
+ - `carrier-neutral-v2` 使用 runtime loader 取得当前 0.1.5 connection module,支持 `requestBodyMode`、exact Fetch route 和 streaming body,并将 generic `connection.rpc.handle()` 绑定到拥有 Web carrier 的 replacement context,避免 0.1.5 的 405 回归。
85
+ - client bundle 是显式 profile dispatcher:host 通过 `webserver/index-inject` 注入 profile id,0.1.2/0.1.5 使用各自官方 factory;profile 缺失或未知时不猜测、不加载错误 variant。
86
+ - 激活时进程逐字输出固定警告:
63
87
 
64
- 新版本经实测验证后逐个加入白名单;版本探测失败一律自动休眠(fail-closed)。
88
+ ```text
89
+ @iasiv5/dsh-skip-browser-auth: BrowserAuth has been skipped. DSH Web is using the trusted-network behavior; this plugin does not verify any upstream proxy.
90
+ ```
91
+
92
+ trusted-network mode 的判据只有一个:**请求来源是否通过信任栅栏**(Host/Origin/Fetch Metadata/trustedHosts)。能通过栅栏的请求一律被接受,不存在身份层,本插件也不校验任何上游反代。
93
+
94
+ ## 测试与验证
95
+
96
+ ```sh
97
+ # 安装真实 rc.2 + rc.15 fixture,并运行 build/typecheck/全部 Node tests
98
+ npm run test:all
99
+
100
+ # 单独重建与加载 host
101
+ rm -rf lib
102
+ npm run build
103
+ node --input-type=module -e "await import('./lib/host.js')"
104
+
105
+ # 确认 patch 生成幂等
106
+ npm run build
107
+ git diff --exit-code cordis.patch.yml
108
+ ```
65
109
 
66
- ## 机制与安全(简述)
110
+ 测试矩阵覆盖:
67
111
 
68
- - 组合期版本探针采用**双锚点**(宿主 runtime 本体 + 官方 connection 包)与**双分支**版本确认(pnpm 路径段 / manifest 读取,兼容两种安装布局),精确匹配白名单才激活;任何解析失败、版本漂移、结构漂移都保持休眠。
69
- - 激活后复用官方导出的 `HostConnectionService` 提供 trusted-network Connection:Host/Origin/trustedHosts 请求信任栅栏完整保留,仅移除身份层;激活时进程逐字输出固定警告 `@iasiv5/dsh-skip-browser-auth: BrowserAuth has been skipped. ...`。
70
- - 运行前 backstop 复核 manifest 白名单与行绑定状态,任何不满足即拒绝运行。
112
+ - 两个完整 profile pair、混合版本、未知版本和 manifest/path drift
113
+ - pnpm 路径段与 npm flat manifest fallback
114
+ - legacy buffered bridge 与 carrier-neutral streaming bridge
115
+ - 真实 0.1.5 dedicated `connection.rpc.handle()`、exact Fetch route 和 405 回归
116
+ - 两个官方 client factory 的 materialization、profile dispatcher 和未知 profile fail-closed
117
+ - rc.2 dormant 基线、dispose/unload、Host/Origin/Fetch Metadata trust fence
71
118
 
72
- 详细设计、行为矩阵与实机验证记录见 [docs/DESIGN.md](./docs/DESIGN.md)。
119
+ 完整的 GitHub API 审计、profile 数据模型、adapter seam、client 变体和未来新增代际 checklist 见 [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md);机制详版见 [docs/DESIGN.md](./docs/DESIGN.md)。
73
120
 
74
- 本插件包含或移植 DeepSeek Harness 的 MIT 许可代码,详见 [LICENSE](./LICENSE)。
121
+ 本插件以 [MIT License](./LICENSE) 发布;包含或移植的 DeepSeek Harness(MIT)代码,其版权与许可声明见 [NOTICE](./NOTICE)。
package/cordis.patch.yml CHANGED
@@ -1,17 +1,18 @@
1
1
  # 本文件由 scripts/write-patch.mjs 从 lib/gate.js 常量机械生成,请勿手改。
2
2
  # 自门控 bundle patch:第一行把官方 connection 行的 disabled 绑定到版本探针
3
3
  # (双锚点:宿主 runtime 本体 @deepseek-ai/dsh 与官方 connection 包的 pnpm
4
- # 路径段版本必须同为 0.1.2-rc.1;runtime 锚点只可能来自 runtime fallback,
5
- # 不会被插件自身依赖污染);第二行插入本插件的 trusted-connection 行,其
6
- # disabled 为行绑定表达式的否定(行绑定内嵌同一探针)。
4
+ # runtime/connection 必须命中同一个 active compatibility profile;runtime 锚点
5
+ # 只可能来自 runtime fallback,不会被插件自身依赖污染);第二行插入本插件的
6
+ # trusted-connection 行,其 config 携带 profile id,disabled 为行绑定表达式的否定。
7
7
  # 任何门控条件不成立(锚点解析失败、版本漂移、行不存在、名字漂移、
8
8
  # 官方行未禁用),Replacement 一律保持 dormant(fail-closed)。探针为假时,
9
9
  # 即使后续 patch 层把官方行强制 disabled,行绑定内嵌探针仍保持 dormant。
10
10
  - id: connection
11
11
  name: '@deepseek-ai/dsh-client-connection'
12
12
  disabled: !!js >-
13
- (() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return false; const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof
14
- manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return false; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return false; } if (anchorVersion !== '0.1.2-rc.1') return false; } return true; } catch (gateError) { return false; } })()
13
+ (() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof
14
+ manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"}]}]; for (const profile of profiles)
15
+ { if (profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()); return profile !== null && ["legacy-web-v1","carrier-neutral-v2"].includes(profile); } catch (gateError) { return false; } })()
15
16
  - insert:
16
17
  - id: trusted-connection
17
18
  name: '@iasiv5/dsh-skip-browser-auth'
@@ -19,7 +20,11 @@
19
20
  - webRuntime
20
21
  config:
21
22
  trustedHosts: !!js ctx.webRuntime.trustedHosts
23
+ compatibilityProfile: !!js >-
24
+ (() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof
25
+ manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"}]}]; for (const profile of profiles) { if
26
+ (profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()
22
27
  disabled: !!js >-
23
- !((() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '@deepseek-ai/dsh-client-connection') return false; const probeAllowed = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return false; const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule =
24
- processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return false; } let anchorVersion; try { anchorVersion = versionOfUrl(url,
25
- patternSource); } catch (gateError) { return false; } if (anchorVersion !== '0.1.2-rc.1') return false; } return true; } catch (gateError) { return false; } })()); if (probeAllowed !== true) return false; return official.disabled === true; } catch (gateError) { return false; } })())
28
+ !((() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '@deepseek-ai/dsh-client-connection') return false; const probeAllowed = ((() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function')
29
+ return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let
30
+ anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"}]}]; for (const profile of profiles) { if (profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()); return profile !== null && ["legacy-web-v1","carrier-neutral-v2"].includes(profile); } catch (gateError) { return false; } })()); if (probeAllowed !== true) return false; return official.disabled === true; } catch (gateError) { return false; } })())
@@ -0,0 +1,23 @@
1
+ import { createHostRuntime } from './types.js';
2
+ /** Host implementation for the 0.1.5 carrier-neutral connection contract. */
3
+ export const HOST_ADAPTER_ID = 'carrier-neutral';
4
+ export function createCarrierNeutralRuntime(input) {
5
+ const runtime = createHostRuntime(input, HOST_ADAPTER_ID);
6
+ if (typeof runtime.fetchHandler.requestBodyMode !== 'function') {
7
+ throw new Error('@iasiv5/dsh-skip-browser-auth: carrier-neutral profile requires a requestBodyMode-aware connection handler');
8
+ }
9
+ // 0.1.5's carrier-neutral service keeps generic rpc.handle() registrations
10
+ // on the reading context, which is often a sibling plugin without webServer.
11
+ // Bind only this replacement instance to the adapter's carrier context. This
12
+ // is the narrow equivalent of the upstream workaround and leaves fetch
13
+ // registry ownership untouched.
14
+ const service = runtime.connection;
15
+ const originalRegister = service.register;
16
+ if (typeof originalRegister !== 'function') {
17
+ throw new Error('@iasiv5/dsh-skip-browser-auth: carrier-neutral connection lacks its RPC register seam');
18
+ }
19
+ service.register = function registerWithCarrier(_owner, channel, handler) {
20
+ return originalRegister.call(this, input.ctx, channel, handler);
21
+ };
22
+ return runtime;
23
+ }
@@ -0,0 +1,6 @@
1
+ import { createHostRuntime } from './types.js';
2
+ /** Host implementation for the 0.1.2-rc.1 web-carrier contract. */
3
+ export const HOST_ADAPTER_ID = 'legacy-web';
4
+ export function createLegacyWebRuntime(input) {
5
+ return createHostRuntime(input, HOST_ADAPTER_ID);
6
+ }
@@ -0,0 +1,20 @@
1
+ import { bridge } from '../bridge.js';
2
+ /** Construct the common trusted /api carrier route for one compatibility adapter. */
3
+ export function createHostRuntime(input, adapterId) {
4
+ const connection = new input.module.HostConnectionService(input.ctx, input.trustedHosts, input.auth);
5
+ const fetchHandler = connection.createSharedFetchHandler(input.module.API_PATH);
6
+ const route = {
7
+ kind: 'prefix',
8
+ path: input.module.API_PATH,
9
+ handler: async (req, res) => {
10
+ const rejection = connection.requestRejection(req);
11
+ if (rejection !== undefined) {
12
+ res.writeHead(rejection);
13
+ res.end(rejection === 401 ? 'unauthorized' : 'forbidden');
14
+ return;
15
+ }
16
+ await bridge(req, res, fetchHandler, input.maxRequestBodyBytes);
17
+ },
18
+ };
19
+ return { connection, route, fetchHandler, adapterId };
20
+ }
package/lib/bridge.js CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * 移植自官方 packages/client/connection/src/http-bridge.ts(rc.1),语义照抄。
6
6
  */
7
+ import { Readable } from 'node:stream';
7
8
  /** Default carrier cap for all HTTP RPC bodies: sized for the default
8
9
  * aggregate image limit (200 MiB) after base64 expansion plus envelope
9
10
  * headroom (~267.7 MiB required), rounded up for slack. The bridge buffers
@@ -28,38 +29,60 @@ export async function bridge(req, res, apiHandler, maxRequestBodyBytes = DEFAULT
28
29
  if (!res.writableEnded)
29
30
  abort.abort();
30
31
  });
31
- const declaredLength = req.headers['content-length'];
32
- if (declaredLength !== undefined && Number(declaredLength) > maxRequestBodyBytes) {
33
- res.writeHead(413, { connection: 'close' });
34
- res.end();
35
- req.destroy();
36
- return;
37
- }
38
- const chunks = [];
39
- let received = 0;
40
- for await (const chunk of req) {
41
- const buffer = chunk;
42
- received += buffer.byteLength;
43
- if (received > maxRequestBodyBytes) {
32
+ /* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
33
+ requests; the fields are only optional on the client-side IncomingMessage type */
34
+ const url = new URL(req.url ?? '/', 'http://dsh.internal');
35
+ const method = req.method ?? 'GET';
36
+ const headers = Object.fromEntries(Object.entries(req.headers).filter(([, value]) => typeof value === 'string'));
37
+ const bodyMode = apiHandler.requestBodyMode?.({ method, url }) ?? 'buffered';
38
+ let request;
39
+ if (bodyMode === 'buffered') {
40
+ const declaredLength = req.headers['content-length'];
41
+ if (declaredLength !== undefined && Number(declaredLength) > maxRequestBodyBytes) {
44
42
  res.writeHead(413, { connection: 'close' });
45
43
  res.end();
46
44
  req.destroy();
47
45
  return;
48
46
  }
49
- chunks.push(buffer);
47
+ const chunks = [];
48
+ let received = 0;
49
+ for await (const chunk of req) {
50
+ const buffer = chunk;
51
+ received += buffer.byteLength;
52
+ if (received > maxRequestBodyBytes) {
53
+ res.writeHead(413, { connection: 'close' });
54
+ res.end();
55
+ req.destroy();
56
+ return;
57
+ }
58
+ chunks.push(buffer);
59
+ }
60
+ request = new Request(url, {
61
+ method,
62
+ headers,
63
+ ...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {},
64
+ signal: abort.signal,
65
+ });
66
+ }
67
+ else {
68
+ request = new Request(url, {
69
+ method,
70
+ headers,
71
+ body: Readable.toWeb(req),
72
+ signal: abort.signal,
73
+ duplex: 'half',
74
+ });
50
75
  }
51
- /* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
52
- requests; the fields are only optional on the client-side IncomingMessage type */
53
- const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), {
54
- method: req.method ?? 'GET',
55
- headers: Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === 'string')),
56
- ...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {},
57
- signal: abort.signal,
58
- });
59
76
  const response = await apiHandler.fetch(request);
60
- res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
77
+ const requestUnread = bodyMode === 'streaming' && !req.readableEnded;
78
+ const responseHeaders = Object.fromEntries(response.headers.entries());
79
+ res.writeHead(response.status, requestUnread
80
+ ? { ...responseHeaders, connection: 'close' }
81
+ : responseHeaders);
61
82
  if (response.body === null) {
62
83
  res.end();
84
+ if (requestUnread)
85
+ req.destroy();
63
86
  return;
64
87
  }
65
88
  for await (const chunk of response.body) {
@@ -80,4 +103,6 @@ export async function bridge(req, res, apiHandler, maxRequestBodyBytes = DEFAULT
80
103
  }
81
104
  }
82
105
  res.end();
106
+ if (requestUnread)
107
+ req.destroy();
83
108
  }