@dsh-plus/secret-env 0.1.1 → 0.1.2

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
@@ -3,12 +3,14 @@ import { Context, Service } from "@deepseek-ai/cordis";
3
3
  //#region src/config.d.ts
4
4
  declare const Config: z<Schemastery.ObjectS<{
5
5
  secrets: z<unknown[], any[]>;
6
+ masked: z<string[], string[]>;
6
7
  }>, Schemastery.ObjectT<{
7
8
  secrets: z<unknown[], any[]>;
9
+ masked: z<string[], string[]>;
8
10
  }>>;
9
11
  type SecretEnvConfig = Schemastery.TypeT<typeof Config>;
10
12
  //#endregion
11
- //#region src/service.d.ts
13
+ //#region src/inventory.d.ts
12
14
  /** 列表端点的全局条目(describe 视图,绝无值)。 */
13
15
  interface GlobalEntry {
14
16
  name: string;
@@ -17,6 +19,8 @@ interface GlobalEntry {
17
19
  configured: boolean;
18
20
  source?: string;
19
21
  writable: boolean;
22
+ /** 会话视图:该变量在本会话被屏蔽(无 sessionId 的调用恒为 false)。 */
23
+ masked: boolean;
20
24
  }
21
25
  /** 列表端点的会话条目。 */
22
26
  interface SessionEntry {
@@ -26,14 +30,29 @@ interface SessionEntry {
26
30
  once: boolean;
27
31
  createdAt: string;
28
32
  }
33
+ /**
34
+ * 列表端点的继承条目:宿主进程环境里已存在的 DSH_VAR_* 变量
35
+ * (非本插件索引管理;默认纳入注入)。masked = 生效屏蔽态,
36
+ * globallyMasked = 是否由全局屏蔽导致(会话视图里据此禁用会话级开关)。
37
+ */
38
+ interface InheritedEntry {
39
+ name: string;
40
+ envName: string;
41
+ masked: boolean;
42
+ globallyMasked: boolean;
43
+ }
44
+ //#endregion
45
+ //#region src/service.d.ts
29
46
  declare class SecretEnvService extends Service {
30
47
  static [Context.inject]: string[];
31
48
  /** 全局值镜像(seam resolve 为异步,contributor 同步读这里)。 */
32
49
  private readonly globalMirror;
33
50
  /** 会话桶:sessionId → 后缀 → 条目。 */
34
51
  private readonly buckets;
35
- /** 变量名contributor 注销器(shell-env 要求一键一主)。 */
36
- private readonly contributors;
52
+ /** 会话级屏蔽名单:sessionId后缀集合(纯内存,随会话终结清除)。 */
53
+ private readonly sessionMasks;
54
+ /** contributor 登记簿(受管/继承两条线,一键一主)。 */
55
+ private readonly book;
37
56
  /** 行级 config(settings 缺席时的索引来源)。 */
38
57
  private readonly base;
39
58
  /** installSection 给的实时 getter(scope.get 的活视图,读即最新,无时序竞态)。 */
@@ -46,16 +65,22 @@ declare class SecretEnvService extends Service {
46
65
  constructor(ctx: Context, config: SecretEnvConfig | undefined);
47
66
  /** 当前生效索引(settings 活视图优先,缺席回落行级 config)。 */
48
67
  private currentMeta;
68
+ /** 当前全局屏蔽名单(与索引同源,活读取)。 */
69
+ private currentMasked;
70
+ /** 继承变量后缀:宿主进程环境里已存在的 DSH_VAR_* 且不在本插件索引内。 */
71
+ private inheritedSuffixes;
49
72
  /** 索引成员判定(索引是全局名管理面的唯一事实源)。 */
50
73
  private isIndexed;
51
- /** 索引差量调和:新名建镜像,移出名撤镜像并回收其独占的 contributor。 */
74
+ /** 索引差量调和:新名建镜像,移出名撤镜像并回落继承登记(若宿主仍有同名)。 */
52
75
  private reconcile;
53
76
  /** 重新 resolve 一个全局名并同步镜像与 contributor(外部热编辑亦走此路)。 */
54
77
  private refreshMirror;
55
- /** contributor 登记簿:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
78
+ /** 受管登记簿同步:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
56
79
  private syncContributor;
57
- private registerContributor;
58
- /** 注入解析:会话值优先,未命中回落全局镜像;once 命中即焚(注销推迟到微任务,避免 collect 迭代中改注册表)。 */
80
+ /**
81
+ * 受管注入解析:会话屏蔽名单优先(任何来源都拦),会话值次之,全局屏蔽再次,
82
+ * 未命中回落全局镜像;once 命中即焚(注销推迟到微任务,避免 collect 迭代中改登记簿)。
83
+ */
59
84
  private resolveFor;
60
85
  /** 会话终结:整桶清除并回收其独占的 contributor。 */
61
86
  private dropBucket;
@@ -64,10 +89,17 @@ declare class SecretEnvService extends Service {
64
89
  list(sessionId?: string): Promise<{
65
90
  global: GlobalEntry[];
66
91
  session: SessionEntry[];
92
+ inherited: InheritedEntry[];
67
93
  }>;
94
+ /**
95
+ * 屏蔽/恢复一个变量:sessionId 缺席时为全局屏蔽(持久于 settings,
96
+ * 主要面向继承变量);否则为会话级屏蔽(纯内存,会话终结即清)。
97
+ * 两级屏蔽都在 contributor resolve 时现判,下一条命令即生效。
98
+ */
99
+ setMask(suffix: string, masked: boolean, sessionId?: string): Promise<void>;
68
100
  /** 写入全局值(seam 拒绝继承环境遮蔽与空值,此处先行给出结构化错误)。 */
69
101
  setGlobal(suffix: string, value: string, description: string): Promise<void>;
70
- /** 删除全局值与元数据。 */
102
+ /** 删除全局值与元数据;宿主环境仍有同名变量时回落为继承变量继续纳入。 */
71
103
  unsetGlobal(suffix: string): Promise<void>;
72
104
  /** 写入会话级值(纯内存;host 重启或会话终结即失)。 */
73
105
  setSession(sessionId: string, suffix: string, value: string, description: string, once: boolean): void;
package/lib/index.js CHANGED
@@ -23,7 +23,10 @@ const SecretMetaSchema = z.object({
23
23
  description: z.string().description("用途描述(仅人读,不进提示词)").default(""),
24
24
  createdAt: z.string().description("创建时间 ISO").default("")
25
25
  });
26
- const Config = z.object({ secrets: z.array(SecretMetaSchema).description("全局密钥元数据索引(值存凭据库)").default([]) });
26
+ const Config = z.object({
27
+ secrets: z.array(SecretMetaSchema).description("全局密钥元数据索引(值存凭据库)").default([]),
28
+ masked: z.array(z.string()).description("全局屏蔽的变量名后缀(继承变量等;注入时跳过)").default([])
29
+ });
27
30
  //#endregion
28
31
  //#region src/errors.ts
29
32
  /**
@@ -43,12 +46,12 @@ var SecretEnvError = class extends Error {
43
46
  /**
44
47
  * 变量名语法(纯函数,零依赖;node 半与浏览器半共用):
45
48
  * 用户给出后缀(如 `GITHUB_TOKEN`),插件拼出完整受管变量名
46
- * `DSH_SECRET_GITHUB_TOKEN`——即模型经 `env | grep ^DSH_` 可见、
47
- * 在 shell 命令中以 `$DSH_SECRET_*` 引用的那个名字。
49
+ * `DSH_VAR_GITHUB_TOKEN`——即模型经 `env | grep ^DSH_` 可见、
50
+ * 在 shell 命令中以 `$DSH_VAR_*` 引用的那个名字。
48
51
  * @module secret-env/names
49
52
  */
50
53
  /** 受管变量名前缀(属 dsh-shell-env 的 DSH_* 受管命名空间)。 */
51
- const ENV_PREFIX = "DSH_SECRET_";
54
+ const ENV_PREFIX = "DSH_VAR_";
52
55
  /** 后缀语法:POSIX 环境变量名,大写字母开头,总长(含前缀)不超过 79。 */
53
56
  const SUFFIX_PATTERN = /^[A-Z][A-Z0-9_]*$/;
54
57
  const MAX_SUFFIX_LENGTH = 64;
@@ -69,11 +72,11 @@ function envNameOf(suffix) {
69
72
  }
70
73
  /** 判断一个完整变量名是否属本插件管理(用于凭据热更事件过滤)。 */
71
74
  function isManagedEnvName(envName) {
72
- return envName.startsWith("DSH_SECRET_") && envName.length > 11;
75
+ return envName.startsWith("DSH_VAR_") && envName.length > 8;
73
76
  }
74
77
  /** 由完整变量名反解后缀;非本插件名返回 undefined。 */
75
78
  function suffixOf(envName) {
76
- return isManagedEnvName(envName) ? envName.slice(11) : void 0;
79
+ return isManagedEnvName(envName) ? envName.slice(8) : void 0;
77
80
  }
78
81
  //#endregion
79
82
  //#region src/api.ts
@@ -181,20 +184,93 @@ function registerSecretEnvApi(ctx, service) {
181
184
  }, res);
182
185
  }
183
186
  });
187
+ ctx.webServer.register({
188
+ kind: "prefix",
189
+ path: `${ROUTE}/mask/set`,
190
+ handler: async (req, res) => {
191
+ guard("mask/set", async () => {
192
+ if (req.method !== "POST") throw new SecretEnvError("method", "POST only");
193
+ const body = JSON.parse(await readBody(req));
194
+ const sessionId = readString(body.sessionId);
195
+ await service.setMask(readSuffix(body.name), body.masked === true, sessionId === "" ? void 0 : sessionId);
196
+ sendJson(res, 200, { ok: true });
197
+ }, res);
198
+ }
199
+ });
184
200
  }
