@lynn123411/dsh-web-auth-url 0.1.2 → 0.1.4

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 CHANGED
@@ -5,10 +5,11 @@ DSH Web GUI 的访问鉴权补位:把当前实例**带 token 的访问地址**
5
5
  ## 特性
6
6
 
7
7
  - **`DSH_WEB_AUTH_URL` 托管环境变量**:每次 shell 调用即时解析的带 token 回环地址(`http://127.0.0.1:<port>/?token=…`),DSH 重启换 token 后自动跟随;不缓存、不落盘。
8
- - **提示词段落 `app:web-auth-url`**:紧随 `app:web-surface`(order + 10)贡献一段说明——只报变量名、不报值,并交代必需用法:第一次请求 `curl -c "$JAR" -b "$JAR" "$DSH_WEB_AUTH_URL"` 拿到 303 并种下 cookie,之后复用同一个 jar 即已授权。
9
- - **`prompt` 三档**:`env`(默认)只报变量名,token 不进提示词与 transcript;`inline` 把带 token 的地址直接写进段落(地址每次装配即时解析,重启后不过期,但 token 会随请求发给模型服务商并落盘进 session);`off` 只保留环境变量。
8
+ - **提示词段落 `app:web-auth-url`**:紧随 `app:web-surface`(order + 10)贡献一段说明——只报变量名、不报值,并交代必需用法:第一次请求 `curl -s -c "$JAR" -b "$JAR" -o /dev/null -w '%{http_code}' "$DSH_WEB_AUTH_URL"` 拿到 303 并种下 cookie,**之后复用同一个 jar、不再带 token**(token 只对 `GET /` 有效,其余路径带它一律 401)。
9
+ - **`prompt` 三档**:`env`(默认)只报变量名,token 不进提示词与 transcript;`inline` 把带 token 的地址直接写进段落(地址每次装配即时解析,重启后不过期,但 token 会随请求发给模型服务商并落盘进 session);`off` 只保留环境变量。两档共用同一份句式,只有地址那处被替换。
10
10
  - **无 web 部署静默降级**:CLI / TUI / 缺 `connection` 的组合下注册照常完成,但 `resolve` 返回空、段落渲染成空串,而空段落由内核 `renderPrompt` 过滤,系统提示词里不留痕迹。
11
11
  - **不新增端口、不新增凭据**:token 取自已装载的 `connection` 服务,与启动时打印到 stdout 的是同一个。
12
+ - **`selfDir()`**:本插件自身安装目录的真实路径(预留工具,当前无调用方)。开发副本是 `link:` 软链接且 `~/.dsh` 与仓库常在不同卷,`ctx.pluginPackages.packageOf(...)` 会返回链接路径;`selfDir()` 走 Node 解析器 + `realpath`,正常发布安装下两者相同。
12
13
 
13
14
  ## 安装
14
15
 
package/lib/index.js CHANGED
@@ -1,34 +1,31 @@
1
1
  /**
2
2
  * @lynn123411/dsh-web-auth-url — 宿主半区(node)。
3
3
  *
4
- * Web GUI 的每个请求都要过 connection 的浏览器会话鉴权,而鉴权的入口只有
5
- * 一个:带本进程 launch token 的根地址。干净地址 `http://127.0.0.1:<port>`
6
- * 一律 401,而且这是 `dsh-web-app` 有意为之——它把干净地址交给模型
7
- * (`app:web-surface` 段落)和 shell(`DSH_WEB_URL`),带 token 的地址只
8
- * 打印到 stdout、交给默认浏览器。
4
+ * connection 的浏览器会话鉴权只有一个入口:带本进程 launch token 的根地址;
5
+ * `dsh-web-app` 有意只把干净地址交给模型与 shell,带 token 的地址仅打印到
6
+ * stdout。于是模型自测这个 GUI 时第一枪必然 401。本插件补上这个缺口而不把
7
+ * secret 放进提示词或 transcript:托管 `DSH_WEB_AUTH_URL`(每次 shell 调用
8
+ * 即时解析,跟随重启后的 token 轮换),并紧随 `app:web-surface` 说明它的
9
+ * 用法——只报变量名,`prompt: 'inline'` 才把地址写进段落。
9
10
  *
10
- * 代价是模型想自测这个 GUI(抓页面、探路由、确认客户端插件有没有生效)时
11
- * 第一枪必然 401,只能翻启动器日志找 token。本插件补上这个缺口,同时不把
12
- * secret 放进提示词或 transcript:
13
- *
14
- * 1. `DSH_WEB_AUTH_URL` —— 每次 shell 调用**即时解析**的带 token 地址,
15
- * 因此 DSH 重启换 token 后自动跟随(静态字符串做不到这一点)。
16
- * 2. 紧随 `app:web-surface`(order + 10)的一段提示词,说明该地址存在,
17
- * 以及「第一次请求 303 换 cookie、之后复用同一个 jar」这个必需用法——
18
- * 只报变量名,不报值;`prompt: 'inline'` 才把地址直接写进段落。
19
- *
20
- * 没有 web 部署的宿主(CLI / TUI / 缺 connection 的组合)不是错误路径:注册
21
- * 照常完成,`resolve` 返回空、段落渲染成空串,而空段落会被 renderPrompt
22
- * 过滤掉,系统提示词里不留痕迹。
11
+ * 没有 web 部署的宿主不是错误路径:注册照常完成,值解析为空、段落渲染成空
12
+ * 串,而空段落会被 renderPrompt 过滤掉。
23
13
  *
24
14
  * @module dsh-web-auth-url
25
15
  */
