@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.
package/lib/index.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ //#region src/config.d.ts
4
+ declare const Config: z<Schemastery.ObjectS<{
5
+ secrets: z<unknown[], any[]>;
6
+ }>, Schemastery.ObjectT<{
7
+ secrets: z<unknown[], any[]>;
8
+ }>>;
9
+ type SecretEnvConfig = Schemastery.TypeT<typeof Config>;
10
+ //#endregion
11
+ //#region src/service.d.ts
12
+ /** 列表端点的全局条目(describe 视图,绝无值)。 */
13
+ interface GlobalEntry {
14
+ name: string;
15
+ envName: string;
16
+ description: string;
17
+ configured: boolean;
18
+ source?: string;
19
+ writable: boolean;
20
+ }
21
+ /** 列表端点的会话条目。 */
22
+ interface SessionEntry {
23
+ name: string;
24
+ envName: string;
25
+ description: string;
26
+ once: boolean;
27
+ createdAt: string;
28
+ }
29
+ declare class SecretEnvService extends Service {
30
+ static [Context.inject]: string[];
31
+ /** 全局值镜像(seam resolve 为异步,contributor 同步读这里)。 */
32
+ private readonly globalMirror;
33
+ /** 会话桶:sessionId → 后缀 → 条目。 */
34
+ private readonly buckets;
35
+ /** 变量名 → contributor 注销器(shell-env 要求一键一主)。 */
36
+ private readonly contributors;
37
+ /** 行级 config(settings 缺席时的索引来源)。 */
38
+ private readonly base;
39
+ /** installSection 给的实时 getter(scope.get 的活视图,读即最新,无时序竞态)。 */
40
+ private readIndex;
41
+ /** 上次调和时的索引快照(onChange 差量用)。 */
42
+ private lastMeta;
43
+ private settingsRef;
44
+ /** 启动镜像建立完成(测试与需要确定性的调用方可等待)。 */
45
+ readonly ready: Promise<void>;
46
+ constructor(ctx: Context, config: SecretEnvConfig | undefined);
47
+ /** 当前生效索引(settings 活视图优先,缺席回落行级 config)。 */
48
+ private currentMeta;
49
+ /** 索引成员判定(索引是全局名管理面的唯一事实源)。 */
50
+ private isIndexed;
51
+ /** 索引差量调和:新名建镜像,移出名撤镜像并回收其独占的 contributor。 */
52
+ private reconcile;
53
+ /** 重新 resolve 一个全局名并同步镜像与 contributor(外部热编辑亦走此路)。 */
54
+ private refreshMirror;
55
+ /** contributor 登记簿:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
56
+ private syncContributor;
57
+ private registerContributor;
58
+ /** 注入解析:会话值优先,未命中回落全局镜像;once 命中即焚(注销推迟到微任务,避免 collect 迭代中改注册表)。 */
59
+ private resolveFor;
60
+ /** 会话终结:整桶清除并回收其独占的 contributor。 */
61
+ private dropBucket;
62
+ private persistMeta;
63
+ /** 列表(值永不出现;global 条目来自 credentials.describe 的安全视图)。 */
64
+ list(sessionId?: string): Promise<{
65
+ global: GlobalEntry[];
66
+ session: SessionEntry[];
67
+ }>;
68
+ /** 写入全局值(seam 拒绝继承环境遮蔽与空值,此处先行给出结构化错误)。 */
69
+ setGlobal(suffix: string, value: string, description: string): Promise<void>;
70
+ /** 删除全局值与元数据。 */
71
+ unsetGlobal(suffix: string): Promise<void>;
72
+ /** 写入会话级值(纯内存;host 重启或会话终结即失)。 */
73
+ setSession(sessionId: string, suffix: string, value: string, description: string, once: boolean): void;
74
+ /** 删除会话级值。 */
75
+ unsetSession(sessionId: string, suffix: string): void;
76
+ dispose(): void;
77
+ }
78
+ //#endregion
79
+ //#region src/errors.d.ts
80
+ /**
81
+ * 结构化错误(独立模块,避免 service ↔ api 循环依赖)。
82
+ * 端点映射为 HTTP 状态码 + 错误码,UI 按码给文案。
83
+ * @module secret-env/errors
84
+ */
85
+ declare class SecretEnvError extends Error {
86
+ readonly code: string;
87
+ constructor(code: string, message: string);
88
+ }
89
+ //#endregion
90
+ //#region src/index.d.ts
91
+ declare const name = "dsh-plus-secret-env";
92
+ declare const inject: readonly ["credentials", "shellEnv"];
93
+ declare module '@deepseek-ai/cordis' {
94
+ interface Context {
95
+ secretEnv: SecretEnvService;
96
+ }
97
+ }
98
+ declare function apply(ctx: Context, config?: SecretEnvConfig): void;
99
+ //#endregion
100
+ export { Config, SecretEnvError, SecretEnvService, apply, inject, name };
package/lib/index.js ADDED
@@ -0,0 +1,428 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
+ //#region src/ns.ts
5
+ /**
6
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
7
+ * 服务端以字面量注册元数据索引,浏览器半作为 settings.section 页标识。
8
+ * @module secret-env/ns
9
+ */
10
+ const SETTINGS_NS = "dsh-plus-secret-env";
11
+ //#endregion
12
+ //#region src/config.ts
13
+ /**
14
+ * 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
15
+ * (用户层,$DSH_HOME/settings.yaml 热生效)共用同一 schemastery schema。
16
+ * `secrets` 只是全局密钥的**元数据索引**(后缀/描述/创建时间),
17
+ * 值永远走 dsh-credentials seam($DSH_HOME/.credentials.yaml),不进本文件。
18
+ * @module secret-env/config
19
+ */
20
+ /** 一条全局密钥的元数据(值不在此处)。 */
21
+ const SecretMetaSchema = z.object({
22
+ name: z.string().min(1).description("变量名后缀(如 GITHUB_TOKEN)").default(""),
23
+ description: z.string().description("用途描述(仅人读,不进提示词)").default(""),
24
+ createdAt: z.string().description("创建时间 ISO").default("")
25
+ });
26
+ const Config = z.object({ secrets: z.array(SecretMetaSchema).description("全局密钥元数据索引(值存凭据库)").default([]) });
27
+ //#endregion
28
+ //#region src/errors.ts
29
+ /**
30
+ * 结构化错误(独立模块,避免 service ↔ api 循环依赖)。
31
+ * 端点映射为 HTTP 状态码 + 错误码,UI 按码给文案。
32
+ * @module secret-env/errors
33
+ */
34
+ var SecretEnvError = class extends Error {
35
+ code;
36
+ constructor(code, message) {
37
+ super(message);
38
+ this.code = code;
39
+ }
40
+ };
41
+ //#endregion
42
+ //#region src/names.ts
43
+ /**
44
+ * 变量名语法(纯函数,零依赖;node 半与浏览器半共用):
45
+ * 用户给出后缀(如 `GITHUB_TOKEN`),插件拼出完整受管变量名
46
+ * `DSH_SECRET_GITHUB_TOKEN`——即模型经 `env | grep ^DSH_` 可见、
47
+ * 在 shell 命令中以 `$DSH_SECRET_*` 引用的那个名字。
48
+ * @module secret-env/names
49
+ */
50
+ /** 受管变量名前缀(属 dsh-shell-env 的 DSH_* 受管命名空间)。 */
51
+ const ENV_PREFIX = "DSH_SECRET_";
52
+ /** 后缀语法:POSIX 环境变量名,大写字母开头,总长(含前缀)不超过 79。 */
53
+ const SUFFIX_PATTERN = /^[A-Z][A-Z0-9_]*$/;
54
+ const MAX_SUFFIX_LENGTH = 64;
55
+ /** 规范化用户输入:去空白并转大写。 */
56
+ function normalizeSuffix(raw) {
57
+ return raw.trim().toUpperCase();
58
+ }
59
+ /** 校验规范化后的后缀;合法返回 null,非法返回失败码。 */
60
+ function validateSuffix(suffix) {
61
+ if (suffix.length === 0) return "empty";
62
+ if (suffix.length > MAX_SUFFIX_LENGTH) return "too-long";
63
+ if (!SUFFIX_PATTERN.test(suffix)) return "bad-charset";
64
+ return null;
65
+ }
66
+ /** 由后缀拼出完整受管变量名(调用方保证后缀已通过校验)。 */
67
+ function envNameOf(suffix) {
68
+ return `${ENV_PREFIX}${suffix}`;
69
+ }
70
+ /** 判断一个完整变量名是否属本插件管理(用于凭据热更事件过滤)。 */
71
+ function isManagedEnvName(envName) {
72
+ return envName.startsWith("DSH_SECRET_") && envName.length > 11;
73
+ }
74
+ /** 由完整变量名反解后缀;非本插件名返回 undefined。 */
75
+ function suffixOf(envName) {
76
+ return isManagedEnvName(envName) ? envName.slice(11) : void 0;
77
+ }
78
+ //#endregion
79
+ //#region src/api.ts
80
+ const ROUTE = "/dsh-plus/secret-env";
81
+ function sendJson(res, status, body) {
82
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
83
+ res.end(JSON.stringify(body));
84
+ }
85
+ function readBody(req) {
86
+ return new Promise((resolve, reject) => {
87
+ const chunks = [];
88
+ req.on("data", (chunk) => chunks.push(chunk));
89
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
90
+ req.on("error", reject);
91
+ });
92
+ }
93
+ /** 解析并校验请求里的变量名后缀;失败抛 SecretEnvError。 */
94
+ function readSuffix(raw) {
95
+ if (typeof raw !== "string") throw new SecretEnvError("invalid-name", "name must be a string");
96
+ const suffix = normalizeSuffix(raw);
97
+ const failure = validateSuffix(suffix);
98
+ if (failure !== null) throw new SecretEnvError("invalid-name", `invalid name: ${failure}`);
99
+ return suffix;
100
+ }
101
+ function readString(raw) {
102
+ return typeof raw === "string" ? raw : "";
103
+ }
104
+ /** 错误 → HTTP 映射(值类错误 400,遮蔽/冲突 409,其余 500;均只带错误码与消息)。 */
105
+ function sendError(res, error) {
106
+ if (error instanceof SecretEnvError) {
107
+ sendJson(res, error.code === "shadowed" || error.code === "conflict" ? 409 : 400, {
108
+ ok: false,
109
+ error: error.code,
110
+ message: error.message
111
+ });
112
+ return;
113
+ }
114
+ sendJson(res, 500, {
115
+ ok: false,
116
+ error: "internal"
117
+ });
118
+ }
119
+ /** 注册端点(webServer 缺席时由调用方保证不调用)。 */
120
+ function registerSecretEnvApi(ctx, service) {
121
+ const logger = ctx.logger("secret-env");
122
+ const guard = (label, run, res) => {
123
+ run().catch((error) => {
124
+ if (!(error instanceof SecretEnvError)) logger.warn(`${label} failed: ${error instanceof Error ? error.message : String(error)}`);
125
+ sendError(res, error);
126
+ });
127
+ };
128
+ ctx.webServer.register({
129
+ kind: "prefix",
130
+ path: `${ROUTE}/list`,
131
+ handler: async (req, res) => {
132
+ const sessionId = new URL(req.url ?? "", "http://localhost").searchParams.get("sessionId") ?? void 0;
133
+ guard("list", async () => sendJson(res, 200, await service.list(sessionId)), res);
134
+ }
135
+ });
136
+ ctx.webServer.register({
137
+ kind: "prefix",
138
+ path: `${ROUTE}/global/set`,
139
+ handler: async (req, res) => {
140
+ guard("global/set", async () => {
141
+ if (req.method !== "POST") throw new SecretEnvError("method", "POST only");
142
+ const body = JSON.parse(await readBody(req));
143
+ await service.setGlobal(readSuffix(body.name), readString(body.value), readString(body.description));
144
+ sendJson(res, 200, { ok: true });
145
+ }, res);
146
+ }
147
+ });
148
+ ctx.webServer.register({
149
+ kind: "prefix",
150
+ path: `${ROUTE}/global/unset`,
151
+ handler: async (req, res) => {
152
+ guard("global/unset", async () => {
153
+ if (req.method !== "POST") throw new SecretEnvError("method", "POST only");
154
+ const body = JSON.parse(await readBody(req));
155
+ await service.unsetGlobal(readSuffix(body.name));
156
+ sendJson(res, 200, { ok: true });
157
+ }, res);
158
+ }
159
+ });
160
+ ctx.webServer.register({
161
+ kind: "prefix",
162
+ path: `${ROUTE}/session/set`,
163
+ handler: async (req, res) => {
164
+ guard("session/set", async () => {
165
+ if (req.method !== "POST") throw new SecretEnvError("method", "POST only");
166
+ const body = JSON.parse(await readBody(req));
167
+ service.setSession(readString(body.sessionId), readSuffix(body.name), readString(body.value), readString(body.description), body.once === true);
168
+ sendJson(res, 200, { ok: true });
169
+ }, res);
170
+ }
171
+ });
172
+ ctx.webServer.register({
173
+ kind: "prefix",
174
+ path: `${ROUTE}/session/unset`,
175
+ handler: async (req, res) => {
176
+ guard("session/unset", async () => {
177
+ if (req.method !== "POST") throw new SecretEnvError("method", "POST only");
178
+ const body = JSON.parse(await readBody(req));
179
+ service.unsetSession(readString(body.sessionId), readSuffix(body.name));
180
+ sendJson(res, 200, { ok: true });
181
+ }, res);
182
+ }
183
+ });
184
+ }
185
+ //#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
+ */
198
+ function asMeta(value) {
199
+ if (value === void 0 || value === null || typeof value !== "object") return [];
200
+ const list = value.secrets;
201
+ if (!Array.isArray(list)) return [];
202
+ return list.filter((item) => typeof item === "object" && item !== null).map((item) => ({
203
+ name: typeof item.name === "string" ? item.name : "",
204
+ description: typeof item.description === "string" ? item.description : "",
205
+ createdAt: typeof item.createdAt === "string" ? item.createdAt : ""
206
+ })).filter((item) => item.name.length > 0);
207
+ }
208
+ var SecretEnvService = class extends Service {
209
+ static [Context.inject] = ["credentials", "shellEnv"];
210
+ /** 全局值镜像(seam resolve 为异步,contributor 同步读这里)。 */
211
+ globalMirror = /* @__PURE__ */ new Map();
212
+ /** 会话桶:sessionId → 后缀 → 条目。 */
213
+ buckets = /* @__PURE__ */ new Map();
214
+ /** 变量名 → contributor 注销器(shell-env 要求一键一主)。 */
215
+ contributors = /* @__PURE__ */ new Map();
216
+ /** 行级 config(settings 缺席时的索引来源)。 */
217
+ base;
218
+ /** installSection 给的实时 getter(scope.get 的活视图,读即最新,无时序竞态)。 */
219
+ readIndex;
220
+ /** 上次调和时的索引快照(onChange 差量用)。 */
221
+ lastMeta = [];
222
+ settingsRef;
223
+ /** 启动镜像建立完成(测试与需要确定性的调用方可等待)。 */
224
+ ready;
225
+ constructor(ctx, config) {
226
+ super(ctx, "secretEnv");
227
+ this.base = config ?? { secrets: [] };
228
+ ctx.inject(["settings"], (settingsCtx) => {
229
+ const settings = settingsCtx.settings;
230
+ settings.installSection(ctx, SETTINGS_NS, Config, this.base, {
231
+ setSource: (source) => {
232
+ this.readIndex = source;
233
+ },
234
+ onChange: () => {
235
+ this.reconcile();
236
+ }
237
+ });
238
+ this.settingsRef = settings;
239
+ this.reconcile();
240
+ });
241
+ ctx.root.on("session/disposed", (session) => {
242
+ this.dropBucket(session.id);
243
+ });
244
+ ctx.on("credentials/reference-updated", (ref) => {
245
+ const suffix = suffixOf(String(ref));
246
+ if (suffix !== void 0 && this.isIndexed(suffix)) this.refreshMirror(suffix);
247
+ });
248
+ ctx.inject(["webServer"], (webCtx) => {
249
+ registerSecretEnvApi(webCtx, this);
250
+ });
251
+ this.ready = this.reconcile();
252
+ }
253
+ /** 当前生效索引(settings 活视图优先,缺席回落行级 config)。 */
254
+ currentMeta() {
255
+ return asMeta(this.readIndex !== void 0 ? this.readIndex() : this.base);
256
+ }
257
+ /** 索引成员判定(索引是全局名管理面的唯一事实源)。 */
258
+ isIndexed(suffix) {
259
+ return this.currentMeta().some((item) => item.name === suffix);
260
+ }
261
+ /** 索引差量调和:新名建镜像,移出名撤镜像并回收其独占的 contributor。 */
262
+ async reconcile() {
263
+ const next = this.currentMeta();
264
+ const removed = this.lastMeta.filter((item) => !next.some((n) => n.name === item.name));
265
+ const added = next.filter((item) => !this.lastMeta.some((p) => p.name === item.name));
266
+ this.lastMeta = next;
267
+ for (const item of removed) {
268
+ this.globalMirror.delete(item.name);
269
+ this.syncContributor(item.name);
270
+ }
271
+ for (const item of added) await this.refreshMirror(item.name);
272
+ }
273
+ /** 重新 resolve 一个全局名并同步镜像与 contributor(外部热编辑亦走此路)。 */
274
+ async refreshMirror(suffix) {
275
+ const resolved = await this.ctx.credentials.resolve(credentialRef(envNameOf(suffix)));
276
+ if (resolved === void 0) this.globalMirror.delete(suffix);
277
+ else this.globalMirror.set(suffix, resolved.value);
278
+ this.syncContributor(suffix);
279
+ }
280
+ /** contributor 登记簿:有任一来源(全局镜像或任一会话桶)则注册,否则注销。 */
281
+ syncContributor(suffix) {
282
+ const envName = envNameOf(suffix);
283
+ const hasGlobal = this.globalMirror.has(suffix);
284
+ let hasSession = false;
285
+ for (const bucket of this.buckets.values()) if (bucket.has(suffix)) {
286
+ hasSession = true;
287
+ break;
288
+ }
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
+ }
298
+ }
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 迭代中改注册表)。 */
317
+ resolveFor(suffix, execution) {
318
+ const sessionId = execution.agent?.id;
319
+ const bucket = sessionId === void 0 ? void 0 : this.buckets.get(String(sessionId));
320
+ const entry = bucket?.get(suffix);
321
+ if (entry !== void 0) {
322
+ if (entry.once && bucket !== void 0) {
323
+ bucket.delete(suffix);
324
+ queueMicrotask(() => this.syncContributor(suffix));
325
+ }
326
+ return entry.value;
327
+ }
328
+ return this.globalMirror.get(suffix);
329
+ }
330
+ /** 会话终结:整桶清除并回收其独占的 contributor。 */
331
+ dropBucket(sessionId) {
332
+ const bucket = this.buckets.get(sessionId);
333
+ if (bucket === void 0) return;
334
+ this.buckets.delete(sessionId);
335
+ for (const suffix of bucket.keys()) this.syncContributor(suffix);
336
+ }
337
+ async persistMeta(next) {
338
+ if (this.settingsRef === void 0) return;
339
+ await this.settingsRef.update(SETTINGS_NS, { secrets: next });
340
+ }
341
+ /** 列表(值永不出现;global 条目来自 credentials.describe 的安全视图)。 */
342
+ async list(sessionId) {
343
+ return {
344
+ global: await Promise.all(this.currentMeta().map(async (item) => {
345
+ const info = await this.ctx.credentials.describe(credentialRef(envNameOf(item.name)));
346
+ return {
347
+ name: item.name,
348
+ envName: envNameOf(item.name),
349
+ description: item.description,
350
+ configured: info.configured,
351
+ source: info.source,
352
+ writable: info.writable
353
+ };
354
+ })),
355
+ session: [...(sessionId === void 0 ? void 0 : this.buckets.get(sessionId))?.entries() ?? []].map(([name, entry]) => ({
356
+ name,
357
+ envName: envNameOf(name),
358
+ description: entry.description,
359
+ once: entry.once,
360
+ createdAt: entry.createdAt
361
+ }))
362
+ };
363
+ }
364
+ /** 写入全局值(seam 拒绝继承环境遮蔽与空值,此处先行给出结构化错误)。 */
365
+ async setGlobal(suffix, value, description) {
366
+ if (value.length === 0) throw new SecretEnvError("empty-value", "value must not be empty");
367
+ try {
368
+ await this.ctx.credentials.set(credentialRef(envNameOf(suffix)), value);
369
+ } catch (error) {
370
+ throw new SecretEnvError("shadowed", error instanceof Error ? error.message : String(error));
371
+ }
372
+ const meta = this.currentMeta();
373
+ const next = meta.find((item) => item.name === suffix) === void 0 ? [...meta, {
374
+ name: suffix,
375
+ description,
376
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
377
+ }] : meta.map((item) => item.name === suffix ? {
378
+ ...item,
379
+ description
380
+ } : item);
381
+ await this.persistMeta(next);
382
+ this.lastMeta = next;
383
+ await this.refreshMirror(suffix);
384
+ }
385
+ /** 删除全局值与元数据。 */
386
+ async unsetGlobal(suffix) {
387
+ await this.ctx.credentials.unset(credentialRef(envNameOf(suffix)));
388
+ const next = this.currentMeta().filter((item) => item.name !== suffix);
389
+ await this.persistMeta(next);
390
+ this.lastMeta = next;
391
+ this.globalMirror.delete(suffix);
392
+ this.syncContributor(suffix);
393
+ }
394
+ /** 写入会话级值(纯内存;host 重启或会话终结即失)。 */
395
+ setSession(sessionId, suffix, value, description, once) {
396
+ if (sessionId.length === 0) throw new SecretEnvError("no-session", "sessionId required");
397
+ if (value.length === 0) throw new SecretEnvError("empty-value", "value must not be empty");
398
+ const bucket = this.buckets.get(sessionId) ?? /* @__PURE__ */ new Map();
399
+ bucket.set(suffix, {
400
+ value,
401
+ description,
402
+ once,
403
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
404
+ });
405
+ this.buckets.set(sessionId, bucket);
406
+ this.syncContributor(suffix);
407
+ }
408
+ /** 删除会话级值。 */
409
+ unsetSession(sessionId, suffix) {
410
+ const bucket = this.buckets.get(sessionId);
411
+ if (bucket === void 0 || !bucket.delete(suffix)) return;
412
+ if (bucket.size === 0) this.buckets.delete(sessionId);
413
+ this.syncContributor(suffix);
414
+ }
415
+ dispose() {
416
+ for (const disposeContributor of this.contributors.values()) disposeContributor();
417
+ this.contributors.clear();
418
+ }
419
+ };
420
+ //#endregion
421
+ //#region src/index.ts
422
+ const name = "dsh-plus-secret-env";
423
+ const inject = ["credentials", "shellEnv"];
424
+ function apply(ctx, config) {
425
+ ctx.plugin(SecretEnvService, config);
426
+ }
427
+ //#endregion
428
+ export { Config, SecretEnvError, SecretEnvService, apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@dsh-plus/secret-env",
3
+ "version": "0.1.0",
4
+ "description": "dsh-plus service+ui plugin: 密钥环境变量——以 $DSH_SECRET_* 变量名向 agent 暴露密钥(全局持久/会话级/一次性),执行期经 dsh-shell-env 原生注入,值不进消息流,不影响缓存率",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": "./lib/client.js",
14
+ "./src/*": "./src/*",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "src"
20
+ ],
21
+ "dependencies": {
22
+ "@dsh-plus/shared": "0.1.7"
23
+ },
24
+ "peerDependencies": {
25
+ "@deepseek-ai/cordis": "^4.0.1",
26
+ "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.2",
27
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
28
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
29
+ "@deepseek-ai/dsh-shell-env": "^0.1.2-alpha.2",
30
+ "@deepseek-ai/schemastery": "^3.18.1"
31
+ },
32
+ "devDependencies": {
33
+ "@deepseek-ai/cordis": "4.0.2",
34
+ "@deepseek-ai/dsh-credentials": "0.1.2-alpha.2",
35
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
36
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
37
+ "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-shell-env": "0.1.2-alpha.2",
39
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
40
+ "@deepseek-ai/schemastery": "3.18.2",
41
+ "@types/node": "26.4.0",
42
+ "@types/react": "~18.3.31",
43
+ "react": "^18.2.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dsh": {
49
+ "client": {
50
+ "inject": [
51
+ "@deepseek-ai/dsh-client-ui-renderer",
52
+ "@deepseek-ai/dsh-client-locale",
53
+ "@deepseek-ai/dsh-api-remotes"
54
+ ],
55
+ "platform": "web"
56
+ }
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "watch": "tsdown --watch"
61
+ }
62
+ }