185
201
  //#endregion
186
- //#region src/service.ts
187
- /**
188
- * secret-env 服务主体(host 半):
189
- * - 全局臂:值经 dsh-credentials seam 持久($DSH_HOME/.credentials.yaml),
190
- * 服务内存中维护同步镜像(contributor.resolve 是同步签名,无法现查异步 seam);
191
- * - 会话臂:Map<SessionId, Map<后缀, 值>> 纯内存,session/disposed 清除;
192
- * - 注入臂:每个变量名一个 dsh-shell-env contributor,每次 shell 执行由
193
- * 注册表 collect 现取——写入/删除下一条命令即生效,不进消息流、不动前缀。
194
- *
195
- * 红线:值只流经「端点 → 本服务 → dshEnv」,任何日志/事件/返回值不带值。
196
- * @module secret-env/service
197
- */
202
+ //#region src/contributors.ts
203
+ var ContributorBook = class {
204
+ managed = /* @__PURE__ */ new Map();
205
+ inherited = /* @__PURE__ */ new Map();
206
+ deps;
207
+ constructor(deps) {
208
+ this.deps = deps;
209
+ }
210
+ /** 受管登记:有任一来源值则注册,否则注销。 */
211
+ syncManaged(suffix, hasValue) {
212
+ const envName = envNameOf(suffix);
213
+ const registered = this.managed.has(envName);
214
+ if (hasValue && !registered) {
215
+ this.registerManaged(suffix, envName);
216
+ return;
217
+ }
218
+ if (!hasValue && registered) {
219
+ this.managed.get(envName)?.();
220
+ this.managed.delete(envName);
221
+ }
222
+ }
223
+ /** 继承登记:按 shouldForwardInherited 现判注册/注销。 */
224
+ syncInherited(suffix) {
225
+ const envName = envNameOf(suffix);
226
+ const registered = this.inherited.get(envName);
227
+ if (this.deps.shouldForwardInherited(suffix) && registered === void 0) {
228
+ const dispose = this.deps.shellEnv.register({
229
+ name: `secret-env-inherited:${suffix}`,
230
+ variables: { [envName]: { description: "inherited variable" } },
231
+ resolve: (execution) => {
232
+ const value = this.deps.resolveInherited(suffix, execution);
233
+ return value === void 0 || value === "" ? {} : { [envName]: value };
234
+ }
235
+ });
236
+ this.inherited.set(envName, dispose);
237
+ return;
238
+ }
239
+ if (!this.deps.shouldForwardInherited(suffix) && registered !== void 0) {
240
+ registered();
241
+ this.inherited.delete(envName);
242
+ }
243
+ }
244
+ registerManaged(suffix, envName) {
245
+ const inherited = this.inherited.get(envName);
246
+ if (inherited !== void 0) {
247
+ inherited();
248
+ this.inherited.delete(envName);
249
+ }
250
+ try {
251
+ const dispose = this.deps.shellEnv.register({
252
+ name: `secret-env:${suffix}`,
253
+ variables: { [envName]: { description: this.deps.describeManaged(suffix) } },
254
+ resolve: (execution) => {
255
+ const value = this.deps.resolveManaged(suffix, execution);
256
+ return value === void 0 ? {} : { [envName]: value };
257
+ }
258
+ });
259
+ this.managed.set(envName, dispose);
260
+ } catch (error) {
261
+ throw new SecretEnvError("conflict", `variable ${envName} conflicts with an existing contributor: ${error instanceof Error ? error.message : String(error)}`);
262
+ }
263
+ }
264
+ dispose() {
265
+ for (const dispose of this.managed.values()) dispose();
266
+ this.managed.clear();
267
+ for (const dispose of this.inherited.values()) dispose();
268
+ this.inherited.clear();
269
+ }
270
+ };
271
+ //#endregion
272
+ //#region src/inventory.ts
273
+ /** 从索引数据解析全局密钥元数据列表(宽容过滤非法项)。 */
198
274
  function asMeta(value) {
199
275
  if (value === void 0 || value === null || typeof value !== "object") return [];
200
276
  const list = value.secrets;
@@ -205,14 +281,61 @@ function asMeta(value) {
205
281
  createdAt: typeof item.createdAt === "string" ? item.createdAt : ""
206
282
  })).filter((item) => item.name.length > 0);