26
16
 
27
17
  import z from '@deepseek-ai/schemastery'
28
18
 
19
+ import { realpathSync } from 'node:fs'
20
+ import { basename, dirname } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
29
23
  /** Cordis 插件名(= patch 行 id)。 */
30
24
  export const name = 'dsh-web-auth-url'
31
25
 
26
+ /** npm 包名:{@link selfDir} 靠它从本模块 URL 解析自身安装位置。 */
27
+ const PKG_NAME = '@lynn123411/dsh-web-auth-url'
28
+
32
29
  /** 本插件不硬依赖任何服务:web 服务缺失时静默降级,见模块头。 */
33
30
  export const inject = []
34
31
 
@@ -38,9 +35,6 @@ const LOOPBACK_HOST = '127.0.0.1'
38
35
  /** 本插件唯一登记的托管环境变量。 */
39
36
  const AUTH_URL_KEY = 'DSH_WEB_AUTH_URL'
40
37
 
41
- /** web-app 交给 shell 的干净地址,只在提示词里做对照。 */
42
- const CLEAN_URL_KEY = 'DSH_WEB_URL'
43
-
44
38
  /** shellEnv 贡献者名。 */
45
39
  const CONTRIBUTOR_NAME = 'dsh-web-auth-url'
46
40
 
@@ -50,6 +44,26 @@ const SECTION_NAME = 'app:web-auth-url'
50
44
  /** 段落排在 `app:web-surface` 之后,紧挨着它所补充的那段话。 */
51
45
  const SECTION_OFFSET = 10
52
46
 
47
+ /** 段落里出现 token 地址的位置(`env` 档保留字面量,`inline` 档换成实址)。 */
48
+ const URL_PLACEHOLDER = `$${AUTH_URL_KEY}`
49
+
50
+ /**
51
+ * 段落模板:两档共用一份句式,只替换地址,文案不会各自漂移。
52
+ *
53
+ * 首句借 `$DSH_WEB_URL` 把这段挂到 `app:web-surface` 刚报的那台 GUI 上(那
54
+ * 段不提 token / 401 / cookie,两段不重复);末句点明「token 只用于第一次
55
+ * 换 cookie」——connection 的 token 分支要求 `GET /` 且 pathname 恰为 `/`,
56
+ * 其余路径带 token 一律 401,不点破模型会继续在 `/api` 上挂 `?token=`。
57
+ *
58
+ * 地址一律加引号:token 是 base64url,理论上无外壳元字符,但两条分支用同
59
+ * 一种写法才不会有后加字符时的分叉。
60
+ */
61
+ const SECTION_TEMPLATE = [
62
+ 'The Web GUI at `$DSH_WEB_URL` needs auth: its URL and every request carry `$DSH_WEB_AUTH_URL`.',
63
+ 'First `curl -s -c "$JAR" -b "$JAR" -o /dev/null -w \'%{http_code}\' "$DSH_WEB_AUTH_URL"` answers 303',
64
+ 'and sets the session cookie; reuse that jar, not the token, afterwards.',
65
+ ].join(' ')
66
+
53
67
  /** 插件配置。 */
