@hyzyn/dsh-tty 0.17.2 → 0.18.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/lib/index.d.ts CHANGED
@@ -50,7 +50,8 @@
50
50
  * OSC 133/7 钩子(zsh ZDOTDIR 桩 / bash --rcfile 桩),输出流解析出命令
51
51
  * 边界(tty_capture{last} / tty_expect 早停)与实时 cwd(tty_list)。
52
52
  * 辅助路由:/api/dsh-tty/ssh-config(~/.ssh/config 导入候选)、
53
- * /api/dsh-tty/env-vars(SSH 对话框 env:VAR 下拉,仅变量名)、
53
+ * /api/dsh-tty/credential-refs(凭据存储里已知的引用名,SSH 对话框选择器候选,只要名字)、
54
+ * /api/dsh-tty/env-vars(env 插件托管变量名;SSH 对话框已改从连接簿 + 凭据存储取候选,此路由保留兼容)、
54
55
  * /api/dsh-tty/shells(设置卡片「Shell 路径」候选,仅路径)、
55
56
  * /api/dsh-tty/sftp/*(SFTP 文件传输 0.7.0:list/mkdir/rename/remove/
56
57
  * download/upload;0.8.0 起 mkdir 支持 parents 逐级补齐,spec 解析与
package/lib/index.js CHANGED
@@ -14,7 +14,7 @@ import xtermHeadless from '@xterm/headless';
14
14
  const HeadlessTerminal = xtermHeadless.Terminal;
15
15
  import { definePlugin } from '@hyzyn/dsh-kit';
16
16
  import { defineTool } from '@deepseek-ai/dsh-tools';
17
- import { spawnSsh, sshTarget, expandHome } from './ssh.js';
17
+ import { spawnSsh, sshTarget, expandHome, setCredentialResolver } from './ssh.js';
18
18
  import { probeSsh } from './probe.js';
19
19
  import { buildCommandSpawn, buildShellSpawn } from './shell-integration.js';
20
20
  import { parseSshConfig } from './ssh-config.js';
@@ -1663,6 +1663,51 @@ function readManagedEnvKeys() {
1663
1663
  return [];
1664
1664
  }
1665
1665
  }
1666
+ /**
1667
+ * 凭据存储里**已知的引用名**(~/.dsh/.credentials.yaml 的 `refs:` 块键),只读给
1668
+ * SSH 对话框的引用选择器当候选。
1669
+ *
1670
+ * 为什么必须读文件:官方把「引用半边」设计成**不可枚举**——
1671
+ * `CredentialProvider.listRecords` 的注释原话是 "Unlike the reference half, which has no
1672
+ * enumeration because configuration surfaces learn which references exist from settings
1673
+ * schemas",而浏览器侧 `ctx.remote.credentials`(dsh-api-settings-controller)只开
1674
+ * `describe` / `set` / `unset`,连 `listRecords` 都没开。所以要让用户在下拉里看见
1675
+ * "这本存储里已经有什么名字",宿主侧读文件是唯一出路;**只要键名、不取值**(值只在本函数
1676
+ * 的局部 `lines` 里路过,不进任何返回值,也不写日志)。
1677
+ *
1678
+ * 解析刻意最小(与上面 readManagedEnvKeys 同款,不为它引 YAML 依赖):只认 `refs:` 顶层
1679
+ * 区块内「恰好两个空格 + POSIX 标识符 + 冒号」的行。本地 provider 写入时用 `yaml` 严格
1680
+ * 校验过(version: 1 / 值必须非空字符串 / 键必须是标识符),所以这个格式是稳的;真被手改
1681
+ * 坏了也只是候选少几个 —— 引用最终仍由连接时的凭据层校验存在性。
1682
+ *
1683
+ * 路径解析与本地 provider 的默认一致($DSH_HOME 优先,空串视为未设,再退 ~/.dsh)。边界:
1684
+ * 若有人给 provider 配了自定义 `path` / `dshHome`,这里看不到那些引用(字段仍可手输名字)。
1685
+ */
1686
+ function readCredentialRefNames() {
1687
+ const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
1688
+ const file = join(dshHome, '.credentials.yaml');
1689
+ try {
1690
+ const names = [];
1691
+ let inRefs = false;
1692
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
1693
+ // 顶层键(0 缩进)切换区块;`refs:` 之后的条目才是引用名
1694
+ if (/^[A-Za-z_][A-Za-z0-9_]*:/.test(line)) {
1695
+ inRefs = line.startsWith('refs:');
1696
+ continue;
1697
+ }
1698
+ if (!inRefs)
1699
+ continue;
1700
+ const match = line.match(/^ {2}([A-Za-z_][A-Za-z0-9_]*):/);
1701
+ if (match !== null)
1702
+ names.push(match[1]);
1703
+ }
1704
+ return [...new Set(names)].sort().slice(0, 500);
1705
+ }
1706
+ catch {
1707
+ // 没有存储文件(还没存过任何东西)/ 没有读权限:候选为空,不报错
1708
+ return [];
1709
+ }
1710
+ }
1666
1711
  /**
1667
1712
  * 设置卡片「Shell 路径」候选(可选可输入的数据源):/etc/shells + $SHELL +
1668
1713
  * 常见安装路径,去重后过滤「存在且可执行」,$SHELL 排最前。只回路径,
@@ -2075,6 +2120,22 @@ const plugin = definePlugin({
2075
2120
  }
2076
2121
  return { patch };
2077
2122
  };
2123
+ /*
2124
+ * credentials(**可选**依赖):连接簿里的 `env:NAME` 是**引用**,值归官方凭据 provider ——
2125
+ * 它自己叠 `file`(`$DSH_HOME/.credentials.yaml`)/ `env` / `project-env` / `user-env` 各层,
2126
+ * 并保证「每次操作重新解析」(改完下一个操作即生效,不必重启宿主)。
2127
+ *
2128
+ * 为什么必须走它:凭据存储里的值**永远不会 materialize 进环境**(provider README 原话),
2129
+ * 所以只读 `process.env` 等于"存进凭据存储的密码连接时读不到" —— 「保存时存入凭据存储」
2130
+ * 那条链此前就是断在这里(客户端那半切好了、宿主这半没切)。
2131
+ *
2132
+ * 服务缺失(老宿主 / 未装该 bundle)时不注册,`resolveSecret` 自己退回 `process.env`,行为与
2133
+ * 从前一致 —— 所以这是**可选**依赖,不抬高 engines 下限。
2134
+ */
2135
+ ctx.inject(['credentials'], (credCtx) => {
2136
+ setCredentialResolver(credCtx.credentials ?? null);
2137
+ return () => { setCredentialResolver(null); };
2138
+ });
2078
2139
  // webServer:WS upgrade 路由 + 配置读写路由(/api/dsh-tty/config)
