@dsh-plus/secret-env 0.1.10 → 0.1.12

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/client.js CHANGED
@@ -223,28 +223,24 @@ window.__ModuleLoader__.load({
223
223
  }
224
224
  //#endregion
225
225
  //#region ../shared/src/client/fetch.ts
226
- /**
227
- * 同源自定义端点的最小 fetch 封装(各插件 client/api.ts 的公共收编版)。
228
- * 仅同源(credentials: same-origin),错误统一抛 Error(body.error)。
229
- * @module @dsh-plus/shared/client/fetch
230
- */
231
- async function parse(res) {
226
+ async function parse(res, guard) {
232
227
  const body = await res.json();
233
228
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
229
+ if (guard !== void 0 && !guard(body)) throw new Error(`响应结构不符合预期(${res.url})`);
234
230
  return body;
235
231
  }
236
232
  /** GET 同源 JSON 端点。 */
237
- async function getJson(route) {
238
- return parse(await fetch(route, { credentials: "same-origin" }));
233
+ async function getJson(route, guard) {
234
+ return parse(await fetch(route, { credentials: "same-origin" }), guard);
239
235
  }
240
236
  /** POST 同源 JSON 端点(body 可省略)。 */
241
- async function postJson(route, body) {
237
+ async function postJson(route, body, guard) {
242
238
  return parse(await fetch(route, {
243
239
  method: "POST",
244
240
  credentials: "same-origin",
245
241
  headers: { "content-type": "application/json" },
246
- body: body === void 0 ? void 0 : JSON.stringify(body)
247
- }));
242
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
243
+ }), guard);
248
244
  }
249
245
  //#endregion
250
246
  //#region ../shared/src/client/i18n.ts
@@ -283,7 +279,10 @@ window.__ModuleLoader__.load({
283
279
  confirm: "Confirm",
284
280
  cancel: "Cancel"
285
281
  };
286
- /** own 覆盖同键合并(返回新对象,入参不可变)。 */
282
+ /**
283
+ * own 覆盖同键合并(返回新对象,入参不可变)。
284
+ * 返回类型保留两侧键集合,调用方据此派生 DictKey。
285
+ */
287
286
  function mergeDict(base, own) {
288
287
  return {
289
288
  ...base,
@@ -519,6 +518,11 @@ ${extra}
519
518
  "error.method": "Method not allowed.",
520
519
  "error.internal": "Internal error; check the host log."
521
520
  };
521
+ /** 服务端错误码 → 文案键的运行期收窄:码来自端点响应,不可当作编译期已知键。 */
522
+ function errorKeyOf(code) {
523
+ const key = `error.${code}`;
524
+ return Object.hasOwn(ownZh, key) ? key : null;
525
+ }
522
526
  const zh = mergeDict(commonZh, ownZh);
523
527
  const en = mergeDict(commonEn, ownEn);
524
528
  //#endregion
@@ -535,8 +539,14 @@ ${extra}
535
539
  this.code = code;
536
540
  }
537
541
  };
542
+ /** 端点应答形状守卫:三个数组字段缺一即判非法(渲染期直接 map,不能靠断言)。 */
543
+ function isSecretList(value) {
544
+ if (typeof value !== "object" || value === null) return false;
545
+ const list = value;
546
+ return Array.isArray(list.global) && Array.isArray(list.session) && Array.isArray(list.inherited);
547
+ }
538
548
  async function fetchSecrets(sessionId) {
539
- return getJson(`/dsh-plus/secret-env/list${sessionId === void 0 ? "" : `?sessionId=${encodeURIComponent(sessionId)}`}`);
549
+ return getJson(`/dsh-plus/secret-env/list${sessionId === void 0 ? "" : `?sessionId=${encodeURIComponent(sessionId)}`}`, isSecretList);
540
550
  }
541
551
  async function write(path, body) {
542
552
  try {
@@ -908,9 +918,8 @@ ${extra}
908
918
  */
909
919
  /** 端点错误 → 本地化文案(未知码回落 internal)。 */
910
920
  function errorText(t, error) {
911
- const code = error instanceof ApiError ? error.code : "internal";
912
- const text = t(`error.${code}`);
913
- return text === `error.${code}` ? t("error.internal") : text;
921
+ const key = errorKeyOf(error instanceof ApiError ? error.code : "internal");
922
+ return key === null ? t("error.internal") : t(key);
914
923
  }
915
924
  /** 名称输入的实时规范化:键入即转大写(去空白交给保存时的 normalizeSuffix)。 */
916
925
  function liveName(raw) {
package/lib/index.d.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  //#region src/config.d.ts
4
+ /** 一条全局密钥的元数据(值不在此处)。 */
5
+ interface SecretMeta {
6
+ name: string;
7
+ description: string;
8
+ createdAt: string;
9
+ }
10
+ /** `secrets` 条目入参形态(schema 各字段均有 default,故调用方可省略)。 */
11
+ interface SecretMetaInput {
12
+ name?: string;
13
+ description?: string;
14
+ createdAt?: string;
15
+ }
4
16
  declare const Config: z<Schemastery.ObjectS<{
5
- secrets: z<unknown[], any[]>;
17
+ secrets: z<SecretMetaInput[], SecretMeta[]>;
6
18
  masked: z<string[], string[]>;
7
19
  }>, Schemastery.ObjectT<{
8
- secrets: z<unknown[], any[]>;
20
+ secrets: z<SecretMetaInput[], SecretMeta[]>;
9
21
  masked: z<string[], string[]>;
10
22
  }>>;
11
23
  type SecretEnvConfig = Schemastery.TypeT<typeof Config>;
@@ -17,7 +29,8 @@ interface GlobalEntry {
17
29
  envName: string;
18
30
  description: string;
19
31
  configured: boolean;
20
- source?: string;
32
+ /** 并上 undefined:构造处可能无来源(服务端),wire 上 JSON 会省略该键,故与缺席等价。 */
33
+ source?: string | undefined;
21
34
  writable: boolean;
22
35
  /** 会话视图:该变量在本会话被屏蔽(无 sessionId 的调用恒为 false)。 */
23
36
  masked: boolean;
@@ -121,7 +134,7 @@ declare class SecretEnvError extends Error {
121
134
  //#endregion
122
135
  //#region src/index.d.ts
123
136
  declare const name = "dsh-plus-secret-env";
124
- declare const inject: readonly ["credentials", "shellEnv"];
137
+ declare const inject: readonly ['credentials', 'shellEnv'];
125
138
  declare module '@deepseek-ai/cordis' {
126
139
  interface Context {
127
140
  secretEnv: SecretEnvService;
package/lib/index.js CHANGED
@@ -17,7 +17,6 @@ const SETTINGS_NS = "dsh-plus-secret-env";
17
17
  * 值永远走 dsh-credentials seam($DSH_HOME/.credentials.yaml),不进本文件。
18
18
  * @module secret-env/config
19
19
  */
20
- /** 一条全局密钥的元数据(值不在此处)。 */
21
20
  const SecretMetaSchema = z.object({
22
21
  name: z.string().min(1).description("变量名后缀(如 GITHUB_TOKEN)").default(""),
23
22
  description: z.string().description("用途描述(仅人读,不进提示词)").default(""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/secret-env",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "dsh-plus service+ui plugin: 密钥环境变量——以 $DSH_VAR_* 变量名向 agent 暴露密钥(全局持久/会话级/一次性),执行期经 dsh-shell-env 原生注入,值不进消息流,不影响缓存率",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -19,26 +19,26 @@
19
19
  "src"
20
20
  ],
21
21
  "dependencies": {
22
- "@dsh-plus/shared": "0.1.15"
22
+ "@dsh-plus/shared": "0.1.17"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@deepseek-ai/cordis": "^4.0.2",
26
- "@deepseek-ai/dsh-credentials": "^0.1.5-rc.1",
27
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
28
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
29
- "@deepseek-ai/dsh-shell-env": "^0.1.5-rc.1",
26
+ "@deepseek-ai/dsh-credentials": "^0.1.5-rc.2",
27
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
28
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
29
+ "@deepseek-ai/dsh-shell-env": "^0.1.5-rc.2",
30
30
  "@deepseek-ai/schemastery": "^3.18.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@deepseek-ai/cordis": "4.0.2",
34
- "@deepseek-ai/dsh-client-ui-commands": "0.1.5-rc.1",
35
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.1",
36
- "@deepseek-ai/dsh-credentials": "0.1.5-rc.1",
37
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
38
- "@deepseek-ai/dsh-session": "0.1.5-rc.1",
39
- "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
40
- "@deepseek-ai/dsh-shell-env": "0.1.5-rc.1",
41
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
34
+ "@deepseek-ai/dsh-client-ui-commands": "0.1.5-rc.2",
35
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
36
+ "@deepseek-ai/dsh-credentials": "0.1.5-rc.2",
37
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
38
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2",
39
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
40
+ "@deepseek-ai/dsh-shell-env": "0.1.5-rc.2",
41
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
42
42
  "@deepseek-ai/schemastery": "3.18.2",
43
43
  "@types/node": "26.4.0",
44
44
  "@types/react": "~18.3.31",
package/src/client/api.ts CHANGED
@@ -47,14 +47,24 @@ interface WriteResult {
47
47
 
48
48
  /** 端点错误:携带结构化错误码,UI 按码映射文案。 */
49
49
  export class ApiError extends Error {
50
- constructor(readonly code: string) {
50
+ readonly code: string
51
+
52
+ constructor(code: string) {
51
53
  super(code)
54
+ this.code = code
52
55
  }
53
56
  }
54
57
 
58
+ /** 端点应答形状守卫:三个数组字段缺一即判非法(渲染期直接 map,不能靠断言)。 */
59
+ function isSecretList(value: unknown): value is SecretList {
60
+ if (typeof value !== 'object' || value === null) return false
61
+ const list = value as { global?: unknown; session?: unknown; inherited?: unknown }
62
+ return Array.isArray(list.global) && Array.isArray(list.session) && Array.isArray(list.inherited)
63
+ }
64
+
55
65
  export async function fetchSecrets(sessionId?: string): Promise<SecretList> {
56
66
  const query = sessionId === undefined ? '' : `?sessionId=${encodeURIComponent(sessionId)}`
57
- return getJson<SecretList>(`/dsh-plus/secret-env/list${query}`)
67
+ return getJson<SecretList>(`/dsh-plus/secret-env/list${query}`, isSecretList)
58
68
  }
59
69
 
60
70
  async function write(path: string, body: Record<string, unknown>): Promise<void> {
@@ -11,6 +11,7 @@
11
11
  import type { Context } from '@deepseek-ai/cordis'
12
12
 
13
13
  import { registerVarCommand } from './command.ts'
14
+ import type { Translate } from './i18n.ts'
14
15
  import { en, NS, zh } from './i18n.ts'
15
16
  import { SecretMenu, type TokenSpanLike } from './menu.tsx'
16
17
  import { SessionPanelHost } from './panel-host.tsx'
@@ -29,7 +30,7 @@ interface SlotsLike {
29
30
 
30
31
  interface LocaleLike {
31
32
  register(ns: string, dict: { zh: Record<string, string>; en: Record<string, string> }): () => void
32
- bind(ns: string): (key: string) => string
33
+ bind(ns: string): Translate
33
34
  }
34
35
 
35
36
  /** 会话作用域句柄的结构子集(dsh-api-session-controller 客户端 sessions 服务)。 */
@@ -7,7 +7,7 @@
7
7
  * @module secret-env/client/command
8
8
  */
9
9
  import type { CommandContribution } from '@deepseek-ai/dsh-client-ui-commands/client'
10
-
10
+ import type { Translate } from './i18n.ts'
11
11
  import { requestOpenSessionPanel } from './panel-bus.ts'
12
12
 
13
13
  /**
@@ -37,7 +37,7 @@ interface SessionsAvailabilityLike {
37
37
  export function registerVarCommand(
38
38
  ctx: CommandHostContext,
39
39
  sessions: SessionsAvailabilityLike,
40
- t: (key: string) => string,
40
+ t: Translate,
41
41
  ): void {
42
42
  ctx.inject(['commandUi'], (scope) => {
43
43
  scope.effect(
@@ -5,13 +5,14 @@
5
5
  */
6
6
  import { type NameError, normalizeSuffix, validateSuffix } from '../names.ts'
7
7
  import { ApiError } from './api.ts'
8
+ import { errorKeyOf, type Translate } from './i18n.ts'
8
9
 
9
10
  /** 端点错误 → 本地化文案(未知码回落 internal)。 */
10
- export function errorText(t: (key: string) => string, error: unknown): string {
11
+ export function errorText(t: Translate, error: unknown): string {
11
12
  const code = error instanceof ApiError ? error.code : 'internal'
12
- const text = t(`error.${code}`)
13
- // locale.bind 对缺失键原样回显键名,据此判断回落。
14
- return text === `error.${code}` ? t('error.internal') : text
13
+ // 错误码来自端点响应,运行期收窄到字典已知键;未知码回落 internal。
14
+ const key = errorKeyOf(code)
15
+ return key === null ? t('error.internal') : t(key)
15
16
  }
16
17
 
17
18
  /** 名称输入的实时规范化:键入即转大写(去空白交给保存时的 normalizeSuffix)。 */
@@ -74,7 +74,7 @@ const ownZh = {
74
74
  'error.no-session': '缺少会话标识,请刷新后重试。',
75
75
  'error.method': '请求方法不允许。',
76
76
  'error.internal': '服务内部错误,请查看宿主日志。',
77
- }
77
+ } as const
78
78
 
79
79
  const ownEn = {
80
80
  nav: 'Env Variables',
@@ -140,7 +140,19 @@ const ownEn = {
140
140
  'error.no-session': 'Missing session id; refresh and retry.',
141
141
  'error.method': 'Method not allowed.',
142
142
  'error.internal': 'Internal error; check the host log.',
143
+ } as const
144
+
145
+ export type DictKey = keyof typeof commonZh | keyof typeof ownZh
146
+
147
+ /** 卡片/面板共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
148
+ export type Translate = (key: DictKey) => string
149
+
150
+ /** 服务端错误码 → 文案键的运行期收窄:码来自端点响应,不可当作编译期已知键。 */
151
+ export function errorKeyOf(code: string): DictKey | null {
152
+ const key = `error.${code}`
153
+ return Object.hasOwn(ownZh, key) ? (key as DictKey) : null
143
154
  }
144
155
 
145
- export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
146
- export const en: Record<string, string> = mergeDict(commonEn, ownEn)
156
+ // 标注为 Record<DictKey, string>:en 缺任一键即编译期报错(中英强制对齐)。
157
+ export const zh: Record<DictKey, string> = mergeDict(commonZh, ownZh)
158
+ export const en: Record<DictKey, string> = mergeDict(commonEn, ownEn)
@@ -10,6 +10,7 @@
10
10
  import { type ReactElement, useEffect, useRef, useState } from 'react'
11
11
 
12
12
  import { fetchSecrets, type SecretList } from './api.ts'
13
+ import type { Translate } from './i18n.ts'
13
14
  import {
14
15
  applyChipCorrection,
15
16
  type ChipCorrection,
@@ -38,7 +39,7 @@ export interface TokenSpanLike {
38
39
  export interface SecretMenuProps {
39
40
  /** 插槽 inject 工厂注入的当前会话 id。 */
40
41
  sessionId: string
41
- t(key: string): string
42
+ t: Translate
42
43
  /** 框架标准钩子:输入状态快照订阅(SnapshotSelectorHook)。 */
43
44
  useInput?<S>(selector: (state: InputStateLike) => S): S
44
45
  /** inject 面注入:经官方 scoped bail 通道替换令牌文本;返回是否被编辑器应用。 */
@@ -6,14 +6,14 @@
6
6
  * @module secret-env/client/panel-host
7
7
  */
8
8
  import { type ReactElement, useEffect, useRef, useState } from 'react'
9
-
9
+ import type { Translate } from './i18n.ts'
10
10
  import { SessionSecretsPanel } from './panel.tsx'
11
11
  import { onOpenSessionPanel } from './panel-bus.ts'
12
12
 
13
13
  export interface SessionPanelHostProps {
14
14
  /** 插槽 inject 工厂注入的当前会话 id。 */
15
15
  sessionId: string
16
- t(key: string): string
16
+ t: Translate
17
17
  }
18
18
 
19
19
  export function SessionPanelHost(props: SessionPanelHostProps): ReactElement | null {
@@ -28,10 +28,11 @@ import {
28
28
  unsetSession,
29
29
  } from './api.ts'
30
30
  import { copyText, errorText, liveName, nameErrorOf } from './common.ts'
31
+ import type { Translate } from './i18n.ts'
31
32
 
32
33
  export interface SessionSecretsPanelProps {
33
34
  sessionId: string
34
- t(key: string): string
35
+ t: Translate
35
36
  /** 提供时渲染头栏(标题/刷新/关闭),供 overlay 弹层模式使用。 */
36
37
  onClose?(): void
37
38
  }
@@ -46,7 +47,7 @@ interface FormState {
46
47
  const EMPTY_FORM: FormState = { name: '', value: '', description: '', once: false }
47
48
 
48
49
  /** 复制按钮(复制成功短暂亮勾)。 */
49
- function CopyButton(props: { t(key: string): string; text: string }): ReactElement {
50
+ function CopyButton(props: { t: Translate; text: string }): ReactElement {
50
51
  const { t } = props
51
52
  const [copied, setCopied] = useState(false)
52
53
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -77,11 +78,7 @@ function CopyButton(props: { t(key: string): string; text: string }): ReactEleme
77
78
  }
78
79
 
79
80
  /** 会话内屏蔽开关(眼睛语义:可见=注入,划掉=本会话屏蔽)。 */
80
- function MaskToggle(props: {
81
- t(key: string): string
82
- masked: boolean
83
- onToggle(): void
84
- }): ReactElement {
81
+ function MaskToggle(props: { t: Translate; masked: boolean; onToggle(): void }): ReactElement {
85
82
  const { t, masked } = props
86
83
  return (
87
84
  <button
@@ -27,6 +27,7 @@ import {
27
27
  unsetGlobal,
28
28
  } from './api.ts'
29
29
  import { copyText, errorText, liveName, nameErrorOf } from './common.ts'
30
+ import type { Translate } from './i18n.ts'
30
31
  import { SessionSecretsPanel } from './panel.tsx'
31
32
 
32
33
  /** sessions 服务的列表快照面(结构子集;ObservableSnapshot 契约)。 */
@@ -39,7 +40,7 @@ export interface SessionsListLike {
39
40
  }
40
41
 
41
42
  export interface SectionProps {
42
- t(key: string): string
43
+ t: Translate
43
44
  sessions?: SessionsListLike
44
45
  }
45
46
 
@@ -52,7 +53,7 @@ interface FormState {
52
53
  const EMPTY_FORM: FormState = { name: '', value: '', description: '' }
53
54
 
54
55
  /** 复制按钮(复制成功短暂亮勾)。 */
55
- function CopyButton(props: { t(key: string): string; text: string }): ReactElement {
56
+ function CopyButton(props: { t: Translate; text: string }): ReactElement {
56
57
  const { t } = props
57
58
  const [copied, setCopied] = useState(false)
58
59
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
package/src/config.ts CHANGED
@@ -13,8 +13,23 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
13
13
  export const SETTINGS_NS = NS_LITERAL
14
14
 
15
15
  /** 一条全局密钥的元数据(值不在此处)。 */
16
- // biome-ignore lint/suspicious/noExplicitAny: dts 可移植性——嵌套 schema 的精确类型经 TypeT 消费,显式断掉 cosmokit 推断链
17
- const SecretMetaSchema: any = z.object({
16
+ export interface SecretMeta {
17
+ name: string
18
+ description: string
19
+ createdAt: string
20
+ }
21
+
22
+ /** `secrets` 条目入参形态(schema 各字段均有 default,故调用方可省略)。 */
23
+ export interface SecretMetaInput {
24
+ name?: string
25
+ description?: string
26
+ createdAt?: string
27
+ }
28
+
29
+ // 显式标注而非 `any`:z.object() 的推断类型含 cosmokit 的 `& Dict` 索引签名,
30
+ // 直接导出会触发 TS2883(inferred type cannot be named / not portable)并使
31
+ // tsdown 的 dts 生成失败。标注成具名契约后既保住类型、又让产物可移植。
32
+ const SecretMetaSchema: z<SecretMetaInput, SecretMeta> = z.object({
18
33
  name: z.string().min(1).description('变量名后缀(如 GITHUB_TOKEN)').default(''),
19
34
  description: z.string().description('用途描述(仅人读,不进提示词)').default(''),
20
35
  createdAt: z.string().description('创建时间 ISO').default(''),
@@ -29,10 +44,3 @@ export const Config = z.object({
29
44
  })
30
45
 
31
46
  export type SecretEnvConfig = Schemastery.TypeT<typeof Config>
32
-
33
- /** 一条元数据的运行时形态。 */
34
- export interface SecretMeta {
35
- name: string
36
- description: string
37
- createdAt: string
38
- }
package/src/inventory.ts CHANGED
@@ -20,7 +20,8 @@ export interface GlobalEntry {
20
20
  envName: string
21
21
  description: string
22
22
  configured: boolean
23
- source?: string
23
+ /** 并上 undefined:构造处可能无来源(服务端),wire 上 JSON 会省略该键,故与缺席等价。 */
24
+ source?: string | undefined
24
25
  writable: boolean
25
26
  /** 会话视图:该变量在本会话被屏蔽(无 sessionId 的调用恒为 false)。 */
26
27
  masked: boolean