54
68
  export const Config = z.object({
55
69
  /**
@@ -79,7 +93,7 @@ function resolveAuthenticatedUrl(ctx) {
79
93
  }
80
94
 
81
95
  /**
82
- * 渲染提示词段落。
96
+ * 渲染提示词段落:一份模板,只换地址那处。
83
97
  * @param ctx - 装配用的上下文。
84
98
  * @param mode - 已解析的 `prompt` 配置。
85
99
  * @returns 段落文本;地址不可解析时返回空串(等于不贡献段落)。
@@ -87,18 +101,35 @@ function resolveAuthenticatedUrl(ctx) {
87
101
  function promptText(ctx, mode) {
88
102
  const url = resolveAuthenticatedUrl(ctx)
89
103
  if (url === undefined) return ''
90
- const inline = mode === 'inline'
91
- const address = inline
92
- ? url
93
- : `\`$${AUTH_URL_KEY}\` (\`$${CLEAN_URL_KEY}\` is the same origin without the token)`
94
- const target = inline ? url : `"$${AUTH_URL_KEY}"`
95
- return [
96
- 'Shell access to this Web GUI is authenticated: an unauthenticated request to the clean URL answers 401,',
97
- "because the browser session is a signed cookie minted from this process's launch token.",
98
- `The authenticated address is ${address}.`,
99
- `Exchange the token once and keep the jar — \`curl -s -c "$JAR" -b "$JAR" -o /dev/null -w '%{http_code}' ${target}\` answers 303 and sets the cookie;`,
100
- 'every later request through that jar is authorized.',
101
- ].join(' ')
104
+ return mode === 'inline'
105
+ ? SECTION_TEMPLATE.replaceAll(URL_PLACEHOLDER, url)
106
+ : SECTION_TEMPLATE
107
+ }
108
+
109
+ /**
110
+ * 本插件自身安装目录的真实路径(预留工具,当前没有调用方)。
111
+ *
112
+ * 用它,不要用 `ctx.pluginPackages.packageOf(...)`:`packageOf` 只把 specifier
113
+ * 解析成包名并读 `<node_modules>/<包名>/package.json`,返回的可能是**软链接
114
+ * 路径**——开发副本里 `~/.dsh/profiles/web/node_modules/@lynn123411/dsh-*` 正是
115
+ * `link:` 到仓库,跨卷时 Node 的 realpath 不穿透,dirname 出来的是仓库外的
116
+ * 路径。这里走 Node 自己的解析器再 realpath:正常发布安装下两者相同。
117
+ *
118
+ * 宿主半边运行时**不能**用 `import.meta.resolve`(Cordis 装载的模块拿不到该
119
+ * 能力),所以按包名从自己的 URL 解析,再退回自身目录。
120
+ *
121
+ * @returns 本包目录的绝对路径;解析失败时退回本模块所在目录。
122
+ */
123
+ export function selfDir() {
124
+ const fallback = dirname(fileURLToPath(import.meta.url))
125
+ try {
126
+ const entry = fileURLToPath(import.meta.resolve(`${PKG_NAME}`, import.meta.url))
127
+ let dir = dirname(entry)
128
+ if (basename(dir) === 'lib') dir = dirname(dir)
129
+ return realpathSync(dir)
130
+ } catch {
131
+ return fallback
132
+ }
102
133
  }
103
134
 
104
135
  /**
@@ -114,7 +145,7 @@ export function apply(ctx, config) {
114
145
  name: CONTRIBUTOR_NAME,
115
146
  variables: {
116
147
  [AUTH_URL_KEY]: {
117
- description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session, carrying this process launch token for the browser-session exchange.',
148
+ description: 'Authenticated URL of this Web GUI; GET it once with a cookie jar to log in, then reuse the jar.',
118
149
  },
119
150
  },
120
151
  resolve: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-web-auth-url",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "把当前 Web 实例的带 token 访问地址交给模型与 shell:托管 DSH_WEB_AUTH_URL(每次 shell 调用即时解析,跟随重启后的 token 轮换),并贡献一段紧随 web-surface 的提示词说明 cookie jar 用法,secret 不进提示词与 transcript。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -46,11 +46,11 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@deepseek-ai/cordis": "4.0.4",
49
- "@deepseek-ai/dsh-shell-env": "0.1.7-rc.2",
50
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2"
49
+ "@deepseek-ai/dsh-shell-env": ">=0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.7-rc.2"
51
51
  },
52
52
  "engines": {
53
- "dsh": "0.1.7-rc.2",
53
+ "dsh": ">=0.1.7-rc.2",
54
54
  "node": "^22.19 || >=24"
55
55
  },
56
56
  "publishConfig": {
@@ -9,6 +9,9 @@
9
9
  * `app:web-auth-url` 必须紧跟 `app:web-surface`。`getSectionOrder('WEB_SURFACE')`
10
10
  * 返回 undefined 时本插件算出的 order 是 NaN,真实 `section()` 会直接抛错。
11
11
  *
12
+ * 文案断言只钉**承重的那几件事**(两档都带引号的 curl 管道、env 档不出现 token、
13
+ * 末句的「别再用 token」),不钉整句子:段落文案今后再压也不该动自检。
14
+ *
12
15
  * 两个内核包是本插件的 devDependencies(与 engines.dsh 同版本),因此自检不需要
13
16
  * 指向任何 DSH 安装副本,`pnpm install` 后即可离线跑。文件末尾另有一段守卫
14
17
  * 反例:cordis 对未声明服务的属性读取真的抛错,上面的「用 ctx.inject 拿服务」
@@ -19,7 +22,7 @@
19
22
 
20
23
  import assert from 'node:assert/strict';
21
24
  import { createRequire } from 'node:module';
22
- import { existsSync, readFileSync } from 'node:fs';
25
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
23
26
  import { tmpdir } from 'node:os';
24
27
  import { dirname, join } from 'node:path';
25
28
  import { fileURLToPath } from 'node:url';
@@ -30,8 +33,9 @@ import * as ShellEnv from '@deepseek-ai/dsh-shell-env';
30
33
 
31
34
  const require = createRequire(import.meta.url);
32
35
  const here = dirname(fileURLToPath(import.meta.url));
33
- const manifest = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
34
- const { apply, Config, inject, name } = await import('../lib/index.js');
36
+ const packageDir = realpathSync(join(here, '..'));
37
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
38
+ const { apply, Config, inject, name, selfDir } = await import('../lib/index.js');
35
39
 
36
40
  const PORT = 3080;
37
41
  const TOKEN = 'launch-token-example';
@@ -93,16 +97,31 @@ assert.equal(Config({ prompt: 'inline' }).prompt, 'inline');
93
97
  assert.equal(Config({ prompt: 'off' }).prompt, 'off');
94
98
  assert.throws(() => Config({ prompt: 'nope' }), '非法 prompt 必须被 schema 拒绝');
95
99
 
96
- assert.equal(manifest.engines.dsh, '0.1.7-rc.2', 'engines.dsh 必须是精确的目标版本');
100
+ // --- selfDir:本插件自身安装目录(预留工具) ------------------------------
101
+ // 钉「realpath 后的落点 = 本包目录」:开发副本下包名会被解析到 profiles 里的
102
+ // 链接路径,只有 realpath 之后才回到仓库。不 realpath 的实现在开发副本当场挂。
103
+ assert.equal(selfDir(), packageDir, 'selfDir 必须 realpath 回本包目录');
104
+ assert.equal(realpathSync(selfDir()), selfDir(), 'selfDir 必须已经是 realpath,调用方不必再处理链接');
105
+ assert.equal(existsSync(join(selfDir(), manifest.main)), true, 'selfDir 必须能拼出真实入口文件');
106
+ assert.equal(existsSync(join(selfDir(), 'cordis.patch.yml')), true, 'selfDir 必须能拼出随包文件');
107
+
108
+ assert.equal(manifest.engines.dsh, '>=0.1.7-rc.2', 'engines.dsh 必须声明不低于基线 0.1.7-rc.2 的支持范围');
97
109
  assert.equal(manifest.peerDependencies['@deepseek-ai/cordis'], '^4.0.4');
98
110
  assert.equal(installedVersion('@deepseek-ai/cordis'), '4.0.4', 'cordis 必须对齐安装版本');
99
111
  assert.equal(manifest.dependencies['@deepseek-ai/schemastery'], '^3.18.4');
100
112
  assert.equal(installedVersion('@deepseek-ai/schemastery'), '3.18.4', 'schemastery 必须对齐安装版本');
113
+ // engines.dsh 自 0.1.7-rc.2 起写成 >=0.1.7-rc.2(大于等于当前版本),而 devDependencies
114
+ // 仍定向安装基线版本 0.1.7-rc.2,两者刻意不同:声明层必须写同一个范围,安装层必须钉住基线。
101
115
  for (const pkg of ['@deepseek-ai/dsh-system-prompt', '@deepseek-ai/dsh-shell-env']) {
102
116
  assert.equal(
103
- installedVersion(pkg),
117
+ manifest.devDependencies[pkg],
104
118
  manifest.engines.dsh,
105
- `${pkg} 的 devDependency 必须与 engines.dsh 同版本,否则自检跑的不是目标版本`,
119
+ `${pkg} 的 devDependency 必须与 engines.dsh 写同一个版本范围`,
120
+ );
121
+ assert.equal(
122
+ installedVersion(pkg),
123
+ '0.1.7-rc.2',
124
+ '自检跑的内核包必须仍是基线版本 0.1.7-rc.2',
106
125
  );
107
126
  }
108
127
 
@@ -159,24 +178,41 @@ for (const pkg of ['@deepseek-ai/dsh-system-prompt', '@deepseek-ai/dsh-shell-env
159
178
 
160
179
  const text = assembly.sections[mine].text;
161
180
  assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 档必须报出变量名');
181
+ assert.ok(text.includes('DSH_WEB_URL'), '首句必须点到干净地址,才挂得上 app:web-surface 刚报的那台 GUI');
162
182
  assert.ok(!text.includes(TOKEN), 'env 档绝不能把 token 写进段落');
163
183
 
164
184
  const rendered = renderPrompt(assembly);
165
185
  assert.ok(rendered.includes('DSH_WEB_AUTH_URL'), 'env 档渲染后的系统提示词必须交代变量名');
166
186
  assert.ok(!rendered.includes(TOKEN), 'env 档渲染后的系统提示词里绝不能出现 token');
167
187
  assert.ok(
168
- text.includes('401') && text.includes('303') && text.includes('$JAR'),
169
- 'env 档必须交代 401 / 303 / cookie jar 用法',
188
+ text.includes('curl -s -c "$JAR" -b "$JAR" -o /dev/null -w \'%{http_code}\' "$DSH_WEB_AUTH_URL"'),
189
+ 'env 档必须给出带引号的 cookie jar 管道,且地址用变量引用的写法',
190
+ );
191
+ assert.ok(text.includes('303'), 'env 档必须交代第一次请求拿 303 种下 cookie');
192
+ assert.ok(
193
+ /reuse that jar, not the token/.test(text),
194
+ 'env 档必须点明 token 只用于换 cookie——其余路径带 token 一律 401',
170
195
  );
196
+ assert.ok(text.length < 320, `段落已压到 ${String(text.length)} 字符,再涨就该走回那段解释性文案了`);
171
197
  }
172
198
 
173
199
  // --- inline / off 两档 ----------------------------------------------------
174
200
  {
175
201
  const { ctx } = await host();
176
202
  await mount(ctx, { prompt: 'inline' });
177
- const rendered = renderPrompt(await ctx.systemPrompt.assemble({}));
203
+ const assembly = await ctx.systemPrompt.assemble({});
204
+ const text = assembly.sections.find((section) => section.name === SECTION_NAME).text;
205
+ const rendered = renderPrompt(assembly);
178
206
  assert.ok(rendered.includes(AUTH_URL), 'inline 档必须把带 token 的地址写进段落');
179
207
  assert.ok(!rendered.includes('DSH_WEB_AUTH_URL'), 'inline 档不再报变量名');
208
+ assert.ok(
209
+ text.includes(`"${AUTH_URL}"`),
210
+ 'inline 档的地址也要带引号:两档共用同一份模板,只有地址被替换',
211
+ );
212
+ assert.ok(
213
+ /reuse that jar, not the token/.test(text),
214
+ 'inline 档与 env 档同句式,收尾那句同样要在',
215
+ );
180
216
  assert.equal(ctx.shellEnv.collect({}).DSH_WEB_AUTH_URL, AUTH_URL, 'inline 档仍须托管环境变量');
181
217
  }
182
218
  {
@@ -199,7 +235,7 @@ for (const pkg of ['@deepseek-ai/dsh-system-prompt', '@deepseek-ai/dsh-shell-env
199
235
  assert.notEqual(mine, undefined, '段落注册不依赖 web 部署,照常完成');
200
236
  assert.equal(mine.text, '', '缺 connection 时段落必须渲染成空串');
201
237
  assert.ok(
202
- !renderPrompt(assembly).includes('Shell access to this Web GUI'),
238
+ !renderPrompt(assembly).includes('needs auth'),
203
239
  '空段落由真实 renderPrompt 过滤,系统提示词里不留痕迹',
204
240
  );
205
241
  }