2079
2140
  ctx.inject(['webServer'], (webCtx) => {
2080
2141
  webCtx.effect(() => {
@@ -2179,6 +2240,25 @@ const plugin = definePlugin({
2179
2240
  writeJson(res, 200, { ok: true, names: readManagedEnvKeys() });
2180
2241
  },
2181
2242
  });
2243
+ // 凭据存储里已知的引用名(SSH 对话框选择器候选):只回**名字**,绝不回值。
2244
+ // 为什么不像其它设置界面那样只列"自己 schema 里的引用":官方那条路对这本存储
2245
+ // 来说列不全(引用半边不可枚举,见 readCredentialRefNames),而这个选择器的用途
2246
+ // 恰恰就是"我存过什么、能不能复用"。
2247
+ registerGated({
2248
+ kind: 'exact',
2249
+ path: '/api/dsh-tty/credential-refs',
2250
+ handler: async (req, res) => {
2251
+ if (!isLoopbackHttp(req)) {
2252
+ writeJson(res, 403, { error: 'forbidden: loopback-only' });
2253
+ return;
2254
+ }
2255
+ if (req.method !== 'GET') {
2256
+ writeJson(res, 405, { error: 'method not allowed: ' + String(req.method) });
2257
+ return;
2258
+ }
2259
+ writeJson(res, 200, { ok: true, names: readCredentialRefNames() });
2260
+ },
2261
+ });
2182
2262
  // known_hosts 指纹导入候选(TOFU 预填充):hashed 条目用连接簿主机名还原
2183
2263
  registerGated({
2184
2264
  kind: 'exact',