dsh-webui-oauth 0.7.2 → 0.7.3
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 +38 -0
- package/README.en.md +30 -1
- package/README.md +12 -0
- package/index.js +70 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录每个版本的**用户可见**变更。破坏性变更与升级动作单独标注。
|
|
4
4
|
|
|
5
|
+
## 0.7.3
|
|
6
|
+
|
|
7
|
+
整合 `dsh-remote-trust`,并确认与官方 DeepSeek 账号登录(0.2.1+)的兼容边界。
|
|
8
|
+
|
|
9
|
+
### 新增:远程访问信任(可选内置,原 `dsh-remote-trust`)
|
|
10
|
+
|
|
11
|
+
经域名反代访问时,客户端把页面判为"非 loopback",设置 → 模型报
|
|
12
|
+
`settings are unavailable in this browser`。开启:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
remote-web-ui:
|
|
16
|
+
trustRemoteHost: true
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- 通过官方 `webServer.tapIndex` 注入 `__DSH_TRANSPORT__.ownsHost = true`;
|
|
20
|
+
- 注入**幂等**:与 `dsh-remote-trust` 同时安装不会重复;
|
|
21
|
+
- `tapIndex` 不存在时只记一条 warn 并返回 false,插件其余功能照常(版本兼容);
|
|
22
|
+
- `status` 回显 `remoteTrust`;
|
|
23
|
+
- 安全边界:只应在页面已由本插件保护时启用;不改变服务端信任护栏,
|
|
24
|
+
仍需 `--trusted-host <对外域名>`。
|
|
25
|
+
|
|
26
|
+
### 兼容:官方 DeepSeek 账号登录(0.2.1+)
|
|
27
|
+
|
|
28
|
+
- `/oauth/callback` 保持公开,包括**登录开始时才动态注册**的那一次(新增回归断言);
|
|
29
|
+
- 上游 `deepseek-account-platform` 的 `loginOrigin()` 只接受
|
|
30
|
+
`http://localhost|127.0.0.1|[::1]:<port>`,**不支持反代域名**——这是上游设计限制,
|
|
31
|
+
与插件无关;
|
|
32
|
+
- 文档补充:本机 `http://127.0.0.1:3080` 完成一次登录后,账号 token 保存在 Host
|
|
33
|
+
凭证里,之后经域名访问仍然有效;
|
|
34
|
+
- 刻意不 monkey-patch 核心内部函数(升级即碎),需要域名直登请推动上游放宽。
|
|
35
|
+
|
|
36
|
+
### 测试
|
|
37
|
+
|
|
38
|
+
- 新增 `test/remote-trust.test.mjs`(21 项):注入幂等、配置读取、`tapIndex` 缺失降级、
|
|
39
|
+
与 `dsh-remote-trust` 共存;
|
|
40
|
+
- `test/gate-default-deny.test.mjs` 新增 2 项:晚注册的 `/oauth/callback` 仍保持公开;
|
|
41
|
+
- 全量 21 个测试文件、558 项断言通过。
|
|
42
|
+
|
|
5
43
|
## 0.7.2
|
|
6
44
|
|
|
7
45
|
合并上游(Yuuz12/dsh-webui-auth)0.3.5–0.3.7 中**可安全合并**的部分;
|
package/README.en.md
CHANGED
|
@@ -108,7 +108,23 @@ Authentication is enforced in four layers, all implemented by **wrapping the web
|
|
|
108
108
|
```
|
|
109
109
|
- `trustBrowserOrigin` defaults to `true` when omitted (legacy behavior); an explicit `false` makes both sites **ignore** the browser-influenced origin and use `publicBaseUrl`. With no `publicBaseUrl` either, the token redirect falls back to the request Host / loopback, and OIDC fails closed with an error.
|
|
110
110
|
- The old `oidc.trustBrowserOrigin` is still accepted: when the settings section does not set the switch explicitly, it is used as the fallback, so existing deployments migrate smoothly.
|
|
111
|
-
|
|
111
|
+
|
|
112
|
+
- **Remote-access trust (0.7.3, optional built-in equivalent of `dsh-remote-trust`)**: when the page is
|
|
113
|
+
reached through a domain reverse proxy, the client treats it as non-loopback and Settings → Models
|
|
114
|
+
reports `settings are unavailable in this browser`. Enable:
|
|
115
|
+
```yaml
|
|
116
|
+
remote-web-ui:
|
|
117
|
+
trustRemoteHost: true
|
|
118
|
+
```
|
|
119
|
+
It injects `__DSH_TRANSPORT__.ownsHost = true` into index.html through the official
|
|
120
|
+
`webServer.tapIndex`; the injection is idempotent, so installing `dsh-remote-trust` as well does not
|
|
121
|
+
duplicate it. If `tapIndex` is unavailable it only logs a warning and everything else keeps working
|
|
122
|
+
(version compatibility).
|
|
123
|
+
**Security boundary**: the declaration marks the page as a "trusted local" surface and also opens
|
|
124
|
+
other client-side features gated on `isLoopback`; enable it only when the page is already protected
|
|
125
|
+
by this plugin. It does not change the server-side trust fence, so `--trusted-host <public host>` is
|
|
126
|
+
still required.
|
|
127
|
+
- **Extra public paths (0.6.7, the companion switch for deny-by-default)**: endpoints called by machines that cannot carry a browser session (GitHub webhooks, third-party OAuth callbacks) must be declared explicitly, otherwise the gate answers 401. Same settings section:
|
|
112
128
|
```yaml
|
|
113
129
|
remote-web-ui:
|
|
114
130
|
publicPaths:
|
|
@@ -401,6 +417,19 @@ wired into `npm test`).
|
|
|
401
417
|
|
|
402
418
|
## Known limits
|
|
403
419
|
|
|
420
|
+
- **DeepSeek official account login (0.2.1+)**: `/oauth/callback` is on the public allowlist
|
|
421
|
+
(including the route registered dynamically when a login starts), so the anonymous callback is never
|
|
422
|
+
blocked by the gate. However, upstream `@deepseek-ai/dsh-deepseek-account-platform`'s
|
|
423
|
+
`loginOrigin()` **only accepts** `http://localhost:<port>`, `http://127.0.0.1:<port>` and
|
|
424
|
+
`http://[::1]:<port>` with an explicit non-zero port; reverse-proxied domains are explicitly
|
|
425
|
+
unsupported. Therefore:
|
|
426
|
+
- via `http://127.0.0.1:3080` the official login completes normally;
|
|
427
|
+
- via a domain it fails with a `protocol` error (upstream limitation, not this plugin). Complete the
|
|
428
|
+
login once through `http://127.0.0.1:3080` or an SSH local forward — the account token is stored in
|
|
429
|
+
the host credential store and keeps working through the domain afterwards;
|
|
430
|
+
- logging in directly on a domain requires upstream to relax that rule (or a fork of the account
|
|
431
|
+
platform). This plugin deliberately does not monkey-patch core internals, which would break on
|
|
432
|
+
every upgrade.
|
|
404
433
|
- **DSH Desktop**: `dsh-desktop-*` marker pass-through is supported (markers survive login and
|
|
405
434
|
re-login), but upstream 0.3.6's **host carrier bridge is intentionally not included**. That bridge
|
|
406
435
|
decides "loopback" purely from the socket address, and this deployment sits behind an nginx proxy on
|
package/README.md
CHANGED
|
@@ -119,6 +119,14 @@ DSH WebUI 身份认证插件(持久化插件)。在「设置 → 身份认
|
|
|
119
119
|
|
|
120
120
|
第 2 行是**动态跳转不可用时的兜底**,不是第二个开关:Host 被改写成回环时,服务端从请求里**拿不到**任何浏览器侧地址,开关即使开着也没有可用值,只能回退 `publicBaseUrl`(该值来自 `settings.yaml`,运维可控、非攻击者可控,不构成开放重定向)。
|
|
121
121
|
|
|
122
|
+
|
|
123
|
+
- **远程访问信任(0.7.3,原 `dsh-remote-trust` 的可选内置)**:经域名反代访问时,客户端会把页面判为"非 loopback",设置 → 模型会报 `settings are unavailable in this browser`。开启下面这一项即可(等价于安装 `dsh-remote-trust`,但不需要额外插件):
|
|
124
|
+
```yaml
|
|
125
|
+
remote-web-ui:
|
|
126
|
+
trustRemoteHost: true
|
|
127
|
+
```
|
|
128
|
+
它通过官方 `webServer.tapIndex` 往 index.html 注入 `__DSH_TRANSPORT__.ownsHost = true`;注入幂等,与 `dsh-remote-trust` 同时安装也不会重复。`tapIndex` 不存在时只记一条 warn,插件其余功能照常(版本兼容)。
|
|
129
|
+
**安全边界**:该声明会把页面当作"可信本机",同时放开其他以 `isLoopback` 为条件的本机专属客户端表面;只应在页面已由本插件保护时启用,且服务端仍需 `--trusted-host <对外域名>`(它不改变服务端信任护栏)。
|
|
122
130
|
- **追加公开路径(0.6.7,默认拒绝的配套开关)**:机器调用、无法携带浏览器会话的端点(GitHub webhook、第三方 OAuth 回跳等)必须显式声明,否则会被闸门 401。同一 settings 段:
|
|
123
131
|
```yaml
|
|
124
132
|
remote-web-ui:
|
|
@@ -411,6 +419,10 @@ IdP 侧日志确认完整往返:`discovery → jwks → authorize(issued code)
|
|
|
411
419
|
|
|
412
420
|
## 已知边界
|
|
413
421
|
|
|
422
|
+
- **DeepSeek 官方账号登录(0.2.1+)**:本插件已把 `/oauth/callback` 放进公开白名单(包括登录开始时才**动态注册**的那一次),匿名回调不会被闸门拦截。但上游 `@deepseek-ai/dsh-deepseek-account-platform` 的 `loginOrigin()` **只接受** `http://localhost:<port>`、`http://127.0.0.1:<port>`、`http://[::1]:<port>`(显式非零端口),明确不支持反代域名。因此:
|
|
423
|
+
- 本机 `http://127.0.0.1:3080` 访问:官方登录可正常走完;
|
|
424
|
+
- 经域名访问:点登录会以 `protocol` 错失败(这是上游设计限制,与插件无关)。可先用 `http://127.0.0.1:3080` 或 SSH 本地转发完成一次登录——账号 token 保存在 Host 凭证里,之后经域名访问仍然有效;
|
|
425
|
+
- 若要在域名下直接登录,需要上游放开该限制(或自行 fork 账号平台)。本插件不会 monkey-patch 核心内部函数,以避免升级即碎。
|
|
414
426
|
- **DSH Desktop**:支持 `dsh-desktop-*` 标记透传(登录 / 退出后重登不再丢失标记),但**不提供**上游 0.3.6 的「宿主载体桥」。该桥只用 socket 是否回环来判定,而本部署是 nginx 同机反代——所有请求的 socket 都是 `127.0.0.1`,启用它等于"持有核心 Cookie 即可绕过插件会话 / SSO",与「仅允许 SSO」冲突。若必须使用 DSH Desktop 的 shell 直连模式,请改用上游原版插件,并自行评估该旁路风险。
|
|
415
427
|
- **审计日志字段类型**:0.7.2 起 `null` 与 boolean 原样写入(不再变成字符串 `"null"` / `"true"`);旧日志解析代码若假设"所有字段都是字符串",需相应调整。
|
|
416
428
|
- **dsh 0.2.2-rc.1 的 fallback 形状**:该版本起 `webServer.fallback` 可能是 `{ resource }`(`dsh-host-frontend-static` 就是这样注册的),而不再是函数。插件已兼容函数 / `{ resource }` / `{ handler }` 三种形状;资源型 fallback 同样会被闸门包装。若升级后 `/` 返回 `500` 且 body 为空(nginx 日志 `500 0`),请确认插件版本 ≥ 0.7.1。
|
package/index.js
CHANGED
|
@@ -1151,6 +1151,71 @@ export function matchesPublicPath(path, patterns) {
|
|
|
1151
1151
|
return false
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
// ---------------- 远程访问信任(原 dsh-remote-trust 的可选内置) ----------------
|
|
1155
|
+
//
|
|
1156
|
+
// 背景:dsh-client-connection 用 `__DSH_TRANSPORT__.ownsHost === true` 或页面 hostname
|
|
1157
|
+
// 是否回环来判定"可信本机";dsh-client-ui-settings 据此决定设置镜像的持久化模式。
|
|
1158
|
+
// 经域名反代访问(非 loopback)时页面被判为不可信,设置 → 模型会报
|
|
1159
|
+
// "settings are unavailable in this browser"。
|
|
1160
|
+
//
|
|
1161
|
+
// 官方逃生通道是 index.html 里的 `__DSH_TRANSPORT__.ownsHost = true`。这里内置同样的
|
|
1162
|
+
// 注入,默认**关闭**,由部署方显式开启:
|
|
1163
|
+
//
|
|
1164
|
+
// remote-web-ui:
|
|
1165
|
+
// trustRemoteHost: true
|
|
1166
|
+
//
|
|
1167
|
+
// 安全边界:该声明会让页面被当作"可信本机",同时放开其他以 isLoopback 为条件的
|
|
1168
|
+
// 本机专属客户端表面。因此只应在页面已由本插件(会话 + 可选 SSO)保护时启用;
|
|
1169
|
+
// 它不改变服务端信任护栏(/api 仍是 loopback ∪ --trusted-host)。
|
|
1170
|
+
const REMOTE_TRUST_SCRIPT =
|
|
1171
|
+
'<script>globalThis.__DSH_TRANSPORT__=Object.assign({},globalThis.__DSH_TRANSPORT__,{ownsHost:true});</script>'
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* index.html 变换:注入 ownsHost 声明。幂等(已有 ownsHost 则原样返回),
|
|
1175
|
+
* 缺 </head> 时也原样返回——绝不因为页面形状变化把 index 弄坏。
|
|
1176
|
+
* 与 dsh-remote-trust 同时安装时不会重复注入(同一幂等判据)。
|
|
1177
|
+
*/
|
|
1178
|
+
export function injectRemoteTrust(html) {
|
|
1179
|
+
if (typeof html !== 'string') return html
|
|
1180
|
+
if (html.includes('ownsHost')) return html
|
|
1181
|
+
return html.replace('</head>', REMOTE_TRUST_SCRIPT + '</head>')
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** 读取 remote-web-ui.trustRemoteHost(默认 false)。 */
|
|
1185
|
+
export function remoteTrustEnabled(ctx) {
|
|
1186
|
+
try {
|
|
1187
|
+
const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : undefined
|
|
1188
|
+
const section = settings && typeof settings.get === 'function'
|
|
1189
|
+
? settings.get(PUBLIC_BASE_NAMESPACE)
|
|
1190
|
+
: undefined
|
|
1191
|
+
return !!(section && section.trustRemoteHost === true)
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
return false
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* 按配置安装 index 变换。
|
|
1199
|
+
* 版本兼容:tapIndex 不存在时记一条 warn 并返回 false,绝不抛异常
|
|
1200
|
+
* (上游若改动该内部 API,插件其余功能必须照常工作)。
|
|
1201
|
+
*
|
|
1202
|
+
* @returns {boolean} 是否已安装
|
|
1203
|
+
*/
|
|
1204
|
+
export function installRemoteTrust(ctx) {
|
|
1205
|
+
if (!remoteTrustEnabled(ctx)) return false
|
|
1206
|
+
const ws = ctx && ctx.webServer
|
|
1207
|
+
if (!ws || typeof ws.tapIndex !== 'function') {
|
|
1208
|
+
try { ctx.logger.warn('[dsh-webui-oauth] remote-web-ui.trustRemoteHost 已启用,但 webServer.tapIndex 不可用;未注入 ownsHost') } catch (e) { /* ignore */ }
|
|
1209
|
+
return false
|
|
1210
|
+
}
|
|
1211
|
+
const off = ws.tapIndex(injectRemoteTrust)
|
|
1212
|
+
try {
|
|
1213
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => off, 'dsh-webui-oauth: remote trust index tap')
|
|
1214
|
+
} catch (e) { /* ignore */ }
|
|
1215
|
+
try { ctx.logger.info('[dsh-webui-oauth] remote trust index tap registered (__DSH_TRANSPORT__.ownsHost = true)') } catch (e) { /* ignore */ }
|
|
1216
|
+
return true
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1154
1219
|
/** 该请求是否是一次浏览器导航(而不是 fetch/XHR 的接口调用)。 */
|
|
1155
1220
|
function wantsHtml(req) {
|
|
1156
1221
|
try {
|
|
@@ -2562,6 +2627,9 @@ export async function apply(ctx) {
|
|
|
2562
2627
|
ctx.effect(() => gate.undo, 'dsh-webui-oauth: route gate')
|
|
2563
2628
|
// 启动时算一次(异步,不阻塞注册;默认 false 是安全的——最坏只是先显示密码页)
|
|
2564
2629
|
refreshSsoOnly().catch(() => {})
|
|
2630
|
+
|
|
2631
|
+
// 远程访问信任(可选):反代域名下让设置页可用。默认关闭;tapIndex 缺失时安全降级。
|
|
2632
|
+
installRemoteTrust(ctx)
|
|
2565
2633
|
if (!gate.ok()) {
|
|
2566
2634
|
ctx.logger.error('[dsh-webui-oauth] ROUTE GATE INCOMPLETE — ' + gate.problems().join('; ')
|
|
2567
2635
|
+ '. /api and/or WebSocket may be unprotected; the gate will still wrap them if they register later.')
|
|
@@ -2867,6 +2935,8 @@ export async function apply(ctx) {
|
|
|
2867
2935
|
username: enabled ? creds.username : null,
|
|
2868
2936
|
ttl: ttlOf(creds),
|
|
2869
2937
|
sessionsPersisted: sessions.ok,
|
|
2938
|
+
// 远程访问信任(可选):是否按 remote-web-ui.trustRemoteHost 注入了 ownsHost。
|
|
2939
|
+
remoteTrust: remoteTrustEnabled(ctx),
|
|
2870
2940
|
// 核心兜底席位形态(handler | resource | absent | unknown):核心升级后
|
|
2871
2941
|
// 如果这里变成 unknown,文档请求会 404 —— 排查时先看这个字段。
|
|
2872
2942
|
fallback: fallbackShape(ctx.webServer),
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webui-oauth",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs && node test/fallback-shape.test.mjs && node test/desktop-marker.test.mjs && node test/audit-null.test.mjs"
|
|
7
|
+
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs && node test/fallback-shape.test.mjs && node test/desktop-marker.test.mjs && node test/audit-null.test.mjs && node test/remote-trust.test.mjs"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|