dsh-feishu-auth 0.1.3 → 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/AGENTS.md +10 -5
- package/README.md +6 -0
- package/docs/architecture.md +10 -0
- package/docs/release.md +4 -2
- package/lib/client.js +103 -0
- package/package.json +7 -1
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,7 @@ DSH(DeepSeek Harness)Web 界面的飞书 OAuth 登录网关。一个 Cordis
|
|
|
19
19
|
| `lib/gate.js` | 网关引擎:分发拦截与层标记、OAuth 端点、拒绝策略、harness 两段式交接 |
|
|
20
20
|
| `lib/feishu.js` | 飞书接口:授权 URL、换 user_access_token、读用户信息 |
|
|
21
21
|
| `lib/session.js` | HMAC 签名 Cookie(会话 / state)、密钥文件读写 |
|
|
22
|
+
| `lib/client.js` | 浏览器半边:设置面板「退出登录」入口,注册进插槽 `settings.action` |
|
|
22
23
|
| `lib/config.js` | 配置解析与 Cookie 名、TTL 常量 |
|
|
23
24
|
| `lib/urls.js` | Host 规范化、回调地址推导、`next` 开放重定向防护、导航请求判定 |
|
|
24
25
|
| `lib/pages.js` | 提示页(拒绝 / 错误 / 未就绪 / 已登出),全部内联样式 |
|
|
@@ -60,9 +61,10 @@ cd ~/.dsh/plugins/dsh-feishu-auth && node --test # 单元用例
|
|
|
60
61
|
| --- | --- |
|
|
61
62
|
| 启动日志 | `飞书登录已挂载 …` + `网关自检通过(未登录 → HTTP 401,已登录 → HTTP 404)`,无 `[error]` |
|
|
62
63
|
| 未登录访问(浏览器式请求) | `302 → /feishu-auth/login?next=…`;`/api` 无 cookie 时是 `401` JSON |
|
|
63
|
-
| 完整登录交接 | `GET /` → `303 /?token=…` →
|
|
64
|
+
| 完整登录交接 | `GET /` → harness 回 401 时给 **200 同站重进页**(自动跳回 `/`)→ 再访问 `/` 得 `200` 真实应用页;harness 不回 401 时走 `303 /?token=…` → 下发 `dsh-auth-*` → 再访问 `/` 得 `200` |
|
|
64
65
|
| 停用验证 | 探针应从 `302`(网关在岗)变成 `401`(harness 自己的门)——这是确认层真的被摘掉的唯一可靠信号 |
|
|
65
66
|
| `/feishu-auth/status` | `{"gate":"enforce","authenticated":…}` |
|
|
67
|
+
| 设置页入口 | 打开设置面板:头部「关闭」左边出现「退出登录」;点击后落到「已退出登录」页,浏览器里本插件与 harness 的 Cookie 都清空 |
|
|
66
68
|
|
|
67
69
|
## 配置项的生效语义
|
|
68
70
|
|
|
@@ -89,7 +91,7 @@ feishu-auth[error] 拿不到 harness 的入口地址(connection 服务不可
|
|
|
89
91
|
| 现象 | 处理 |
|
|
90
92
|
| --- | --- |
|
|
91
93
|
| 所有请求 503 | 凭证不在进程环境里:确认 `~/.dsh/.env` 后重启 |
|
|
92
|
-
|
|
|
94
|
+
| 页面打不开(0.1.3 之前会停在 harness 的 401 页) | 网关自己收尾:先给同站重进页、再换新凭据,两轮都不行才给「还差一步」页并打 warn。若日志里完全没有恢复行、只有 `[error]` 说拿不到入口地址 → 检查 `ctx.inject(['connection'])` 是否仍被 dsh 支持 |
|
|
93
95
|
| 飞书报 `redirect_uri unmatch` | 回调地址没登记/不一致;临时隧道换域名后必须补登记 |
|
|
94
96
|
| 飞书报 `20010` | 账号不在应用可用范围,或应用版本未发布 |
|
|
95
97
|
| 想立刻放行 | `disable.patch.yml` 启动一次,或给该行加 `disabled: true` 后重启 |
|
|
@@ -101,7 +103,8 @@ feishu-auth[error] 拿不到 harness 的入口地址(connection 服务不可
|
|
|
101
103
|
- **绝不用身份比较判断服务成员。** `ctx.webServer` 是 Cordis traceable 服务,成员读取每次都返回新的包装 Proxy:`server.match === 你的函数` 恒为 false。识别自己的层只能靠符号标记,解包靠 `Symbol.for('cordis.original')`。详见架构文档。
|
|
102
104
|
- **挂载/卸载必须幂等。** 热重载时新层可能先于旧层的 disposer 挂上:只允许最新层卸载,发现遗留层要复用它的原始实现而不是往上叠。
|
|
103
105
|
- **新增行为要补用例**,并确认「旧实现下该用例会红」——否则它没锁住任何东西。
|
|
104
|
-
-
|
|
106
|
+
- **不注入 DOM、不改 harness 的客户端资产**:要出现在界面上只走 dsh 的插槽(`lib/client.js`,见架构文档「客户端半边」),不要往页面里塞元素。
|
|
107
|
+
- **浏览器半边必须保持「注册工厂」形态**:`lib/client.js` 顶层只允许 `window.__ModuleLoader__.load({ id, factory })`;`require` 与一切副作用都必须进 factory,否则运行时直接抛。
|
|
105
108
|
- **提示页只用内联样式**,不能依赖被自己保护的静态资源。
|
|
106
109
|
- 配置解析永不抛错:除凭证外的问题降级为默认值并记录;凭证缺失走故障关闭。
|
|
107
110
|
|
|
@@ -116,13 +119,15 @@ feishu-auth[error] 拿不到 harness 的入口地址(connection 服务不可
|
|
|
116
119
|
|
|
117
120
|
## 与 dsh 版本的耦合点
|
|
118
121
|
|
|
119
|
-
升级 dsh
|
|
122
|
+
升级 dsh 后优先复核这几处,任何一处变了都要同步适配:
|
|
120
123
|
|
|
121
124
|
| 依赖 | 用途 | 失效表现 |
|
|
122
125
|
| --- | --- | --- |
|
|
123
126
|
| `webServer.match(pathname)` 分发点 | 唯一的拦截缝隙 | 插件**拒绝启动**并报错(不会静默放过) |
|
|
124
|
-
| `ctx.inject(['connection'])` | 取 harness 入口地址(两段式交接) | 日志 error
|
|
127
|
+
| `ctx.inject(['connection'])` | 取 harness 入口地址(两段式交接) | 日志 error;只剩同站重进这一步可救,跨站链那类场景会落到「还差一步」页 |
|
|
125
128
|
| `dsh-auth-<authority>` Cookie 前缀 | 登出时清掉 harness 自己的 Cookie | 登出后可能被 harness 直接放回 |
|
|
126
129
|
| `/?token=<launch token>` 兑换约定 | 交接第二段 | 同上 |
|
|
130
|
+
| `dsh.client` + `exports["./client"]` 契约 | 浏览器半边的发现与托管(`/plugins/??<id>/client.js`) | 设置页里没有「退出登录」入口,`/plugins/…` 404 |
|
|
131
|
+
| 插槽名 `settings.action`(`ctx.slots`) | 入口在设置面板里的位置 | 入口不出现;需换槽名并核对 `settings.*` 插槽目录 |
|
|
127
132
|
|
|
128
133
|
另见架构文档「已知边界与风险」。
|
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ dsh web --no-open --host 0.0.0.0 --port 3080 --trusted-host <你的隧道域名>
|
|
|
64
64
|
|
|
65
65
|
`allowedUsers` 该填什么:被拒的人会在页面上看到**他自己**的 `open_id`,抄进去保存即可——配置项热生效,不用重启。
|
|
66
66
|
|
|
67
|
+
## 退出登录
|
|
68
|
+
|
|
69
|
+
**设置面板右上角有一个「退出登录」**(`关闭` 按钮左边)。它调 `/feishu-auth/logout`,会清掉本插件的会话 Cookie **和** harness 自己的 `dsh-auth-*`——后者是关键,只清前者的话 harness 会立刻把人放回去。
|
|
70
|
+
|
|
71
|
+
标签页停在旧的 harness 报错页上时,按一次刷新即可恢复:网关会自己把浏览器接回来(同站重进 → 必要时换一份新凭据),**不需要重新登录**。
|
|
72
|
+
|
|
67
73
|
## 常见问题
|
|
68
74
|
|
|
69
75
|
| 现象 | 处理 |
|
package/docs/architecture.md
CHANGED
|
@@ -127,6 +127,15 @@ sequenceDiagram
|
|
|
127
127
|
|
|
128
128
|
被拒的账号会看到**他自己**的 `open_id`(便于运维填 `allowedUsers`),不泄露他人信息;所有插值经 `escapeHtml`。
|
|
129
129
|
|
|
130
|
+
## 客户端半边
|
|
131
|
+
|
|
132
|
+
设置面板里的「退出登录」入口由 `lib/client.js` 提供——插件唯一的浏览器侧代码,同时消费两个外部契约:
|
|
133
|
+
|
|
134
|
+
- **dsh 的客户端插件机制**(`@deepseek-ai/dsh-client-modules`):包在 `package.json` 里声明 `dsh.client`(`platform: web`、`immediately`),产物挂在 `exports["./client"]`。服务端扫描**所有**这样声明的包(不限于 `@deepseek-ai/*`),按模块图顺序合成 combo 脚本,浏览器侧由 `window.__ModuleLoader__` 惰性执行。产物必须是**注册工厂形态**:文件顶层只调用 `window.__ModuleLoader__.load({ id, factory })`,不得有任何副作用或 `require`(运行时会抛 `requested external … before the module system existed`)。本插件手写这个包装,所以仓库仍然零依赖、零构建。
|
|
135
|
+
- **插槽 API**(`ctx.slots`):`inject(name, …)` 等槽出现后再 `register`。用的是 `settings.action`——「content-column header, before Close」,`kind: list`、`replaceRisk: none`,因此我们的条目是**加**进去的,不替换任何既有 UI。`settings.action` 由设置面板条目在挂载时声明,所以必须先 `inject` 再注册。
|
|
136
|
+
|
|
137
|
+
登出本身仍归服务端:入口只是一个指向 `/feishu-auth/logout` 的普通链接,由网关清掉自己的会话 Cookie **和** harness 的 `dsh-auth-*`——少了后者,harness 会当场上门把人放回去。
|
|
138
|
+
|
|
130
139
|
## 失败模式
|
|
131
140
|
|
|
132
141
|
| 情形 | 行为 |
|
|
@@ -138,6 +147,7 @@ sequenceDiagram
|
|
|
138
147
|
| harness 重启后浏览器仍带旧 `dsh-auth-*` | 响应后判定接管:harness 回 401 → 阶梯(同站重进 → 再交接一次)→ 用户无感恢复 |
|
|
139
148
|
| 浏览器沿跨站链到达(飞书 OAuth 回调) | 同一条阶梯的第一步就是为此设计的:同站重进一次即带上 `SameSite=Strict` 的 `dsh-auth-*` |
|
|
140
149
|
| 浏览器两次都不交凭据(无痕窗口 / 拦截扩展) | 阶梯走完 → 插件自己的「还差一步」页 + 一行 warn,不把 harness 的 401 页转给用户 |
|
|
150
|
+
| 客户端 bundle 没被服务(`/plugins/…` 404) | 设置面板里当然也没有入口:核对 `package.json` 的 `dsh.client` + `exports["./client"]`、文件存在,以及件里注册的 `id` 与包名一致(运行时会对不上就抛) |
|
|
141
151
|
| 启动自检失败 | `[error]` 明确报出:未登录请求未被拦,或持有效会话仍被拒 |
|
|
142
152
|
| 配置项(`allowedUsers` / `sessionMaxAgeDays`)非法 | `allowedUsers` 非法 → 致命(避免悄悄放宽到全员);`sessionMaxAgeDays` 非法 → 回落默认值 |
|
|
143
153
|
|
package/docs/release.md
CHANGED
|
@@ -19,11 +19,13 @@ npm 路径靠 `package.json` 的 `dsh.bundle.patch` 声明自己是组合包,`
|
|
|
19
19
|
```bash
|
|
20
20
|
npm ci # 只装 devDependencies(eslint);运行时零依赖
|
|
21
21
|
npm run lint
|
|
22
|
-
npm test # node --test,
|
|
22
|
+
npm test # node --test,50 个用例
|
|
23
23
|
npm run verify # lint + test —— CI 与 prepublishOnly 跑的就是它
|
|
24
|
-
npm pack --dry-run # 检查发布产物内容(
|
|
24
|
+
npm pack --dry-run # 检查发布产物内容(16 个文件)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
`lib/client.js`(浏览器半边)是**手写**的 loader 包装,不是构建产物:文件顶层只允许 `window.__ModuleLoader__.load({ id, factory })`,任何顶层 `require` 或副作用都会被运行时拒绝。改动它的规范见 [docs/architecture.md 的「客户端半边」](architecture.md#客户端半边)。
|
|
28
|
+
|
|
27
29
|
## CI
|
|
28
30
|
|
|
29
31
|
`.github/workflows/ci.yml`,在 push 到 `main`、PR、手动触发时跑:
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the gate: one「退出登录」entry in the settings panel.
|
|
3
|
+
*
|
|
4
|
+
* The server half owns `/feishu-auth/logout`, which clears this gate's session
|
|
5
|
+
* cookie AND the harness's own `dsh-auth-*`, so this half only has to put a link
|
|
6
|
+
* to it in front of the user.
|
|
7
|
+
*
|
|
8
|
+
* Two contracts come from dsh, and both are load-bearing:
|
|
9
|
+
*
|
|
10
|
+
* - The file is a **client bundle**: it registers a factory through
|
|
11
|
+
* `window.__ModuleLoader__.load({ id, factory })` and does nothing else at the
|
|
12
|
+
* top level. `@deepseek-ai/dsh-client-modules` serves it at
|
|
13
|
+
* `/plugins/dsh-feishu-auth/client.js` (it discovers any package declaring
|
|
14
|
+
* `dsh.client` plus `exports["./client"]`, not just `@deepseek-ai/*`), and the
|
|
15
|
+
* runtime rejects a bundle that requests an external before the module system
|
|
16
|
+
* exists. Hence: no top-level `require`, no top-level side effects.
|
|
17
|
+
* - The seat is the slot API's, not ours: `ctx.slots.inject(name, …)` waits for
|
|
18
|
+
* the slot to exist — `settings.action` is declared by the settings panel
|
|
19
|
+
* entry, so it only exists while that panel is mounted — and `register` adds
|
|
20
|
+
* an entry beside the shipped ones (`id` of our own, so nothing is replaced).
|
|
21
|
+
*
|
|
22
|
+
* Styles stay inline: a client bundle may inject CSS from inside its factory
|
|
23
|
+
* closure, but one button does not justify a stylesheet.
|
|
24
|
+
* @module dsh-feishu-auth/client
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
window.__ModuleLoader__.load({
|
|
28
|
+
id: 'dsh-feishu-auth',
|
|
29
|
+
factory: (require) => {
|
|
30
|
+
const module = { exports: {} };
|
|
31
|
+
const exports = module.exports;
|
|
32
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
33
|
+
const React = require('react');
|
|
34
|
+
|
|
35
|
+
/** The seat: content-column header actions, rendered before the Close button. */
|
|
36
|
+
const SLOT = 'settings.action';
|
|
37
|
+
/** This entry's id inside that slot — ours, so the shipped entries stay. */
|
|
38
|
+
const ENTRY_ID = 'feishu-logout';
|
|
39
|
+
/** Where the server half signs this browser out. */
|
|
40
|
+
const LOGOUT_PATH = '/feishu-auth/logout';
|
|
41
|
+
|
|
42
|
+
/** Resting and hover looks, kept close to the shipped header actions. */
|
|
43
|
+
const REST = {
|
|
44
|
+
color: 'var(--dsw-alias-label-secondary, #6b6b70)',
|
|
45
|
+
background: 'transparent',
|
|
46
|
+
};
|
|
47
|
+
const HOVER = {
|
|
48
|
+
color: 'var(--dsw-alias-label-primary, #1d1d1f)',
|
|
49
|
+
background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.12))',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The entry itself: a plain link, so a middle-click or a reload-free
|
|
54
|
+
* navigation both reach the server half.
|
|
55
|
+
* @returns the anchor element.
|
|
56
|
+
*/
|
|
57
|
+
function LogoutAction() {
|
|
58
|
+
const [state, setState] = React.useState(REST);
|
|
59
|
+
return React.createElement(
|
|
60
|
+
'a',
|
|
61
|
+
{
|
|
62
|
+
href: LOGOUT_PATH,
|
|
63
|
+
title: '清掉这台浏览器上的登录凭据',
|
|
64
|
+
style: {
|
|
65
|
+
...state,
|
|
66
|
+
display: 'inline-flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
height: '28px',
|
|
69
|
+
padding: '0 10px',
|
|
70
|
+
border: '0',
|
|
71
|
+
borderRadius: '8px',
|
|
72
|
+
font: 'inherit',
|
|
73
|
+
fontSize: '13px',
|
|
74
|
+
lineHeight: '18px',
|
|
75
|
+
textDecoration: 'none',
|
|
76
|
+
whiteSpace: 'nowrap',
|
|
77
|
+
cursor: 'pointer',
|
|
78
|
+
transition: 'color .12s, background .12s',
|
|
79
|
+
},
|
|
80
|
+
onMouseEnter: () => setState(HOVER),
|
|
81
|
+
onMouseLeave: () => setState(REST),
|
|
82
|
+
},
|
|
83
|
+
'退出登录',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const inject = ['slots'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register the entry, waiting for the settings panel to declare the slot.
|
|
91
|
+
* @param ctx - client plugin context.
|
|
92
|
+
*/
|
|
93
|
+
function apply(ctx) {
|
|
94
|
+
ctx.slots.inject(SLOT, () =>
|
|
95
|
+
ctx.slots.register({ name: SLOT, id: ENTRY_ID, order: 100, label: '退出登录' }, LogoutAction),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
exports.apply = apply;
|
|
100
|
+
exports.inject = inject;
|
|
101
|
+
return module.exports;
|
|
102
|
+
},
|
|
103
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-feishu-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Feishu (Lark) OAuth login gate for the DeepSeek Harness web GUI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"main": "lib/index.js",
|
|
19
19
|
"exports": {
|
|
20
20
|
".": "./lib/index.js",
|
|
21
|
+
"./client": "./lib/client.js",
|
|
21
22
|
"./package.json": "./package.json"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
@@ -40,6 +41,11 @@
|
|
|
40
41
|
"dsh": {
|
|
41
42
|
"bundle": {
|
|
42
43
|
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"inject": [],
|
|
47
|
+
"platform": "web",
|
|
48
|
+
"immediately": true
|
|
43
49
|
}
|
|
44
50
|
},
|
|
45
51
|
"scripts": {
|