@dsh-plus/secret-env 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.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 浏览器半文案(zh/en)。公共键来自 shared common,本文件只维护业务键。
3
+ * @module secret-env/client/i18n
4
+ */
5
+ import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
6
+
7
+ export const NS = 'dsh-plus-secret-env'
8
+
9
+ const ownZh = {
10
+ nav: '密钥变量',
11
+ title: '密钥变量',
12
+ description:
13
+ '以 $DSH_SECRET_* 变量名向 agent 暴露密钥:值在每次 shell 调用时由宿主注入,不进入对话记录与提示词,不影响上下文缓存。',
14
+ // 设置页(全局)
15
+ globalList: '全局密钥',
16
+ colName: '变量名(模型可见)',
17
+ colDescription: '描述',
18
+ colState: '状态',
19
+ colActions: '操作',
20
+ configured: '已配置',
21
+ unconfigured: '未配置',
22
+ readonly: '只读来源',
23
+ addGlobal: '添加全局密钥',
24
+ nameLabel: '变量名后缀',
25
+ nameHint: '大写字母/数字/下划线,如 GITHUB_TOKEN;模型实际使用 $DSH_SECRET_ 前缀拼接后的完整名。',
26
+ valueLabel: '值',
27
+ valueHint: '写入后不可回读;保存于宿主凭据库,不进入对话。',
28
+ descLabel: '描述(可选)',
29
+ descHint: '仅人读备注,不进提示词。',
30
+ save: '保存',
31
+ saving: '保存中…',
32
+ delete: '删除',
33
+ deleteConfirm: '确认删除 {name}?该变量将立即对后续命令失效。',
34
+ copy: '复制变量名',
35
+ copied: '已复制',
36
+ emptyGlobal: '暂无全局密钥。添加后 agent 即可通过变量名引用。',
37
+ loadFailed: '加载失败,请稍后重试。',
38
+ retry: '重试',
39
+ // 会话注入控件
40
+ sessionSecrets: '会话密钥',
41
+ sessionEmpty: '本会话暂无有默认值的密钥。',
42
+ sessionHint: '仅当前会话的 shell 命令可用;会话结束或一次性使用即失效。',
43
+ addSession: '添加会话密钥',
44
+ onceLabel: '一次性(首次使用后销毁)',
45
+ close: '关闭',
46
+ refresh: '刷新',
47
+ scopeGlobal: '全局',
48
+ scopeSession: '会话',
49
+ scopeOnce: '一次性',
50
+ // 错误码
51
+ 'error.invalid-name': '变量名不合法:大写字母开头,仅字母/数字/下划线,≤64 字符。',
52
+ 'error.empty-value': '值不能为空。',
53
+ 'error.shadowed': '进程环境中已存在同名变量且优先级更高,写入无效;请先取消该环境变量。',
54
+ 'error.conflict': '变量名与其他插件注册的受管变量冲突。',
55
+ 'error.no-session': '缺少会话标识,请刷新后重试。',
56
+ 'error.method': '请求方法不允许。',
57
+ 'error.internal': '服务内部错误,请查看宿主日志。',
58
+ }
59
+
60
+ const ownEn = {
61
+ nav: 'Secret Variables',
62
+ title: 'Secret Variables',
63
+ description:
64
+ 'Expose secrets to the agent as $DSH_SECRET_* variables: values are injected by the host into each shell call, never enter the transcript or prompt, and do not affect context caching.',
65
+ globalList: 'Global secrets',
66
+ colName: 'Variable (model-visible)',
67
+ colDescription: 'Description',
68
+ colState: 'State',
69
+ colActions: 'Actions',
70
+ configured: 'Configured',
71
+ unconfigured: 'Not configured',
72
+ readonly: 'Read-only source',
73
+ addGlobal: 'Add global secret',
74
+ nameLabel: 'Name suffix',
75
+ nameHint:
76
+ 'Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $DSH_SECRET_ prefix.',
77
+ valueLabel: 'Value',
78
+ valueHint: 'Write-only after saving; stored in the host credential store, never in chat.',
79
+ descLabel: 'Description (optional)',
80
+ descHint: 'Human-readable note only; never enters prompts.',
81
+ save: 'Save',
82
+ saving: 'Saving…',
83
+ delete: 'Delete',
84
+ deleteConfirm: 'Delete {name}? The variable stops working for subsequent commands immediately.',
85
+ copy: 'Copy variable name',
86
+ copied: 'Copied',
87
+ emptyGlobal: 'No global secrets yet. Once added, the agent can reference them by name.',
88
+ loadFailed: 'Failed to load. Try again later.',
89
+ retry: 'Retry',
90
+ sessionSecrets: 'Session secrets',
91
+ sessionEmpty: 'No session secrets yet.',
92
+ sessionHint:
93
+ 'Available to shell commands of this session only; expires when the session ends or after one use.',
94
+ addSession: 'Add session secret',
95
+ onceLabel: 'One-time (destroyed after first use)',
96
+ close: 'Close',
97
+ refresh: 'Refresh',
98
+ scopeGlobal: 'global',
99
+ scopeSession: 'session',
100
+ scopeOnce: 'once',
101
+ 'error.invalid-name':
102
+ 'Invalid name: start with an uppercase letter; letters/digits/underscore only, ≤64 chars.',
103
+ 'error.empty-value': 'Value must not be empty.',
104
+ 'error.shadowed': 'A same-named process environment variable shadows this write; unset it first.',
105
+ 'error.conflict': 'The variable conflicts with a managed variable registered by another plugin.',
106
+ 'error.no-session': 'Missing session id; refresh and retry.',
107
+ 'error.method': 'Method not allowed.',
108
+ 'error.internal': 'Internal error; check the host log.',
109
+ }
110
+
111
+ export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
112
+ export const en: Record<string, string> = mergeDict(commonEn, ownEn)
@@ -0,0 +1,258 @@
1
+ /**
2
+ * 密钥变量设置页(settings.section 官方插槽):全局密钥的列表与增删。
3
+ * 只展示元数据与模型可见的完整变量名——值写入后不可回读(端点不出值)。
4
+ * 响应式:≤767px 行转堆叠卡、表单转单列。
5
+ * @module secret-env/client/section
6
+ */
7
+ import { type ReactElement, useEffect, useRef, useState } from 'react'
8
+
9
+ import { ApiError, fetchSecrets, type SecretList, setGlobal, unsetGlobal } from './api.ts'
10
+ import { copyText, errorText } from './common.ts'
11
+
12
+ export interface SectionProps {
13
+ t(key: string): string
14
+ }
15
+
16
+ interface FormState {
17
+ name: string
18
+ value: string
19
+ description: string
20
+ }
21
+
22
+ const EMPTY_FORM: FormState = { name: '', value: '', description: '' }
23
+
24
+ /** 单行:完整变量名(复制)+ 描述 + 状态徽标 + 两步删除。 */
25
+ function GlobalRow(props: {
26
+ t: SectionProps['t']
27
+ entry: SecretList['global'][number]
28
+ onDeleted(): void
29
+ onError(text: string): void
30
+ }): ReactElement {
31
+ const { t, entry } = props
32
+ const [confirming, setConfirming] = useState(false)
33
+ const [copied, setCopied] = useState(false)
34
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
35
+
36
+ useEffect(
37
+ () => () => {
38
+ if (timer.current !== null) clearTimeout(timer.current)
39
+ },
40
+ [],
41
+ )
42
+
43
+ const onCopy = (): void => {
44
+ void copyText(`$${entry.envName}`).then((ok) => {
45
+ if (!ok) return
46
+ setCopied(true)
47
+ if (timer.current !== null) clearTimeout(timer.current)
48
+ timer.current = setTimeout(() => setCopied(false), 1500)
49
+ })
50
+ }
51
+
52
+ const onDelete = (): void => {
53
+ unsetGlobal(entry.name)
54
+ .then(() => props.onDeleted())
55
+ .catch((error: unknown) => props.onError(errorText(t, error)))
56
+ }
57
+
58
+ return (
59
+ <div className="dse-row">
60
+ <div className="dse-rowMain">
61
+ <div className="dse-env">
62
+ <span className="dse-envName">${entry.envName}</span>
63
+ <button
64
+ type="button"
65
+ className="dse-iconBtn"
66
+ title={t('copy')}
67
+ aria-label={t('copy')}
68
+ onClick={onCopy}
69
+ >
70
+ {copied ? '✓' : '⧉'}
71
+ </button>
72
+ </div>
73
+ {entry.description !== '' ? <span className="dse-note">{entry.description}</span> : null}
74
+ </div>
75
+ <div className="dse-badges">
76
+ <span className={`dse-badge${entry.configured ? '' : ' dse-badgeDim'}`}>
77
+ {entry.configured ? t('configured') : t('unconfigured')}
78
+ </span>
79
+ {entry.writable ? null : (
80
+ <span className="dse-badge dse-badgeDim" title={entry.source ?? ''}>
81
+ {t('readonly')}
82
+ </span>
83
+ )}
84
+ {confirming ? (
85
+ <>
86
+ <button type="button" className="dse-btn dse-btnPrimary" onClick={onDelete}>
87
+ {t('delete')}
88
+ </button>
89
+ <button
90
+ type="button"
91
+ className="dse-btn dse-btnGhost"
92
+ onClick={() => setConfirming(false)}
93
+ >
94
+ {t('close')}
95
+ </button>
96
+ </>
97
+ ) : (
98
+ <button
99
+ type="button"
100
+ className="dse-iconBtn dse-delBtn"
101
+ title={t('delete')}
102
+ aria-label={t('delete')}
103
+ onClick={() => setConfirming(true)}
104
+ >
105
+
106
+ </button>
107
+ )}
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ export function SecretsSection(props: SectionProps): ReactElement {
114
+ const { t } = props
115
+ const [data, setData] = useState<SecretList | null>(null)
116
+ const [failed, setFailed] = useState(false)
117
+ const [form, setForm] = useState<FormState>(EMPTY_FORM)
118
+ const [saving, setSaving] = useState(false)
119
+ const [status, setStatus] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null)
120
+
121
+ const load = (): void => {
122
+ fetchSecrets()
123
+ .then((list) => {
124
+ setData(list)
125
+ setFailed(false)
126
+ })
127
+ .catch(() => setFailed(true))
128
+ }
129
+
130
+ // biome-ignore lint/correctness/useExhaustiveDependencies: load 为闭包稳定函数(读端点后 setState),仅需首载一次
131
+ useEffect(() => {
132
+ load()
133
+ }, [])
134
+
135
+ const onSave = (): void => {
136
+ setSaving(true)
137
+ setStatus(null)
138
+ setGlobal(form.name, form.value, form.description)
139
+ .then(() => {
140
+ setForm(EMPTY_FORM)
141
+ setStatus({ kind: 'ok', text: t('configured') })
142
+ load()
143
+ })
144
+ .catch((error: unknown) => {
145
+ setStatus({
146
+ kind: 'err',
147
+ text: errorText(t, error instanceof ApiError ? error : new ApiError('internal')),
148
+ })
149
+ })
150
+ .finally(() => setSaving(false))
151
+ }
152
+
153
+ if (data === null) {
154
+ return (
155
+ <div className="dse-section">
156
+ <p className="dse-empty">{failed ? t('loadFailed') : '…'}</p>
157
+ {failed ? (
158
+ <button type="button" className="dse-btn dse-btnGhost" onClick={load}>
159
+ {t('retry')}
160
+ </button>
161
+ ) : null}
162
+ </div>
163
+ )
164
+ }
165
+
166
+ const canSave = form.name.trim() !== '' && form.value !== '' && !saving
167
+ return (
168
+ <div className="dse-section">
169
+ <header className="dse-head">
170
+ <h2 className="dse-title">{t('title')}</h2>
171
+ <p className="dse-desc">{t('description')}</p>
172
+ </header>
173
+
174
+ <h3 className="dse-groupLabel">{t('globalList')}</h3>
175
+ <div className="dse-rows">
176
+ {data.global.length === 0 ? (
177
+ <p className="dse-empty">{t('emptyGlobal')}</p>
178
+ ) : (
179
+ data.global.map((entry) => (
180
+ <GlobalRow
181
+ key={entry.name}
182
+ t={t}
183
+ entry={entry}
184
+ onDeleted={load}
185
+ onError={(text) => setStatus({ kind: 'err', text })}
186
+ />
187
+ ))
188
+ )}
189
+ </div>
190
+
191
+ <h3 className="dse-groupLabel">{t('addGlobal')}</h3>
192
+ <div className="dse-form">
193
+ <div className="dse-formGrid">
194
+ <div className="dse-field">
195
+ <div className="dse-head">
196
+ <label className="dse-label" htmlFor="dse-name">
197
+ {t('nameLabel')}
198
+ </label>
199
+ </div>
200
+ <input
201
+ id="dse-name"
202
+ className="dse-input"
203
+ value={form.name}
204
+ placeholder="GITHUB_TOKEN"
205
+ onChange={(event) => setForm({ ...form, name: event.target.value })}
206
+ />
207
+ <p className="dse-hint">{t('nameHint')}</p>
208
+ </div>
209
+ <div className="dse-field">
210
+ <div className="dse-head">
211
+ <label className="dse-label" htmlFor="dse-value">
212
+ {t('valueLabel')}
213
+ </label>
214
+ </div>
215
+ <input
216
+ id="dse-value"
217
+ className="dse-input"
218
+ type="password"
219
+ autoComplete="off"
220
+ value={form.value}
221
+ onChange={(event) => setForm({ ...form, value: event.target.value })}
222
+ />
223
+ <p className="dse-hint">{t('valueHint')}</p>
224
+ </div>
225
+ </div>
226
+ <div className="dse-field">
227
+ <div className="dse-head">
228
+ <label className="dse-label" htmlFor="dse-desc">
229
+ {t('descLabel')}
230
+ </label>
231
+ </div>
232
+ <input
233
+ id="dse-desc"
234
+ className="dse-input"
235
+ value={form.description}
236
+ onChange={(event) => setForm({ ...form, description: event.target.value })}
237
+ />
238
+ <p className="dse-hint">{t('descHint')}</p>
239
+ </div>
240
+ <div className="dse-foot">
241
+ {status !== null ? (
242
+ <p className={`dse-status${status.kind === 'err' ? ' dse-statusError' : ''}`}>
243
+ {status.text}
244
+ </p>
245
+ ) : null}
246
+ <button
247
+ type="button"
248
+ className="dse-btn dse-btnPrimary"
249
+ disabled={!canSave}
250
+ onClick={onSave}
251
+ >
252
+ {saving ? t('saving') : t('save')}
253
+ </button>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ )
258
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 样式注入:shared cardCss('dse') 提供官方卡片/字段/按钮基件,
3
+ * extra 部分为本插件特有规则——设置页变量表(桌面网格、≤767px 堆叠)
4
+ * 与会话注入控件(官方 chip 胶囊 + popover,窄屏转底部抽屉)。
5
+ * @module secret-env/client/styles
6
+ */
7
+ import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
8
+
9
+ const EXTRA = `
10
+ /* 设置页布局 */
11
+ .dse-section{padding:0 0 24px}
12
+ .dse-head{flex-direction:column;gap:6px;padding:16px 0 12px;display:flex}
13
+ .dse-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:600;margin:0;line-height:1.4}
14
+ .dse-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;margin:0;line-height:1.6}
15
+ .dse-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:14px 0 8px;margin:0}
16
+ .dse-rows{flex-direction:column;display:flex;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;overflow:hidden}
17
+ .dse-row{align-items:center;gap:12px;padding:10px 14px;display:flex;background:var(--dsw-alias-bg-layer-3)}
18
+ .dse-row+.dse-row{border-top:1px solid var(--dsw-alias-border-l2)}
19
+ .dse-rowMain{flex-direction:column;gap:3px;flex:1;min-width:0;display:flex}
20
+ .dse-env{align-items:center;gap:6px;display:flex;min-width:0}
21
+ .dse-envName{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
22
+ .dse-note{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
23
+ .dse-badges{align-items:center;gap:6px;flex:none;display:flex}
24
+ .dse-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
25
+ .dse-badgeDim{background:0 0;color:var(--dsw-alias-label-tertiary)}
26
+ .dse-iconBtn{appearance:none;border:none;background:0 0;cursor:pointer;color:var(--dsw-alias-label-tertiary);border-radius:6px;padding:4px;display:inline-flex}
27
+ .dse-iconBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
28
+ .dse-iconBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
29
+ .dse-delBtn{color:var(--dsw-alias-label-tertiary)}
30
+ .dse-delBtn:hover{color:var(--dsw-alias-label-error)}
31
+ .dse-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;padding:18px 14px;margin:0;background:var(--dsw-alias-bg-layer-3)}
32
+ .dse-form{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);padding:4px 14px 12px;margin-top:10px}
33
+ .dse-formGrid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
34
+ /* 会话注入控件(composer 工具行胶囊,对齐官方 chip 语言) */
35
+ .dse-wrap{position:relative;align-items:center;display:inline-flex}
36
+ .dse-chip{background:var(--dsw-alias-bg-module-platform);min-width:34px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:none;border-radius:999px;align-items:center;gap:4px;padding:2px 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}
37
+ .dse-chip:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
38
+ .dse-chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
39
+ .dse-chip:disabled{opacity:.6;cursor:default}
40
+ .dse-count{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-base);border-radius:999px;min-width:16px;text-align:center;font-size:11px;line-height:16px;padding:0 4px}
41
+ .dse-pop{position:absolute;bottom:calc(100% + 8px);right:0;z-index:60;width:320px;max-width:calc(100vw - 24px);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:10px 12px 12px}
42
+ .dse-popHead{align-items:center;gap:8px;display:flex;padding:2px 0 8px}
43
+ .dse-popTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;flex:1;margin:0}
44
+ .dse-popHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0 0 8px}
45
+ .dse-popList{flex-direction:column;display:flex;max-height:220px;overflow:auto;margin:0 -4px;padding:0 4px}
46
+ .dse-popList .dse-row{background:0 0;border-radius:8px;padding:8px 8px}
47
+ .dse-popList .dse-row+.dse-row{border-top:1px solid var(--dsw-alias-border-l2)}
48
+ .dse-popForm{border-top:1px solid var(--dsw-alias-border-l2);margin-top:8px;padding-top:4px}
49
+ .dse-foot{justify-content:flex-end;gap:8px;display:flex;padding-top:8px}
50
+ /* 响应式:窄屏表格转堆叠,popover 转底部抽屉 */
51
+ @media (max-width:767px){
52
+ .dse-formGrid{grid-template-columns:1fr}
53
+ .dse-row{flex-wrap:wrap;padding:12px 12px}
54
+ .dse-rowMain{flex-basis:100%}
55
+ .dse-badges{margin-left:auto}
56
+ .dse-pop{position:fixed;left:8px;right:8px;bottom:8px;top:auto;width:auto;max-width:none;max-height:70vh;overflow:auto}
57
+ .dse-chip{min-height:32px}
58
+ }
59
+ @media (pointer:coarse){
60
+ .dse-chip{min-height:32px}
61
+ .dse-iconBtn{min-width:44px;min-height:44px;align-items:center;justify-content:center}
62
+ }
63
+ `
64
+
65
+ /** 幂等注入本插件样式;返回新标签(卸载用),已存在或无 document 返回 null。 */
66
+ export function injectSecretEnvStyle(pluginId: string): HTMLStyleElement | null {
67
+ return injectCardStyle(pluginId, cardCss('dse', EXTRA))
68
+ }
package/src/config.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
3
+ * (用户层,$DSH_HOME/settings.yaml 热生效)共用同一 schemastery schema。
4
+ * `secrets` 只是全局密钥的**元数据索引**(后缀/描述/创建时间),
5
+ * 值永远走 dsh-credentials seam($DSH_HOME/.credentials.yaml),不进本文件。
6
+ * @module secret-env/config
7
+ */
8
+ import z from '@deepseek-ai/schemastery'
9
+
10
+ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
11
+
12
+ /** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验)。 */
13
+ export const SETTINGS_NS = NS_LITERAL
14
+
15
+ /** 一条全局密钥的元数据(值不在此处)。 */
16
+ // biome-ignore lint/suspicious/noExplicitAny: dts 可移植性——嵌套 schema 的精确类型经 TypeT 消费,显式断掉 cosmokit 推断链
17
+ const SecretMetaSchema: any = z.object({
18
+ name: z.string().min(1).description('变量名后缀(如 GITHUB_TOKEN)').default(''),
19
+ description: z.string().description('用途描述(仅人读,不进提示词)').default(''),
20
+ createdAt: z.string().description('创建时间 ISO').default(''),
21
+ })
22
+
23
+ export const Config = z.object({
24
+ secrets: z.array(SecretMetaSchema).description('全局密钥元数据索引(值存凭据库)').default([]),
25
+ })
26
+
27
+ export type SecretEnvConfig = Schemastery.TypeT<typeof Config>
28
+
29
+ /** 一条元数据的运行时形态。 */
30
+ export interface SecretMeta {
31
+ name: string
32
+ description: string
33
+ createdAt: string
34
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 结构化错误(独立模块,避免 service ↔ api 循环依赖)。
3
+ * 端点映射为 HTTP 状态码 + 错误码,UI 按码给文案。
4
+ * @module secret-env/errors
5
+ */
6
+ export class SecretEnvError extends Error {
7
+ readonly code: string
8
+ constructor(code: string, message: string) {
9
+ super(message)
10
+ this.code = code
11
+ }
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * dsh-plus secret-env 插件(node 半)。浏览器半见 src/client/client.ts,
3
+ * 经 ./client 导出 + tsdown 构建,由 dsh-client-modules 扫描进 __DSH_BOOT__。
4
+ *
5
+ * 挂载即得:密钥以 $DSH_SECRET_* 变量名向 agent 暴露(dsh-shell-env 执行期
6
+ * 注入,与内建变量同一通道);值经 dsh-credentials seam 或会话内存持有,
7
+ * 不进消息流、不动 prompt 前缀(缓存率零影响)。不新增工具与提示词——
8
+ * 模型经 bash 原生能力(env | grep ^DSH_)发现变量名。
9
+ * @module @dsh-plus/secret-env
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+
13
+ import { Config, type SecretEnvConfig } from './config.ts'
14
+ import { SecretEnvService } from './service.ts'
15
+
16
+ export const name = 'dsh-plus-secret-env'
17
+ export const inject = ['credentials', 'shellEnv'] as const
18
+
19
+ export { Config }
20
+
21
+ declare module '@deepseek-ai/cordis' {
22
+ interface Context {
23
+ secretEnv: SecretEnvService
24
+ }
25
+ }
26
+
27
+ export function apply(ctx: Context, config?: SecretEnvConfig): void {
28
+ // config 由加载器按行级 Config schema 补默认后传入;服务内部完成端点挂载。
29
+ ctx.plugin(SecretEnvService, config)
30
+ }
31
+
32
+ export { SecretEnvError } from './errors.ts'
33
+ export { SecretEnvService } from './service.ts'
package/src/names.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 变量名语法(纯函数,零依赖;node 半与浏览器半共用):
3
+ * 用户给出后缀(如 `GITHUB_TOKEN`),插件拼出完整受管变量名
4
+ * `DSH_SECRET_GITHUB_TOKEN`——即模型经 `env | grep ^DSH_` 可见、
5
+ * 在 shell 命令中以 `$DSH_SECRET_*` 引用的那个名字。
6
+ * @module secret-env/names
7
+ */
8
+
9
+ /** 受管变量名前缀(属 dsh-shell-env 的 DSH_* 受管命名空间)。 */
10
+ export const ENV_PREFIX = 'DSH_SECRET_'
11
+
12
+ /** 后缀语法:POSIX 环境变量名,大写字母开头,总长(含前缀)不超过 79。 */
13
+ const SUFFIX_PATTERN = /^[A-Z][A-Z0-9_]*$/
14
+ const MAX_SUFFIX_LENGTH = 64
15
+
16
+ /** 名称校验失败码(端点与 UI 共用的结构化错误)。 */
17
+ export type NameError = 'empty' | 'bad-charset' | 'too-long'
18
+
19
+ /** 规范化用户输入:去空白并转大写。 */
20
+ export function normalizeSuffix(raw: string): string {
21
+ return raw.trim().toUpperCase()
22
+ }
23
+
24
+ /** 校验规范化后的后缀;合法返回 null,非法返回失败码。 */
25
+ export function validateSuffix(suffix: string): NameError | null {
26
+ if (suffix.length === 0) return 'empty'
27
+ if (suffix.length > MAX_SUFFIX_LENGTH) return 'too-long'
28
+ if (!SUFFIX_PATTERN.test(suffix)) return 'bad-charset'
29
+ return null
30
+ }
31
+
32
+ /** 由后缀拼出完整受管变量名(调用方保证后缀已通过校验)。 */
33
+ export function envNameOf(suffix: string): string {
34
+ return `${ENV_PREFIX}${suffix}`
35
+ }
36
+
37
+ /** 判断一个完整变量名是否属本插件管理(用于凭据热更事件过滤)。 */
38
+ export function isManagedEnvName(envName: string): boolean {
39
+ return envName.startsWith(ENV_PREFIX) && envName.length > ENV_PREFIX.length
40
+ }
41
+
42
+ /** 由完整变量名反解后缀;非本插件名返回 undefined。 */
43
+ export function suffixOf(envName: string): string | undefined {
44
+ return isManagedEnvName(envName) ? envName.slice(ENV_PREFIX.length) : undefined
45
+ }
package/src/ns.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
3
+ * 服务端以字面量注册元数据索引,浏览器半作为 settings.section 页标识。
4
+ * @module secret-env/ns
5
+ */
6
+ export const SETTINGS_NS = 'dsh-plus-secret-env'