207
283
  }
284
+ /** 从同一索引数据解析全局屏蔽名单(与 secrets 共一个 settings 命名空间)。 */
285
+ function asMasked(value) {
286
+ if (value === void 0 || value === null || typeof value !== "object") return [];
287
+ const list = value.masked;
288
+ if (!Array.isArray(list)) return [];
289
+ return list.filter((item) => typeof item === "string" && item.length > 0);
290
+ }
291
+ /** 继承变量后缀:给定进程环境里的 DSH_VAR_* 且不在索引内(按名排序)。 */
292
+ function inheritedSuffixesOf(env, indexed) {
293
+ const found = [];
294
+ for (const key of Object.keys(env)) {
295
+ const suffix = suffixOf(key);
296
+ if (suffix !== void 0 && !indexed.has(suffix)) found.push(suffix);
297
+ }
298
+ return found.sort();
299
+ }
300
+ /** 拼装继承条目列表(list 端点的 inherited 段)。 */
301
+ function inheritedEntriesOf(suffixes, globalMasked, sessionMask) {
302
+ return suffixes.map((name) => {
303
+ const globally = globalMasked.has(name);
304
+ return {
305
+ name,
306
+ envName: envNameOf(name),
307
+ masked: globally || sessionMask?.has(name) === true,
308
+ globallyMasked: globally
309
+ };
310
+ });
311
+ }
312
+ //#endregion
313
+ //#region src/service.ts
314
+ /**
315
+ * secret-env 服务主体(host 半):
316
+ * - 全局臂:值经 dsh-credentials seam 持久($DSH_HOME/.credentials.yaml),
317
+ * 服务内存中维护同步镜像(contributor.resolve 是同步签名,无法现查异步 seam);
318
+ * - 会话臂:Map<SessionId, Map<后缀, 值>> 纯内存,session/disposed 清除;
319
+ * - 屏蔽臂:全局屏蔽名单持久于 settings(继承变量主要开关),
320
+ * 会话级屏蔽纯内存;两级都在 contributor resolve 时现判;
321
+ * - 注入臂:每个变量名一个 dsh-shell-env contributor(ContributorBook),
322
+ * 每次 shell 执行由注册表 collect 现取——写入/删除/屏蔽下一条命令即生效,
323
+ * 不进消息流、不动前缀。继承变量(宿主环境 DSH_VAR_*)平台默认剥离,
324
+ * 本服务按「默认纳入」逐一转发,可被屏蔽名单拦截。
325
+ *
326
+ * 红线:值只流经「端点 → 本服务 → dshEnv」,任何日志/事件/返回值不带值。
327
+ * @module secret-env/service
328
+ */
208
329
  var SecretEnvService = class extends Service {
209
330
  static [Context.inject] = ["credentials", "shellEnv"];
210
331
  /** 全局值镜像(seam resolve 为异步,contributor 同步读这里)。 */
211
332
  globalMirror = /* @__PURE__ */ new Map();
212
333
  /** 会话桶:sessionId → 后缀 → 条目。 */
213
334
  buckets = /* @__PURE__ */ new Map();
214
- /** 变量名contributor 注销器(shell-env 要求一键一主)。 */
215
- contributors = /* @__PURE__ */ new Map();
335
+ /** 会话级屏蔽名单:sessionId后缀集合(纯内存,随会话终结清除)。 */
336
+ sessionMasks = /* @__PURE__ */ new Map();
337
+ /** contributor 登记簿(受管/继承两条线,一键一主)。 */
338
+ book;
216
339
  /** 行级 config(settings 缺席时的索引来源)。 */
217
340
  base;
218
341
  /** installSection 给的实时 getter(scope.get 的活视图,读即最新,无时序竞态)。 */
@@ -224,7 +347,25 @@ var SecretEnvService = class extends Service {
224
347
  ready;
225
348
  constructor(ctx, config) {
226
349
  super(ctx, "secretEnv");
227
- this.base = config ?? { secrets: [] };
350
+ this.base = config ?? {
351
+ secrets: [],
352
+ masked: []
353
+ };
354
+ this.book = new ContributorBook({
355
+ shellEnv: ctx.shellEnv,
356
+ describeManaged: (suffix) => {
357
+ const found = this.currentMeta().find((item) => item.name === suffix)?.description;
358
+ return found !== void 0 && found.trim() !== "" ? found : "secret variable";
359
+ },
360
+ resolveManaged: (suffix, execution) => this.resolveFor(suffix, execution),
361
+ shouldForwardInherited: (suffix) => process.env[envNameOf(suffix)] !== void 0 && !this.isIndexed(suffix),
362
+ resolveInherited: (suffix, execution) => {
363
+ if (this.currentMasked().has(suffix)) return void 0;
364
+ const agentId = execution.agent?.id;
365
+ if (agentId !== void 0 && this.sessionMasks.get(String(agentId))?.has(suffix) === true) return;
366
+ return process.env[envNameOf(suffix)];
367
+ }
368
+ });
228
369
  ctx.inject(["settings"], (settingsCtx) => {
229
370
  const settings = settingsCtx.settings;
230
371
  settings.installSection(ctx, SETTINGS_NS, Config, this.base, {
@@ -239,6 +380,7 @@ var SecretEnvService = class extends Service {
239
380
  this.reconcile();
240
381
  });
241
382
  ctx.root.on("session/disposed", (session) => {
383
+ this.sessionMasks.delete(session.id);
242
384
  this.dropBucket(session.id);
243
385
  });
244
386
  ctx.on("credentials/reference-updated", (ref) => {
@@ -248,17 +390,26 @@ var SecretEnvService = class extends Service {
248
390
  ctx.inject(["webServer"], (webCtx) => {
249
391
  registerSecretEnvApi(webCtx, this);
250
392
  });
393
+ for (const suffix of this.inheritedSuffixes()) this.book.syncInherited(suffix);
251
394
  this.ready = this.reconcile();
252
395
  }
253
396
  /** 当前生效索引(settings 活视图优先,缺席回落行级 config)。 */
254
397
  currentMeta() {
255
398
  return asMeta(this.readIndex !== void 0 ? this.readIndex() : this.base);
256
399
  }
400
+ /** 当前全局屏蔽名单(与索引同源,活读取)。 */
401
+ currentMasked() {
402
+ return new Set(asMasked(this.readIndex !== void 0 ? this.readIndex() : this.base));
403
+ }
404
+ /** 继承变量后缀:宿主进程环境里已存在的 DSH_VAR_* 且不在本插件索引内。 */
405
+ inheritedSuffixes() {
406
+ return inheritedSuffixesOf(process.env, new Set(this.currentMeta().map((item) => item.name)));
407
+ }
257
408
  /** 索引成员判定(索引是全局名管理面的唯一事实源)。 */
258
409
  isIndexed(suffix) {
259
410
  return this.currentMeta().some((item) => item.name === suffix);
260
411
  }
261
- /** 索引差量调和:新名建镜像,移出名撤镜像并回收其独占的 contributor。 */
412
+ /** 索引差量调和:新名建镜像,移出名撤镜像并回落继承登记(若宿主仍有同名)。 */
262
413
  async reconcile() {
263
414
  const next = this.currentMeta();
264
415
  const removed = this.lastMeta.filter((item) => !next.some((n) => n.name === item.name));
@@ -267,6 +418,7 @@ var SecretEnvService = class extends Service {
267
418
  for (const item of removed) {
268
419
  this.globalMirror.delete(item.name);
269
420
  this.syncContributor(item.name);
421
+ this.book.syncInherited(item.name);
270
422
  }
271
423
  for (const item of added) await this.refreshMirror(item.name);
272
424
  }
@@ -277,46 +429,25 @@ var SecretEnvService = class extends Service {
277
429
  else this.globalMirror.set(suffix, resolved.value);
278
430
  this.syncContributor(suffix);
279
431
  }
280
- /** contributor 登记簿:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
432
+ /** 受管登记簿同步:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
281
433
  syncContributor(suffix) {
282
- const envName = envNameOf(suffix);
283
434
  const hasGlobal = this.globalMirror.has(suffix);
284
435
  let hasSession = false;
285
436
  for (const bucket of this.buckets.values()) if (bucket.has(suffix)) {
286
437
  hasSession = true;
287
438
  break;
288
439
  }
289
- const registered = this.contributors.has(envName);
290
- if ((hasGlobal || hasSession) && !registered) {
291
- this.registerContributor(suffix, envName);
292
- return;
293
- }
294
- if (!hasGlobal && !hasSession && registered) {
295
- this.contributors.get(envName)?.();
296
- this.contributors.delete(envName);
297
- }
440
+ this.book.syncManaged(suffix, hasGlobal || hasSession);
298
441
  }
299
- registerContributor(suffix, envName) {
300
- const found = this.currentMeta().find((item) => item.name === suffix)?.description;
301
- const description = found !== void 0 && found.trim() !== "" ? found : "secret variable";
302
- try {
303
- const dispose = this.ctx.shellEnv.register({
304
- name: `secret-env:${suffix}`,
305
- variables: { [envName]: { description } },
306
- resolve: (execution) => {
307
- const value = this.resolveFor(suffix, execution);
308
- return value === void 0 ? {} : { [envName]: value };
309
- }
310
- });
311
- this.contributors.set(envName, dispose);
312
- } catch (error) {
313
- throw new SecretEnvError("conflict", `variable ${envName} conflicts with an existing contributor: ${error instanceof Error ? error.message : String(error)}`);
314
- }
315
- }
316
- /** 注入解析:会话值优先,未命中回落全局镜像;once 命中即焚(注销推迟到微任务,避免 collect 迭代中改注册表)。 */
442
+ /**
443
+ * 受管注入解析:会话屏蔽名单优先(任何来源都拦),会话值次之,全局屏蔽再次,
444
+ * 未命中回落全局镜像;once 命中即焚(注销推迟到微任务,避免 collect 迭代中改登记簿)。
445
+ */
317
446
  resolveFor(suffix, execution) {
318
447
  const sessionId = execution.agent?.id;
319
- const bucket = sessionId === void 0 ? void 0 : this.buckets.get(String(sessionId));
448
+ const sid = sessionId === void 0 ? void 0 : String(sessionId);
449
+ if (sid !== void 0 && this.sessionMasks.get(sid)?.has(suffix) === true) return void 0;
450
+ const bucket = sid === void 0 ? void 0 : this.buckets.get(sid);
320
451
  const entry = bucket?.get(suffix);
321
452
  if (entry !== void 0) {
322
453
  if (entry.once && bucket !== void 0) {
@@ -325,6 +456,7 @@ var SecretEnvService = class extends Service {
325
456
  }
326
457
  return entry.value;
327
458
  }
459
+ if (this.currentMasked().has(suffix)) return void 0;
328
460
  return this.globalMirror.get(suffix);
329
461
  }
330
462
  /** 会话终结:整桶清除并回收其独占的 contributor。 */
@@ -340,6 +472,7 @@ var SecretEnvService = class extends Service {
340
472
  }
341
473
  /** 列表(值永不出现;global 条目来自 credentials.describe 的安全视图)。 */
342
474
  async list(sessionId) {
475
+ const sessionMask = sessionId === void 0 ? void 0 : this.sessionMasks.get(sessionId);
343
476
  return {
344
477
  global: await Promise.all(this.currentMeta().map(async (item) => {
345
478
  const info = await this.ctx.credentials.describe(credentialRef(envNameOf(item.name)));
@@ -349,7 +482,8 @@ var SecretEnvService = class extends Service {
349
482
  description: item.description,
350
483
  configured: info.configured,
351
484
  source: info.source,
352
- writable: info.writable
485
+ writable: info.writable,
486
+ masked: sessionMask?.has(item.name) === true
353
487
  };
354
488
  })),
355
489
  session: [...(sessionId === void 0 ? void 0 : this.buckets.get(sessionId))?.entries() ?? []].map(([name, entry]) => ({
@@ -358,9 +492,29 @@ var SecretEnvService = class extends Service {
358
492
  description: entry.description,
359
493
  once: entry.once,
360
494
  createdAt: entry.createdAt
361
- }))
495
+ })),
496
+ inherited: inheritedEntriesOf(this.inheritedSuffixes(), this.currentMasked(), sessionMask)
362
497
  };
363
498
  }
499
+ /**
500
+ * 屏蔽/恢复一个变量:sessionId 缺席时为全局屏蔽(持久于 settings,
501
+ * 主要面向继承变量);否则为会话级屏蔽(纯内存,会话终结即清)。
502
+ * 两级屏蔽都在 contributor resolve 时现判,下一条命令即生效。
503
+ */
504
+ async setMask(suffix, masked, sessionId) {
505
+ if (sessionId !== void 0) {
506
+ if (sessionId.length === 0) throw new SecretEnvError("no-session", "sessionId required");
507
+ const set = this.sessionMasks.get(sessionId) ?? /* @__PURE__ */ new Set();
508
+ if (masked) set.add(suffix);
509
+ else set.delete(suffix);
510
+ if (set.size === 0) this.sessionMasks.delete(sessionId);
511
+ else this.sessionMasks.set(sessionId, set);
512
+ return;
513
+ }
514
+ const next = asMasked(this.readIndex !== void 0 ? this.readIndex() : this.base).filter((item) => item !== suffix);
515
+ if (masked) next.push(suffix);
516
+ if (this.settingsRef !== void 0) await this.settingsRef.update(SETTINGS_NS, { masked: next });
517
+ }
364
518
  /** 写入全局值(seam 拒绝继承环境遮蔽与空值,此处先行给出结构化错误)。 */
365
519
  async setGlobal(suffix, value, description) {
366
520
  if (value.length === 0) throw new SecretEnvError("empty-value", "value must not be empty");
@@ -382,7 +536,7 @@ var SecretEnvService = class extends Service {
382
536
  this.lastMeta = next;
383
537
  await this.refreshMirror(suffix);
384
538
  }
385
- /** 删除全局值与元数据。 */
539
+ /** 删除全局值与元数据;宿主环境仍有同名变量时回落为继承变量继续纳入。 */
386
540
  async unsetGlobal(suffix) {
387
541
  await this.ctx.credentials.unset(credentialRef(envNameOf(suffix)));
388
542
  const next = this.currentMeta().filter((item) => item.name !== suffix);
@@ -390,6 +544,7 @@ var SecretEnvService = class extends Service {
390
544
  this.lastMeta = next;
391
545
  this.globalMirror.delete(suffix);
392
546
  this.syncContributor(suffix);
547
+ this.book.syncInherited(suffix);
393
548
  }
394
549
  /** 写入会话级值(纯内存;host 重启或会话终结即失)。 */
395
550
  setSession(sessionId, suffix, value, description, once) {
@@ -413,8 +568,7 @@ var SecretEnvService = class extends Service {
413
568
  this.syncContributor(suffix);
414
569
  }
415
570
  dispose() {
416
- for (const disposeContributor of this.contributors.values()) disposeContributor();
417
- this.contributors.clear();
571
+ this.book.dispose();
418
572
  }
419
573
  };
420
574
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-plus/secret-env",
3
- "version": "0.1.1",
4
- "description": "dsh-plus service+ui plugin: 密钥环境变量——以 $DSH_SECRET_* 变量名向 agent 暴露密钥(全局持久/会话级/一次性),执行期经 dsh-shell-env 原生注入,值不进消息流,不影响缓存率",
3
+ "version": "0.1.2",
4
+ "description": "dsh-plus service+ui plugin: 密钥环境变量——以 $DSH_VAR_* 变量名向 agent 暴露密钥(全局持久/会话级/一次性),执行期经 dsh-shell-env 原生注入,值不进消息流,不影响缓存率",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  "src"
20
20
  ],
21
21
  "dependencies": {
22
- "@dsh-plus/shared": "0.1.7"
22
+ "@dsh-plus/shared": "0.1.8"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@deepseek-ai/cordis": "^4.0.1",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@deepseek-ai/cordis": "4.0.2",
34
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
34
35
  "@deepseek-ai/dsh-credentials": "0.1.2-alpha.2",
35
36
  "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
36
37
  "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
@@ -52,7 +53,8 @@
52
53
  "@deepseek-ai/dsh-client-locale",
53
54
  "@deepseek-ai/dsh-api-remotes",
54
55
  "@deepseek-ai/dsh-api-session-controller",
55
- "@deepseek-ai/dsh-client-ui-conversation"
56
+ "@deepseek-ai/dsh-client-ui-conversation",
57
+ "@deepseek-ai/dsh-client-ui-commands"
56
58
  ],
57
59
  "platform": "web"
58
60
  }
package/src/api.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * secret-env HTTP 端点:GET list / POST global.set / global.unset / session.set / session.unset。
2
+ * secret-env HTTP 端点:GET list / POST global.set / global.unset /
3
+ * session.set / session.unset / mask.set。
3
4
  * 与 dsh web 同源(webServer 默认 loopback / 反代信任域),无独立鉴权
4
5
  * (与 usage-panel/notify-email 自定义端点同一暴露面约定)。
5
6
  * 值只在「浏览器 → 本端点 → 服务」链路出现,响应永不回显值。
@@ -151,4 +152,26 @@ export function registerSecretEnvApi(ctx: Context, service: SecretEnvService): v
151
152
  )
152
153
  },
153
154
  })
155
+
156
+ ctx.webServer.register({
157
+ kind: 'prefix',
158
+ path: `${ROUTE}/mask/set`,
159
+ handler: async (req, res) => {
160
+ guard(
161
+ 'mask/set',
162
+ async () => {
163
+ if (req.method !== 'POST') throw new SecretEnvError('method', 'POST only')
164
+ const body = JSON.parse(await readBody(req)) as Record<string, unknown>
165
+ const sessionId = readString(body.sessionId)
166
+ await service.setMask(
167
+ readSuffix(body.name),
168
+ body.masked === true,
169
+ sessionId === '' ? undefined : sessionId,
170
+ )
171
+ sendJson(res, 200, { ok: true })
172
+ },
173
+ res,
174
+ )
175
+ },
176
+ })
154
177
  }
package/src/client/api.ts CHANGED
@@ -11,6 +11,8 @@ export interface GlobalWireEntry {
11
11
  configured: boolean
12
12
  source?: string
13
13
  writable: boolean
14
+ /** 会话视图:本会话已屏蔽(无 sessionId 的请求恒 false)。 */
15
+ masked: boolean
14
16
  }
15
17
 
16
18
  export interface SessionWireEntry {
@@ -21,9 +23,20 @@ export interface SessionWireEntry {
21
23
  createdAt: string
22
24
  }
23
25
 
26
+ /** 继承变量:宿主进程环境中已存在的 DSH_VAR_*(非本插件管理)。 */
27
+ export interface InheritedWireEntry {
28
+ name: string
29
+ envName: string
30
+ /** 生效屏蔽态(全局或本会话)。 */
31
+ masked: boolean
32
+ /** 是否由全局屏蔽导致(会话视图据此禁用会话级开关)。 */
33
+ globallyMasked: boolean
34
+ }
35
+
24
36
  export interface SecretList {
25
37
  global: GlobalWireEntry[]
26
38
  session: SessionWireEntry[]
39
+ inherited: InheritedWireEntry[]
27
40
  }
28
41
 
29
42
  interface WriteResult {
@@ -74,3 +87,8 @@ export function setSession(
74
87
  export function unsetSession(sessionId: string, name: string): Promise<void> {
75
88
  return write('session/unset', { sessionId, name })
76
89
  }
90
+
91
+ /** 屏蔽/恢复:sessionId 缺席为全局屏蔽(设置页),否则为会话级屏蔽。 */
92
+ export function setMask(name: string, masked: boolean, sessionId?: string): Promise<void> {
93
+ return write('mask/set', { name, masked, sessionId })
94
+ }