@lynn123411/dsh-web-auth-url 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lynn123411
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @lynn123411/dsh-web-auth-url
2
+
3
+ DSH Web GUI 的访问鉴权补位:把当前实例**带 token 的访问地址**交给模型与 shell。`dsh-web-app` 有意只把干净地址交给模型(`app:web-surface` 段落)和 shell(`DSH_WEB_URL`),带 token 的地址只打印到 stdout 并交给默认浏览器——于是模型自测这个 GUI(抓页面、探路由、确认客户端插件有没有生效)时第一枪必然 401,只能翻启动器日志找 token。本插件把这半补上,且不把 secret 写进提示词或 session。
4
+
5
+ ## 特性
6
+
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` 只保留环境变量。
10
+ - **无 web 部署静默降级**:CLI / TUI / 缺 `connection` 的组合下注册照常完成,但 `resolve` 返回空、段落渲染成空串,而空段落由内核 `renderPrompt` 过滤,系统提示词里不留痕迹。
11
+ - **不新增端口、不新增凭据**:token 取自已装载的 `connection` 服务,与启动时打印到 stdout 的是同一个。
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ dsh plugin --profile web add @lynn123411/dsh-web-auth-url
17
+ ```
18
+
19
+ 本地开发按仓库规则用 `link:`——`cd ~/.dsh/profiles/web && pnpm add "link:/Users/tny/Desktop/work/my-dsh/plugins/dsh-web-auth-url" --offline`,并在 profile `package.json` 的 `dsh.profile.bundles` 里登记本包(`dsh plugin add` 会自动登记,手工 link 不会)。宿主半边只在 DSH 启动时装载,改完须重启实例;自检 `npm test` 跑 `scripts/smoke-host.mjs`。
@@ -0,0 +1,6 @@
1
+ # @lynn123411/dsh-web-auth-url bundle patch
2
+ # 声明在 package.json 的 dsh.bundle.patch,`dsh plugin add` 后自动并入 profile 层栈。
3
+ # 只 insert 一行宿主半边:本插件没有客户端半边,也没有要覆盖的内核行。
4
+ - insert:
5
+ - id: dsh-web-auth-url
6
+ name: '@lynn123411/dsh-web-auth-url'
package/lib/index.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @lynn123411/dsh-web-auth-url — 宿主半区(node)。
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、交给默认浏览器。
9
+ *
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
+ * 过滤掉,系统提示词里不留痕迹。
23
+ *
24
+ * @module dsh-web-auth-url
25
+ */
26
+
27
+ import z from '@deepseek-ai/schemastery'
28
+
29
+ /** Cordis 插件名(= patch 行 id)。 */
30
+ export const name = 'dsh-web-auth-url'
31
+
32
+ /** 本插件不硬依赖任何服务:web 服务缺失时静默降级,见模块头。 */
33
+ export const inject = []
34
+
35
+ /** 回环主机名,与 dsh-web-app 同口径(模型在宿主机上,回环即本实例)。 */
36
+ const LOOPBACK_HOST = '127.0.0.1'
37
+
38
+ /** 本插件唯一登记的托管环境变量。 */
39
+ const AUTH_URL_KEY = 'DSH_WEB_AUTH_URL'
40
+
41
+ /** web-app 交给 shell 的干净地址,只在提示词里做对照。 */
42
+ const CLEAN_URL_KEY = 'DSH_WEB_URL'
43
+
44
+ /** shellEnv 贡献者名。 */
45
+ const CONTRIBUTOR_NAME = 'dsh-web-auth-url'
46
+
47
+ /** 提示词段落名。 */
48
+ const SECTION_NAME = 'app:web-auth-url'
49
+
50
+ /** 段落排在 `app:web-surface` 之后,紧挨着它所补充的那段话。 */
51
+ const SECTION_OFFSET = 10
52
+
53
+ /** 插件配置。 */
54
+ export const Config = z.object({
55
+ /**
56
+ * 提示词段落如何交代这个地址。
57
+ *
58
+ * - `env`:只报 `$DSH_WEB_AUTH_URL`(默认;token 不进提示词与 transcript)
59
+ * - `inline`:把带 token 的地址直接写进段落(地址每次装配即时解析,重启后
60
+ * 不会过期,但 token 会随每次请求发给模型服务商并落盘进 session)
61
+ * - `off`:不注册段落,只保留环境变量
62
+ */
63
+ prompt: z
64
+ .union([z.const('env'), z.const('inline'), z.const('off')])
65
+ .default('env')
66
+ .description('how the prompt section names the authenticated URL: env (point at $DSH_WEB_AUTH_URL), inline (print the tokenized URL), off (no section)'),
67
+ })
68
+
69
+ /**
70
+ * 解析本进程当前的带 token 地址。
71
+ * @param ctx - 任意能读到 webServer / connection 的上下文。
72
+ * @returns 带 token 的回环地址;没有 web 部署时 undefined。
73
+ */
74
+ function resolveAuthenticatedUrl(ctx) {
75
+ const port = ctx.get('webServer')?.port
76
+ const connection = ctx.get('connection')
77
+ if (port === undefined || typeof connection?.authenticatedUrl !== 'function') return undefined
78
+ return connection.authenticatedUrl(`http://${LOOPBACK_HOST}:${String(port)}`)
79
+ }
80
+
81
+ /**
82
+ * 渲染提示词段落。
83
+ * @param ctx - 装配用的上下文。
84
+ * @param mode - 已解析的 `prompt` 配置。
85
+ * @returns 段落文本;地址不可解析时返回空串(等于不贡献段落)。
86
+ */
87
+ function promptText(ctx, mode) {
88
+ const url = resolveAuthenticatedUrl(ctx)
89
+ 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(' ')
102
+ }
103
+
104
+ /**
105
+ * 装载插件:登记托管环境变量,并按配置贡献提示词段落。
106
+ * @param ctx - 宿主插件上下文。
107
+ * @param config - 已解析的 {@link Config}。
108
+ */
109
+ export function apply(ctx, config) {
110
+ const mode = config?.prompt ?? 'env'
111
+
112
+ ctx.inject(['shellEnv'], (envCtx) => {
113
+ envCtx.shellEnv.register({
114
+ name: CONTRIBUTOR_NAME,
115
+ variables: {
116
+ [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.',
118
+ },
119
+ },
120
+ resolve: () => {
121
+ const url = resolveAuthenticatedUrl(envCtx)
122
+ return url === undefined ? {} : { [AUTH_URL_KEY]: url }
123
+ },
124
+ })
125
+ })
126
+
127
+ if (mode === 'off') return
128
+ ctx.inject(['systemPrompt'], (promptCtx) => {
129
+ promptCtx.systemPrompt.section({
130
+ name: SECTION_NAME,
131
+ order: promptCtx.systemPrompt.getSectionOrder('WEB_SURFACE') + SECTION_OFFSET,
132
+ text: () => promptText(promptCtx, mode),
133
+ })
134
+ })
135
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@lynn123411/dsh-web-auth-url",
3
+ "version": "0.1.0",
4
+ "description": "把当前 Web 实例的带 token 访问地址交给模型与 shell:托管 DSH_WEB_AUTH_URL(每次 shell 调用即时解析,跟随重启后的 token 轮换),并贡献一段紧随 web-surface 的提示词说明 cookie jar 用法,secret 不进提示词与 transcript。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json",
10
+ "./cordis.patch.yml": "./cordis.patch.yml"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "scripts",
15
+ "cordis.patch.yml",
16
+ "README.md"
17
+ ],
18
+ "license": "MIT",
19
+ "keywords": [
20
+ "deepseek-harness",
21
+ "dsh",
22
+ "dsh-plugin",
23
+ "cordis",
24
+ "web",
25
+ "auth"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/tttnny/my-dsh.git",
30
+ "directory": "plugins/dsh-web-auth-url"
31
+ },
32
+ "scripts": {
33
+ "test": "node scripts/smoke-host.mjs",
34
+ "check": "node scripts/smoke-host.mjs"
35
+ },
36
+ "dependencies": {
37
+ "@deepseek-ai/schemastery": "^3.18.2"
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "^4.0.2"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@deepseek-ai/cordis": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "engines": {
48
+ "node": "^22.19 || >=24"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dsh": {
54
+ "bundle": {
55
+ "patch": "./cordis.patch.yml"
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 宿主半边装配自检:用带守卫的假上下文真跑一遍 apply(),断言登记形状、
3
+ * 即时解析的地址,以及三种 prompt 模式各自的段落文本。
4
+ *
5
+ * 守卫必须真的抛错(末尾有反例断言),否则「未声明的服务」这类断言全是空转。
6
+ */
7
+
8
+ import assert from 'node:assert/strict';
9
+ import { apply, Config, inject, name } from '../lib/index.js';
10
+
11
+ const PORT = 3080;
12
+ const TOKEN = 'launch-token-example';
13
+ const CLEAN_URL = `http://127.0.0.1:${PORT}`;
14
+ const AUTH_URL = `${CLEAN_URL}/?token=${TOKEN}`;
15
+
16
+ /**
17
+ * 造一个假上下文。
18
+ * @param services - 树上可读到的服务。
19
+ * @param declared - 本上下文已 inject 的服务名。
20
+ * @returns 读未声明服务即抛 cordis 同款错误的上下文。
21
+ */
22
+ function makeContext(services, declared = new Set()) {
23
+ const base = {
24
+ get: (key) => services[key],
25
+ inject: (names, callback) => {
26
+ for (const key of names) {
27
+ if (!(key in services)) throw new Error(`fake tree is missing service "${key}"`);
28
+ }
29
+ callback(makeContext(services, new Set(names)));
30
+ },
31
+ };
32
+ return new Proxy(base, {
33
+ get(target, property, receiver) {
34
+ if (typeof property === 'symbol' || property in target) return Reflect.get(target, property, receiver);
35
+ if (!declared.has(property)) throw new Error(`cannot get property "${property}" without inject`);
36
+ return services[property];
37
+ },
38
+ });
39
+ }
40
+
41
+ /** 一套带 web 部署的服务桩。 */
42
+ function webServices() {
43
+ return {
44
+ webServer: { port: PORT },
45
+ connection: { authenticatedUrl: (baseUrl) => `${baseUrl}/?token=${TOKEN}` },
46
+ shellEnv: { register: () => () => {} },
47
+ systemPrompt: { section: () => () => {}, getSectionOrder: (order) => (order === 'WEB_SURFACE' ? 10100 : Number.NaN) },
48
+ };
49
+ }
50
+
51
+ /** 捕获一次 apply() 的登记结果。 */
52
+ function load(config, services = webServices()) {
53
+ const captured = { contributor: undefined, section: undefined };
54
+ services.shellEnv.register = (contributor) => {
55
+ captured.contributor = contributor;
56
+ return () => {};
57
+ };
58
+ services.systemPrompt.section = (section) => {
59
+ captured.section = section;
60
+ return () => {};
61
+ };
62
+ apply(makeContext(services), config);
63
+ return { captured, services };
64
+ }
65
+
66
+ // --- 插件身份 -------------------------------------------------------------
67
+ assert.equal(name, 'dsh-web-auth-url');
68
+ assert.deepEqual(inject, [], '插件必须声明零硬依赖,web 缺失时靠 ctx.get 降级');
69
+ assert.equal(Config({}).prompt, 'env', 'prompt 默认必须是 env(token 不进提示词)');
70
+ assert.equal(Config({ prompt: 'inline' }).prompt, 'inline');
71
+ assert.throws(() => Config({ prompt: 'nope' }), '非法 prompt 必须被 schema 拒绝');
72
+
73
+ // --- 环境变量 -------------------------------------------------------------
74
+ {
75
+ const { captured } = load({ prompt: 'env' });
76
+ const contributor = captured.contributor;
77
+ assert.equal(contributor.name, 'dsh-web-auth-url');
78
+ assert.deepEqual(Object.keys(contributor.variables), ['DSH_WEB_AUTH_URL']);
79
+ assert.ok(contributor.variables.DSH_WEB_AUTH_URL.description.trim().length > 0, '登记的变量必须有描述');
80
+ assert.deepEqual(contributor.resolve({}), { DSH_WEB_AUTH_URL: AUTH_URL }, 'resolve 必须给出带 token 的当前地址');
81
+ }
82
+
83
+ // --- token 轮换:每次 resolve 重新解析 -------------------------------------
84
+ {
85
+ const { captured, services } = load({ prompt: 'env' });
86
+ services.connection.authenticatedUrl = (baseUrl) => `${baseUrl}/?token=rotated`;
87
+ assert.deepEqual(captured.contributor.resolve({}), { DSH_WEB_AUTH_URL: `${CLEAN_URL}/?token=rotated` });
88
+ }
89
+
90
+ // --- 无 web 部署:不报错、不贡献值 -----------------------------------------
91
+ {
92
+ const services = webServices();
93
+ delete services.connection;
94
+ delete services.webServer;
95
+ const { captured } = load({ prompt: 'env' }, services);
96
+ assert.deepEqual(captured.contributor.resolve({}), {}, '缺 connection 时 resolve 必须返回空');
97
+ assert.equal(captured.section.text(), '', '缺 connection 时段落必须渲染成空串');
98
+ }
99
+
100
+ // --- 提示词段落 -----------------------------------------------------------
101
+ {
102
+ const { captured } = load({ prompt: 'env' });
103
+ assert.equal(captured.section.name, 'app:web-auth-url');
104
+ assert.equal(captured.section.order, 10110, '段落必须紧跟 WEB_SURFACE(10100)');
105
+ const text = captured.section.text();
106
+ assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 模式必须报出变量名');
107
+ assert.ok(!text.includes(TOKEN), 'env 模式绝不能把 token 写进提示词');
108
+ assert.ok(text.includes('401') && text.includes('303') && text.includes('$JAR'), 'env 模式必须交代 401 / 303 / cookie jar 用法');
109
+ }
110
+ {
111
+ const { captured } = load({ prompt: 'inline' });
112
+ const text = captured.section.text();
113
+ assert.ok(text.includes(AUTH_URL), 'inline 模式必须把带 token 的地址写进段落');
114
+ assert.ok(!text.includes('DSH_WEB_AUTH_URL'), 'inline 模式不再报变量名');
115
+ }
116
+ {
117
+ const { captured } = load({ prompt: 'off' });
118
+ assert.equal(captured.section, undefined, 'off 模式不得注册段落');
119
+ assert.ok(captured.contributor !== undefined, 'off 模式仍须登记环境变量');
120
+ }
121
+
122
+ // --- 守卫自身的反例断言(守卫失效即失败) ---------------------------------
123
+ {
124
+ const ctx = makeContext(webServices());
125
+ assert.throws(() => ctx.shellEnv, /without inject/, '读未声明服务必须抛错,否则上面的断言全是空转');
126
+ assert.equal(ctx.get('shellEnv') !== undefined, true, 'ctx.get 不抛错,可选能力走它');
127
+ }
128
+
129
+ console.log('smoke-host: ok');