@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/client.js +1180 -633
- package/lib/index.d.ts +40 -8
- package/lib/index.js +211 -57
- package/package.json +6 -4
- package/src/api.ts +24 -1
- package/src/client/api.ts +18 -0
- package/src/client/client.ts +28 -21
- package/src/client/command.ts +62 -0
- package/src/client/common.ts +14 -1
- package/src/client/i18n.ts +66 -37
- package/src/client/menu-core.ts +2 -2
- package/src/client/menu.tsx +24 -8
- package/src/client/panel-bus.ts +23 -0
- package/src/client/panel-host.tsx +63 -0
- package/src/client/panel.tsx +385 -0
- package/src/client/section.tsx +194 -38
- package/src/client/styles.ts +10 -13
- package/src/config.ts +4 -0
- package/src/contributors.ts +115 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +101 -0
- package/src/names.ts +3 -3
- package/src/service.ts +107 -94
- package/src/client/composer.tsx +0 -260
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/
|
|
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
|
-
/**
|
|
36
|
-
private readonly
|
|
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
|
-
/**
|
|
74
|
+
/** 索引差量调和:新名建镜像,移出名撤镜像并回落继承登记(若宿主仍有同名)。 */
|
|
52
75
|
private reconcile;
|
|
53
76
|
/** 重新 resolve 一个全局名并同步镜像与 contributor(外部热编辑亦走此路)。 */
|
|
54
77
|
private refreshMirror;
|
|
55
|
-
/**
|
|
78
|
+
/** 受管登记簿同步:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
|
|
56
79
|
private syncContributor;
|
|
57
|
-
|
|
58
|
-
|
|
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({
|
|
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
|
-
* `
|
|
47
|
-
* 在 shell 命令中以 `$
|
|
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 = "
|
|
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("
|
|
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(
|
|
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/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
/**
|
|
215
|
-
|
|
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 ?? {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "dsh-plus service+ui plugin: 密钥环境变量——以 $
|
|
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.
|
|
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 /
|
|
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
|
+
}
|