dsh-webui-oauth 0.7.1 → 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 +82 -0
- package/README.en.md +38 -1
- package/README.md +14 -0
- package/index.js +166 -3
- package/lib/desktop-adapter.js +205 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
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
|
+
|
|
43
|
+
## 0.7.2
|
|
44
|
+
|
|
45
|
+
合并上游(Yuuz12/dsh-webui-auth)0.3.5–0.3.7 中**可安全合并**的部分;
|
|
46
|
+
宿主载体桥 / 桌面端锁因会绕过现有门禁,未合并。
|
|
47
|
+
|
|
48
|
+
### 合并:DSH Desktop 标记透传(upstream 0.3.5 / #8)
|
|
49
|
+
|
|
50
|
+
- 新增 `lib/desktop-adapter.js`(自包含、可整体摘除);
|
|
51
|
+
- 渲染器加载时把 `dsh-desktop-*` 标记写入 HttpOnly Cookie(`dsh_wua_desktop`,12h);
|
|
52
|
+
- 之后落到干净 `/` 时 302 回带标记的 URL,修复桌面端 advanced/extended 模式下
|
|
53
|
+
12 个 layout 依赖插件停在 pending 的问题。
|
|
54
|
+
- **不影响门禁**:`remember()` 在认证判定之前调用但只写 Cookie;`restore()` 只在
|
|
55
|
+
`checkRequest()` 通过之后调用。未登录请求带标记仍然去登录页。
|
|
56
|
+
- 已加 9 项集成断言(oidc-bind 1f)+ 24 项纯函数断言。
|
|
57
|
+
|
|
58
|
+
### 合并:审计日志 null/boolean 不再被字符串化(upstream 0.3.6)
|
|
59
|
+
|
|
60
|
+
`auditLog` 原先把非 string/number 一律 `String()`,导致日志里出现字符串
|
|
61
|
+
`"null"` / `"true"`。现在 null 与 boolean 原样保留。
|
|
62
|
+
|
|
63
|
+
### 合并:fallback 诊断增强(upstream 0.3.7)
|
|
64
|
+
|
|
65
|
+
我们在 0.7.1 已独立修复 `{ resource }` 兼容;本次补上可观测性:
|
|
66
|
+
|
|
67
|
+
- `status` 新增 `fallback: 'handler' | 'resource' | 'absent' | 'unknown'`;
|
|
68
|
+
- 启动日志输出 `core fallback seat: handler/resource`,unknown 时 error;
|
|
69
|
+
- 形态不认识时输出**一次性**诊断,并返回带说明的 404(不再是空 body)。
|
|
70
|
+
|
|
71
|
+
### 未合并(有意)
|
|
72
|
+
|
|
73
|
+
- **宿主载体桥(0.3.6 C6)**:其 `isLoopbackPeer()` 只看 socket 地址。
|
|
74
|
+
本部署是 nginx 同机反代,所有请求的 socket 都是 127.0.0.1,等于"持有核心
|
|
75
|
+
Cookie 即可绕过插件会话/SSO",与「仅允许 SSO」直接冲突。
|
|
76
|
+
- **桌面端锁(0.3.6 C8)**:UI 级锁,对 Web 部署无价值。
|
|
77
|
+
- **exact 路由覆盖(0.3.6 C7)**:我们的 deny-by-default 已覆盖全部 exact
|
|
78
|
+
(实测 `/plugins/events` 匿名 401),无需合并。
|
|
79
|
+
|
|
80
|
+
### 测试
|
|
81
|
+
|
|
82
|
+
- 新增 `test/desktop-marker.test.mjs`(24 项)、`test/audit-null.test.mjs`(7 项);
|
|
83
|
+
`test/fallback-shape.test.mjs` 增加 7 项形状判定;`test/oidc-bind.test.mjs`
|
|
84
|
+
增加 9 项"桌面标记不影响门禁"集成断言。
|
|
85
|
+
- 全量 20 个测试文件、535 项断言通过。
|
|
86
|
+
|
|
5
87
|
## 0.7.1
|
|
6
88
|
|
|
7
89
|
兼容 dsh **0.2.2-rc.1**:`webServer.fallback` 从函数改成了 `{ resource }`。
|
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,27 @@ 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.
|
|
433
|
+
- **DSH Desktop**: `dsh-desktop-*` marker pass-through is supported (markers survive login and
|
|
434
|
+
re-login), but upstream 0.3.6's **host carrier bridge is intentionally not included**. That bridge
|
|
435
|
+
decides "loopback" purely from the socket address, and this deployment sits behind an nginx proxy on
|
|
436
|
+
the same host — every request's socket is `127.0.0.1`, so enabling it would mean "a core cookie is
|
|
437
|
+
enough to bypass the plugin session / SSO", which conflicts with the SSO-only policy. If you must use
|
|
438
|
+
DSH Desktop's shell-direct mode, use the upstream plugin and evaluate that bypass yourself.
|
|
439
|
+
- **Audit field types**: since 0.7.2 `null` and booleans are written as-is (no longer stringified to
|
|
440
|
+
`"null"` / `"true"`); any log consumer that assumed every field is a string must be adjusted.
|
|
404
441
|
- **Fallback shape on dsh 0.2.2-rc.1**: since that version `webServer.fallback` may be `{ resource }` (that is how `dsh-host-frontend-static` registers it) instead of a function. The plugin now handles function / `{ resource }` / `{ handler }`; a resource fallback is gated as well. If `/` returns an empty `500` after upgrading (nginx logs `500 0`), make sure the plugin is ≥ 0.7.1.
|
|
405
442
|
- **Inherent runtime-wrapping window**: between a route-object replacement (service hot-reload) and the next rescan (≤10s) there is an unprotected window; the fail-closed check on enabling covers the "initially exposed" case, so this window only affects hot-reload during runtime.
|
|
406
443
|
- **WebSocket and `trustedHosts`**: in reverse-proxy / LAN deployments (non-loopback Host), WS downlinks need the public hostname added to `client-connection.trustedHosts` in the DSH config (see "Architecture").
|
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,12 @@ 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 核心内部函数,以避免升级即碎。
|
|
426
|
+
- **DSH Desktop**:支持 `dsh-desktop-*` 标记透传(登录 / 退出后重登不再丢失标记),但**不提供**上游 0.3.6 的「宿主载体桥」。该桥只用 socket 是否回环来判定,而本部署是 nginx 同机反代——所有请求的 socket 都是 `127.0.0.1`,启用它等于"持有核心 Cookie 即可绕过插件会话 / SSO",与「仅允许 SSO」冲突。若必须使用 DSH Desktop 的 shell 直连模式,请改用上游原版插件,并自行评估该旁路风险。
|
|
427
|
+
- **审计日志字段类型**:0.7.2 起 `null` 与 boolean 原样写入(不再变成字符串 `"null"` / `"true"`);旧日志解析代码若假设"所有字段都是字符串",需相应调整。
|
|
414
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。
|
|
415
429
|
- **运行时包装的固有窗口**:路由对象被替换(服务热重载)到下一次重扫之间(≤10s)存在未保护窗口;启用认证时的 fail-closed 已挡住「初始裸奔」,此窗口仅影响运行中的热重载场景。
|
|
416
430
|
- **WS 与 trustedHosts**:反代/局域网(非回环 Host)下,WS 下行需在 dsh 配置 `client-connection.trustedHosts` 中加入对外域名(见「架构」节)。
|
package/index.js
CHANGED
|
@@ -157,6 +157,9 @@ import { readFileSync, writeFileSync, appendFileSync, accessSync, mkdirSync, unl
|
|
|
157
157
|
import { fileURLToPath } from "node:url";
|
|
158
158
|
import { resolve as resolvePath } from "node:path";
|
|
159
159
|
import { promisify } from "node:util";
|
|
160
|
+
// 宿主适配层(DSH Desktop 的 dsh-desktop-* 标记透传)。
|
|
161
|
+
// 整体摘除:删掉本行 + 网关里两处标着「宿主适配层」的代码块 + lib/desktop-adapter.js。
|
|
162
|
+
import { desktopAdapter } from './lib/desktop-adapter.js';
|
|
160
163
|
|
|
161
164
|
export const name = 'dsh-webui-oauth'
|
|
162
165
|
|
|
@@ -580,7 +583,11 @@ async function auditLog(ctx, event, fields) {
|
|
|
580
583
|
let v = fields[key]
|
|
581
584
|
if (v === undefined) continue
|
|
582
585
|
if (key === 'ip' && typeof v === 'string') v = anonymizeIp(v)
|
|
583
|
-
|
|
586
|
+
// null / boolean 原样保留:早先一律 String(),日志里会出现字符串 "null"、
|
|
587
|
+
// "true",读起来像出了故障(upstream 0.3.6 的同类修复)。
|
|
588
|
+
entry[key] = (v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
589
|
+
? v
|
|
590
|
+
: String(v)
|
|
584
591
|
}
|
|
585
592
|
appendFileSync(target, JSON.stringify(entry) + '\n', 'utf8')
|
|
586
593
|
} catch (e) {
|
|
@@ -1144,6 +1151,71 @@ export function matchesPublicPath(path, patterns) {
|
|
|
1144
1151
|
return false
|
|
1145
1152
|
}
|
|
1146
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
|
+
|
|
1147
1219
|
/** 该请求是否是一次浏览器导航(而不是 fetch/XHR 的接口调用)。 */
|
|
1148
1220
|
function wantsHtml(req) {
|
|
1149
1221
|
try {
|
|
@@ -1189,6 +1261,62 @@ export async function serveFallback(ctx, req, res) {
|
|
|
1189
1261
|
return false
|
|
1190
1262
|
}
|
|
1191
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* 判断核心兜底席位的形状。
|
|
1266
|
+
* 核心升级可能再次改变它(函数 → { resource } → ?),把形状单独抽出来,
|
|
1267
|
+
* 既供 status 回显,也供启动自检与一次性诊断使用。
|
|
1268
|
+
*
|
|
1269
|
+
* @returns {'absent'|'handler'|'resource'|'unknown'}
|
|
1270
|
+
*/
|
|
1271
|
+
export function fallbackShape(webServer) {
|
|
1272
|
+
if (!webServer) return 'absent'
|
|
1273
|
+
const fallback = webServer.fallback
|
|
1274
|
+
if (fallback === undefined) return 'absent'
|
|
1275
|
+
if (typeof fallback === 'function') return 'handler'
|
|
1276
|
+
if (fallback !== null && typeof fallback === 'object' &&
|
|
1277
|
+
'resource' in fallback && typeof webServer.serveResource === 'function') return 'resource'
|
|
1278
|
+
return 'unknown'
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// 形状不兼容只报一次:每个文档请求都刷一条 error 会把日志淹掉。
|
|
1282
|
+
let fallbackProblemReported = false
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* 形态不认识时的一次性诊断:说清期望什么、实际是什么,别让 500/空 404 掩盖过去。
|
|
1286
|
+
* 仅写日志,不改变响应(响应由 sendFallbackUnavailable 负责)。
|
|
1287
|
+
*/
|
|
1288
|
+
export function reportFallbackProblem(ctx) {
|
|
1289
|
+
if (fallbackProblemReported) return
|
|
1290
|
+
fallbackProblemReported = true
|
|
1291
|
+
try {
|
|
1292
|
+
const ws = ctx && ctx.webServer
|
|
1293
|
+
const fallback = ws ? ws.fallback : undefined
|
|
1294
|
+
const actual = fallback === undefined
|
|
1295
|
+
? 'undefined'
|
|
1296
|
+
: typeof fallback === 'function'
|
|
1297
|
+
? 'function'
|
|
1298
|
+
: (fallback !== null && typeof fallback === 'object')
|
|
1299
|
+
? 'object { ' + Object.keys(fallback).join(', ') + ' }'
|
|
1300
|
+
: typeof fallback
|
|
1301
|
+
const canServe = !!(ws && typeof ws.serveResource === 'function')
|
|
1302
|
+
ctx.logger.error('[dsh-webui-oauth] INCOMPATIBLE CORE: cannot hand the document back to'
|
|
1303
|
+
+ ' webServer.fallback — expected a handler function, or { resource } together with'
|
|
1304
|
+
+ ' webServer.serveResource(); got ' + actual
|
|
1305
|
+
+ ' (serveResource: ' + (canServe ? 'present' : 'missing') + ').'
|
|
1306
|
+
+ ' webServer.fallback shape may have changed again; authenticated pages answer 404 until the plugin is updated.')
|
|
1307
|
+
} catch (e) { /* logging must never throw */ }
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** 形态不认识时给操作者一句能看懂的话,而不是空白页/500。 */
|
|
1311
|
+
export function sendFallbackUnavailable(res) {
|
|
1312
|
+
res.writeHead(404, {
|
|
1313
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
1314
|
+
'x-content-type-options': 'nosniff',
|
|
1315
|
+
'cache-control': 'no-store',
|
|
1316
|
+
})
|
|
1317
|
+
res.end('dsh-webui-oauth: core webServer.fallback is unavailable or incompatible\n')
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1192
1320
|
function rejectUpgrade401(socket) {
|
|
1193
1321
|
try {
|
|
1194
1322
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 13\r\n\r\nunauthorized\n')
|
|
@@ -2499,11 +2627,26 @@ export async function apply(ctx) {
|
|
|
2499
2627
|
ctx.effect(() => gate.undo, 'dsh-webui-oauth: route gate')
|
|
2500
2628
|
// 启动时算一次(异步,不阻塞注册;默认 false 是安全的——最坏只是先显示密码页)
|
|
2501
2629
|
refreshSsoOnly().catch(() => {})
|
|
2630
|
+
|
|
2631
|
+
// 远程访问信任(可选):反代域名下让设置页可用。默认关闭;tapIndex 缺失时安全降级。
|
|
2632
|
+
installRemoteTrust(ctx)
|
|
2502
2633
|
if (!gate.ok()) {
|
|
2503
2634
|
ctx.logger.error('[dsh-webui-oauth] ROUTE GATE INCOMPLETE — ' + gate.problems().join('; ')
|
|
2504
2635
|
+ '. /api and/or WebSocket may be unprotected; the gate will still wrap them if they register later.')
|
|
2505
2636
|
}
|
|
2506
2637
|
|
|
2638
|
+
// 核心兜底席位形状自检:认得出就报一句,认不出就明确报「不兼容当前核心版本」,
|
|
2639
|
+
// 不要等到用户登录换 cookie 时才以 500/空页暴露。'absent' 可能只是
|
|
2640
|
+
// frontend-static 尚未注册,留到首个文档请求时再诊断。
|
|
2641
|
+
{
|
|
2642
|
+
const shape = fallbackShape(ctx.webServer)
|
|
2643
|
+
if (shape === 'handler' || shape === 'resource') {
|
|
2644
|
+
ctx.logger.info('[dsh-webui-oauth] core fallback seat: ' + shape)
|
|
2645
|
+
} else if (shape === 'unknown') {
|
|
2646
|
+
ctx.logger.error('[dsh-webui-oauth] INCOMPATIBLE CORE: webServer.fallback is neither a handler function nor { resource } + webServer.serveResource(); authenticated pages will answer 404')
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2507
2650
|
ctx.logger.info('[dsh-webui-oauth] started, credentials file: ' + configPath())
|
|
2508
2651
|
if (!enabledFlag) {
|
|
2509
2652
|
// H1: token 同时落盘(0600,仅本机操作者可读),setup 成功后删除。
|
|
@@ -2792,6 +2935,11 @@ export async function apply(ctx) {
|
|
|
2792
2935
|
username: enabled ? creds.username : null,
|
|
2793
2936
|
ttl: ttlOf(creds),
|
|
2794
2937
|
sessionsPersisted: sessions.ok,
|
|
2938
|
+
// 远程访问信任(可选):是否按 remote-web-ui.trustRemoteHost 注入了 ownsHost。
|
|
2939
|
+
remoteTrust: remoteTrustEnabled(ctx),
|
|
2940
|
+
// 核心兜底席位形态(handler | resource | absent | unknown):核心升级后
|
|
2941
|
+
// 如果这里变成 unknown,文档请求会 404 —— 排查时先看这个字段。
|
|
2942
|
+
fallback: fallbackShape(ctx.webServer),
|
|
2795
2943
|
gate: {
|
|
2796
2944
|
ok: gate.ok(),
|
|
2797
2945
|
problems: gate.problems().slice(0, 3),
|
|
@@ -3492,7 +3640,20 @@ export async function apply(ctx) {
|
|
|
3492
3640
|
path: '',
|
|
3493
3641
|
handler: async (req, res) => {
|
|
3494
3642
|
try {
|
|
3643
|
+
// ─────── 宿主适配层(DSH Desktop)· 整体摘除时删掉本块 ───────
|
|
3644
|
+
// 必须在认证判定之前:退出登录是客户端自己 location.href = '/',
|
|
3645
|
+
// 服务端看不到带标记的 URL 被放弃,只能在加载时提前记住。
|
|
3646
|
+
// 注意:remember() 只写 Cookie,不参与放行;门禁仍由 checkRequest 决定。
|
|
3647
|
+
desktopAdapter.remember(req, res)
|
|
3648
|
+
// ──────────────────────────────────────────────────────────
|
|
3495
3649
|
if (checkRequest(req)) {
|
|
3650
|
+
// ─────── 宿主适配层(DSH Desktop)· 整体摘除时删掉本块 ───────
|
|
3651
|
+
// 核心 token→cookie 交换会把浏览器 303 到干净的 "/",桌面端
|
|
3652
|
+
// advanced / extended 模式依赖 URL 上的 dsh-desktop-* 标记才会接管
|
|
3653
|
+
// layout 服务,这里把记住的标记补回 URL 再放行。
|
|
3654
|
+
// 只在 checkRequest 通过之后调用:未认证请求绝不会走到这里。
|
|
3655
|
+
if (desktopAdapter.restore(req, res)) return
|
|
3656
|
+
// ──────────────────────────────────────────────────────────
|
|
3496
3657
|
// alpha.2+:插件会话通过后,核心 BrowserAuth 可能仍未认领该浏览器
|
|
3497
3658
|
// (核心 cookie 缺失/过期)。主动引导去带 launch token 的根 URL 完成
|
|
3498
3659
|
// 核心 token→cookie 交换,否则 fallback 的 authorizeIndex 会 401 死锁。
|
|
@@ -3510,8 +3671,10 @@ export async function apply(ctx) {
|
|
|
3510
3671
|
// fallback 可能是函数,也可能是 { resource }(0.2.2-rc.1+)。
|
|
3511
3672
|
// 交给 serveFallback 统一分派,避免 TypeError → 500 空响应。
|
|
3512
3673
|
if (await serveFallback(ctx, req, res)) return
|
|
3513
|
-
|
|
3514
|
-
|
|
3674
|
+
// 形态不认识 / 核心还没注册:明确报一次,并回带说明的 404,
|
|
3675
|
+
// 不要留一个空 body 让运维去猜。
|
|
3676
|
+
reportFallbackProblem(ctx)
|
|
3677
|
+
sendFallbackUnavailable(res)
|
|
3515
3678
|
return
|
|
3516
3679
|
}
|
|
3517
3680
|
res.writeHead(302, { location: loginLocation(req) })
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Desktop 适配层。
|
|
3
|
+
*
|
|
4
|
+
* 本模块与认证逻辑**完全解耦**:不 import 主模块的任何东西,只依赖 Node 内置能力,
|
|
5
|
+
* 因此可以整体删除而不影响认证本身。见文末「如何移除整个适配」。
|
|
6
|
+
*
|
|
7
|
+
* ── 为什么需要它(issue #8)────────────────────────────────────────────
|
|
8
|
+
* DSH Desktop 的 advanced / extended 模式会在组合层**禁用核心 `ui-layout` 行**
|
|
9
|
+
* (dsh-plugin-desktop 的 profile 组合按 mode 改写组合表),此时客户端 `layout`
|
|
10
|
+
* 服务的唯一提供者是桌面端自己的客户端插件,而它**只在 URL 带 `dsh-desktop-*`
|
|
11
|
+
* 标记时才生效** —— `parseDesktopClientEnvironment()` 在标记缺失时返回 undefined,
|
|
12
|
+
* `apply()` 随即直接 return,整个桌面端客户端插件不生效。
|
|
13
|
+
*
|
|
14
|
+
* 任何一次落到干净 "/" 的跳转都会丢掉这些标记,于是 12 个依赖 layout 的客户端插件
|
|
15
|
+
* 永远停在 pending,渲染器 30s 不上报健康,宿主弹出恢复模式对话框。已确认两条路径:
|
|
16
|
+
*
|
|
17
|
+
* ① 首次登录:核心 token→cookie 交换把浏览器 303 到干净的 "/";
|
|
18
|
+
* ② 退出后重登:客户端自行 `location.href = '/'`(lib/client.js 的 onLogout 与
|
|
19
|
+
* 401 分支),服务端从头到尾没看到那个带标记的 URL 被放弃。
|
|
20
|
+
*
|
|
21
|
+
* ── 它做什么 ──────────────────────────────────────────────────────────
|
|
22
|
+
* 主动记忆 + 补投,而不是「跳转前才记」:
|
|
23
|
+
*
|
|
24
|
+
* remember() 渲染器正常加载(URL 带标记)时就把标记写进 Cookie;
|
|
25
|
+
* restore() 文档请求落到干净的 "/" 时,302 回带标记的 URL。
|
|
26
|
+
*
|
|
27
|
+
* 为什么必须「主动」:路径 ② 是客户端发起的跳转,服务端根本没有机会在跳转前做任何事,
|
|
28
|
+
* 等它收到 `GET /` 时标记早已不在。只有在加载时就把标记落盘,才能覆盖所有丢失路径。
|
|
29
|
+
*
|
|
30
|
+
* 暂存**刻意不清除**:清除会引入「记住 → 立刻清掉 → 退出后无处可补」的时序陷阱,
|
|
31
|
+
* 过期交给 Cookie 自身的 Max-Age(12h,且每次加载都会刷新)。
|
|
32
|
+
*
|
|
33
|
+
* ── 安全边界 ──────────────────────────────────────────────────────────
|
|
34
|
+
* 标记按 `dsh-desktop-` 前缀**整体透传**(刻意不做硬编码白名单——DSH Desktop 将来
|
|
35
|
+
* 新增一个标记时会再次静默弄坏桌面端启动,正是本 issue 的同类耦合),其中 `mode`
|
|
36
|
+
* 必须是 `compatibility`/`extended`/`advanced` 之一、值长度上限 64。跳转目标恒为
|
|
37
|
+
* 同源相对路径 "/",**无开放重定向面**;网页版(无 `dsh-desktop-*` 标记)零影响。
|
|
38
|
+
*
|
|
39
|
+
* ── 如何移除整个适配 ──────────────────────────────────────────────────
|
|
40
|
+
* 1. 删掉 index.js 里两处标着「宿主适配层」的代码块(网关 handler 内,共 3 行代码);
|
|
41
|
+
* 2. 删掉 index.js 顶部那行 import,连同它上面两行注释;
|
|
42
|
+
* 3. 删掉 index.js 导出块里提到本模块的那行注释;
|
|
43
|
+
* 4. 删掉本文件,并从 package.json 的 `files` 里去掉 `lib/desktop-adapter.js`。
|
|
44
|
+
*
|
|
45
|
+
* 已实测:只做 1+2 后 index.js 即可独立 `node --check` 通过,无任何悬空引用。
|
|
46
|
+
* 认证本身不依赖本模块 —— 移除后行为等价于 0.3.3:网页版不受影响,桌面端
|
|
47
|
+
* advanced / extended 模式会在登录跳转后再次出现 #8。
|
|
48
|
+
*
|
|
49
|
+
* ── 如何新增一个适配 ──────────────────────────────────────────────────
|
|
50
|
+
* 按同样形状导出即可:
|
|
51
|
+
*
|
|
52
|
+
* { remember(req, res) -> boolean, restore(req, res) -> boolean }
|
|
53
|
+
*
|
|
54
|
+
* remember 在网关最前面**无条件**调用;restore 在插件会话校验通过后调用,
|
|
55
|
+
* 返回 true 表示已接管该请求(已发出响应,调用方应立即 return)。
|
|
56
|
+
* 两个函数都只应触碰响应头,不应改变认证判定。
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const DESKTOP_STASH_COOKIE = 'dsh_wua_desktop'
|
|
60
|
+
const DESKTOP_STASH_TTL_SECONDS = 12 * 60 * 60
|
|
61
|
+
|
|
62
|
+
/** 桌面端标记的统一前缀。 */
|
|
63
|
+
const DESKTOP_MARKER_PREFIX = 'dsh-desktop-'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 已知标记,仅用于固定补投时的 query 顺序;除此之外任何 `dsh-desktop-*` 键
|
|
67
|
+
* 都会一并透传(前向兼容)。
|
|
68
|
+
*/
|
|
69
|
+
const DESKTOP_MARKER_KEYS = [
|
|
70
|
+
'dsh-desktop-mode',
|
|
71
|
+
'dsh-desktop-platform',
|
|
72
|
+
'dsh-desktop-version',
|
|
73
|
+
'dsh-desktop-material',
|
|
74
|
+
'dsh-desktop-mica',
|
|
75
|
+
'dsh-desktop-titlebar-inset',
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** 桌面端合法模式,与 dsh-plugin-desktop 的 MODES 对齐;其它值一律不采信。 */
|
|
79
|
+
const DESKTOP_MODES = new Set(['compatibility', 'extended', 'advanced'])
|
|
80
|
+
|
|
81
|
+
/** 单个标记值的长度上限,防脏 cookie 撑爆响应头。 */
|
|
82
|
+
const DESKTOP_MARKER_MAX_LENGTH = 64
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 读取请求 Cookie。
|
|
86
|
+
* 刻意不 import 主模块的同名工具:本模块要能被单独删掉,不能反向依赖认证实现。
|
|
87
|
+
*/
|
|
88
|
+
function cookieOf(req, name) {
|
|
89
|
+
const raw = req.headers && req.headers.cookie
|
|
90
|
+
if (!raw) return null
|
|
91
|
+
for (const part of String(raw).split(';')) {
|
|
92
|
+
const eq = part.indexOf('=')
|
|
93
|
+
if (eq === -1) continue
|
|
94
|
+
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim()
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseUrl(urlString) {
|
|
100
|
+
try { return new URL(urlString || '/', 'http://dsh.invalid') } catch (e) { return null }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 从一组 search params 里收集可透传的桌面端标记;没有可透传项时返回 null。
|
|
105
|
+
* 已知键先出(顺序稳定),其余同前缀键按出现顺序跟进。
|
|
106
|
+
*/
|
|
107
|
+
function collectDesktopMarkers(params) {
|
|
108
|
+
// mode 是桌面端客户端插件的硬开关:它缺失或非法时 parseDesktopClientEnvironment
|
|
109
|
+
// 会直接 throw,此时补投 platform 之类的其余标记只会把渲染器推向失败,故整体放弃。
|
|
110
|
+
const mode = params.get(DESKTOP_MARKER_PREFIX + 'mode')
|
|
111
|
+
if (mode === null || !DESKTOP_MODES.has(mode)) return null
|
|
112
|
+
const markers = []
|
|
113
|
+
const seen = new Set()
|
|
114
|
+
const take = (key) => {
|
|
115
|
+
if (seen.has(key)) return
|
|
116
|
+
const value = params.get(key)
|
|
117
|
+
if (value === null || value === '' || value.length > DESKTOP_MARKER_MAX_LENGTH) return
|
|
118
|
+
seen.add(key)
|
|
119
|
+
markers.push([key, value])
|
|
120
|
+
}
|
|
121
|
+
for (const key of DESKTOP_MARKER_KEYS) take(key)
|
|
122
|
+
for (const key of params.keys()) {
|
|
123
|
+
if (key.startsWith(DESKTOP_MARKER_PREFIX)) take(key)
|
|
124
|
+
}
|
|
125
|
+
return markers.length > 0 ? markers : null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 从 URL 中提取可透传的桌面端标记;没有可透传项时返回 null。 */
|
|
129
|
+
function desktopMarkersOf(urlString) {
|
|
130
|
+
const url = parseUrl(urlString)
|
|
131
|
+
if (url === null) return null
|
|
132
|
+
return collectDesktopMarkers(url.searchParams)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 暂存标记的 Set-Cookie 值。 */
|
|
136
|
+
function desktopStashCookie(markers) {
|
|
137
|
+
const payload = markers.map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&')
|
|
138
|
+
return DESKTOP_STASH_COOKIE + '=' + encodeURIComponent(payload) +
|
|
139
|
+
'; HttpOnly; SameSite=Lax; Path=/; Max-Age=' + DESKTOP_STASH_TTL_SECONDS
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 读出暂存 cookie 并还原成 query 串(不含 "?");不可用/非法时返回 null。 */
|
|
143
|
+
function desktopStashQuery(req) {
|
|
144
|
+
const raw = cookieOf(req, DESKTOP_STASH_COOKIE)
|
|
145
|
+
if (!raw) return null
|
|
146
|
+
let decoded = null
|
|
147
|
+
try { decoded = decodeURIComponent(raw) } catch (e) { return null }
|
|
148
|
+
// 走与提取端同一套规则:暂存内容来自客户端 Cookie,同样要过滤。
|
|
149
|
+
// 这套对称性是有承重的——补投出的 URL 必然能被 desktopMarkersOf 认出来,
|
|
150
|
+
// restore() 才不会在同一份标记上反复跳转。改任一侧都要同步改另一侧。
|
|
151
|
+
const markers = collectDesktopMarkers(new URLSearchParams(decoded))
|
|
152
|
+
if (markers === null) return null
|
|
153
|
+
return markers.map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 记住本次文档请求 URL 上的桌面端标记。
|
|
158
|
+
*
|
|
159
|
+
* 必须是「主动记忆」而非「跳转前才记」:退出登录是客户端自己 `location.href = '/'`,
|
|
160
|
+
* 服务端根本没机会看到那个带标记的 URL 被放弃——等它看到 `GET /` 时标记早已不在,
|
|
161
|
+
* 无从暂存。因此在渲染器正常加载(URL 带标记)时就落盘,之后无论标记在哪一步丢失
|
|
162
|
+
* (退出重登、会话过期、核心 token 交换、深链)都能补投回来。
|
|
163
|
+
*
|
|
164
|
+
* @returns true 表示写入了暂存 Cookie。
|
|
165
|
+
*/
|
|
166
|
+
function remember(req, res) {
|
|
167
|
+
if (req.method !== 'GET') return false
|
|
168
|
+
const url = parseUrl(req.url)
|
|
169
|
+
if (url === null || url.pathname !== '/') return false
|
|
170
|
+
const markers = desktopMarkersOf(req.url)
|
|
171
|
+
if (markers === null) return false
|
|
172
|
+
res.setHeader('Set-Cookie', desktopStashCookie(markers))
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 文档请求落到干净的 "/" 时,把记住的桌面端标记补回 URL。
|
|
178
|
+
* 刻意不清除暂存(见文件顶部说明),过期交给 Cookie 自身的 Max-Age。
|
|
179
|
+
*
|
|
180
|
+
* @returns true 表示已接管该请求(已发出 302,调用方应立即 return)。
|
|
181
|
+
*/
|
|
182
|
+
function restore(req, res) {
|
|
183
|
+
if (req.method !== 'GET') return false
|
|
184
|
+
const pending = desktopStashQuery(req)
|
|
185
|
+
if (pending === null) return false
|
|
186
|
+
const url = parseUrl(req.url)
|
|
187
|
+
if (url === null || url.pathname !== '/') return false
|
|
188
|
+
// 核心正在做 token→cookie 交换:必须放行,交换完成后它自己会 303 到干净的 "/"。
|
|
189
|
+
// 若在这里抢跳,核心的 dsh-auth-* cookie 永远拿不到,/api 会全 401。
|
|
190
|
+
if (url.searchParams.has('token')) return false
|
|
191
|
+
// 本次文档请求已经带标记,桌面端能正常启动,无需干预。
|
|
192
|
+
if (desktopMarkersOf(req.url) !== null) return false
|
|
193
|
+
res.writeHead(302, { location: '/?' + pending })
|
|
194
|
+
res.end()
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const desktopAdapter = { name: 'dsh-desktop', remember, restore }
|
|
199
|
+
|
|
200
|
+
// 纯函数供回归测试直接调用(见 test/redirect-scheme.test.mjs)
|
|
201
|
+
export {
|
|
202
|
+
remember, restore,
|
|
203
|
+
desktopMarkersOf, desktopStashCookie, desktopStashQuery, collectDesktopMarkers,
|
|
204
|
+
DESKTOP_STASH_COOKIE, DESKTOP_MARKER_KEYS, DESKTOP_STASH_TTL_SECONDS,
|
|
205
|
+
}
|
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"
|
|
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",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"index.js",
|
|
18
18
|
"lib/client.js",
|
|
19
|
+
"lib/desktop-adapter.js",
|
|
19
20
|
"cordis.patch.yml",
|
|
20
21
|
"README.md",
|
|
21
22
|
"README.en.md",
|