dsh-vault 0.3.0 → 0.4.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/README-zh.md +11 -1
- package/README.md +11 -1
- package/lib/client.js +123 -20
- package/lib/client.js.map +1 -1
- package/lib/index.js +310 -10
- package/lib/store.js +203 -4
- package/package.json +1 -1
package/README-zh.md
CHANGED
|
@@ -43,9 +43,17 @@ dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件:把你在使
|
|
|
43
43
|
| `vault_get` | 按 id 读取完整条目(含全部密钥) |
|
|
44
44
|
| `vault_search` | 跨标题/分类/用户名/邮箱/手机/主机/端口/URL/备注/标签/自定义字段(含数字/布尔/嵌套值)检索,返回无密摘要;`limit` 须为 1–100 的整数 |
|
|
45
45
|
| `vault_update` | 按 id 更新字段(未提供的字段保留;空字符串清除该字段;`title` 可改名) |
|
|
46
|
-
| `vault_delete` |
|
|
46
|
+
| `vault_delete` | 软删除条目(移入回收站,磁盘上仍加密保留) |
|
|
47
|
+
| `vault_restore` / `vault_purge` | 从回收站恢复 / 从磁盘永久移除 |
|
|
48
|
+
| `vault_lock` / `vault_unlock` | 显式锁定保险库(清空内存密钥)/ 重新解锁 |
|
|
47
49
|
| `vault_totp` | 为存储的 otpSecret(或直接传入的 Base32/otpauth URI)生成当前 6 位动态验证码 |
|
|
48
50
|
| `vault_generate_password` | 生成强随机密码(长度/字符集/去歧义/分组可选;`group` 须为 ≥2 的整数) |
|
|
51
|
+
| `vault_rotation` | 报告已过期 / 待轮换 / 即将过期的凭据(不含密钥) |
|
|
52
|
+
| `vault_health` | 扫描弱密码与跨条目复用凭据(不含密钥) |
|
|
53
|
+
| `vault_export` / `vault_import` | 整库加密备份/迁移(独立导出密码) |
|
|
54
|
+
| `vault_fill` | 按 host/URL/用户名/标题匹配条目并返回其凭据 |
|
|
55
|
+
| `vault_env` | 把标记 env 的条目(tags 含 `env`)渲染为 `KEY=VALUE` 行 |
|
|
56
|
+
| `vault_templates` | 返回某类凭据(ssh/api-key/oauth 等)的推荐字段 |
|
|
49
57
|
|
|
50
58
|
**典型开发场景**:存一条 SSH 凭据(`kind: ssh` + host/port/username/password 或 privateKey),开发时让模型 `vault_search` 找主机、`vault_get` 取连接信息;存 API 网关的 `api-key`/`oauth` 条目管理 access/refresh token 轮换。
|
|
51
59
|
|
|
@@ -130,6 +138,8 @@ tarball 自带预构建 `lib/` 产物,无需构建或 allowBuilds。
|
|
|
130
138
|
| `name` | 保险库名,用于默认路径(如 `name: work` → `$DSH_HOME/vault/work.json`) |
|
|
131
139
|
| `accessMode` | 模型工具的访问策略,三态:`readonly`(只读,工具与设置页的增/改/删全部被拒绝)、`ask`(默认——写入前询问,每次增/改/删都会走 harness 审批通道请你确认)、`auto`(自动读写,无需逐次确认)。设置页提供同样的三选一下拉并持久化到 `<vault 目录>/access.json`。 |
|
|
132
140
|
| `autoCapture` | `false`(默认)。设为 `true` 时,系统提示词会指导模型识别对话中出现的凭据,并**按用户偏好**用 `vault_add` 提供保存。 |
|
|
141
|
+
| `lockTimeoutSeconds` | 自动锁库:空闲超过该秒数后自动重新锁定(清空内存密钥),之后每次读写需 `vault_unlock`。`0`/缺省为禁用。 |
|
|
142
|
+
| `exportPasswordEnv` | 存放 `vault_export`/`vault_import` 导出密码的环境变量名(绝不能作为模型参数传入)。 |
|
|
133
143
|
|
|
134
144
|
示例:
|
|
135
145
|
|
package/README.md
CHANGED
|
@@ -45,9 +45,17 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
|
|
|
45
45
|
| `vault_get` | Read a full entry by id (including all secrets) |
|
|
46
46
|
| `vault_search` | Search titles/categories/usernames/emails/phones/hosts/ports/URLs/notes/tags/custom fields (incl. numeric/boolean/nested values); returns secret-free summaries; `limit` must be an integer 1–100 |
|
|
47
47
|
| `vault_update` | Update fields by id (unprovided fields kept; empty string clears a field; `title` is renamable) |
|
|
48
|
-
| `vault_delete` |
|
|
48
|
+
| `vault_delete` | Soft-delete an entry (moves it to the trash, still encrypted on disk) |
|
|
49
|
+
| `vault_restore` / `vault_purge` | Bring a trashed entry back / permanently remove it from disk |
|
|
50
|
+
| `vault_lock` / `vault_unlock` | Explicitly lock the vault (wipe the in-memory key) / re-unlock it |
|
|
49
51
|
| `vault_totp` | Generate the current 6-digit code for a stored otpSecret (or a bare Base32 / otpauth URI) |
|
|
50
52
|
| `vault_generate_password` | Generate a strong random password (length/character classes/ambiguity exclusion/grouping; `group` must be an integer ≥ 2) |
|
|
53
|
+
| `vault_rotation` | Report expired / due-for-rotation / expiring-soon credentials (no secrets) |
|
|
54
|
+
| `vault_health` | Scan for weak passwords and credentials reused across entries (no secrets) |
|
|
55
|
+
| `vault_export` / `vault_import` | Portable encrypted backup/migration of the whole vault (separate export password) |
|
|
56
|
+
| `vault_fill` | Find the entry matching a host/URL/username/title and return its credentials |
|
|
57
|
+
| `vault_env` | Render env-flagged entries (tags contain `env`) as `KEY=VALUE` lines |
|
|
58
|
+
| `vault_templates` | List recommended fields for a credential kind (ssh / api-key / oauth / …) |
|
|
51
59
|
|
|
52
60
|
**Typical workflows**: store an SSH credential (`kind: ssh` + host/port/username/password or privateKey) and have the model `vault_search` for the host then `vault_get` the connection details; keep `api-key`/`oauth` entries for API-gateway access/refresh token rotation.
|
|
53
61
|
|
|
@@ -132,6 +140,8 @@ The tarball ships prebuilt `lib/` artifacts, so no build step or `allowBuilds` i
|
|
|
132
140
|
| `name` | Vault name for the default path (e.g. `name: work` → `$DSH_HOME/vault/work.json`) |
|
|
133
141
|
| `accessMode` | Access policy for the model tools. Three states: `readonly` (mutations rejected on tools + UI), `ask` (default — reads free, every add/update/delete goes through the harness approval channel so the user confirms each write), or `auto` (automatic read-write, no per-call prompt). The Settings UI offers this exact three-way choice and persists it to `<vault dir>/access.json`. |
|
|
134
142
|
| `autoCapture` | `false` (default). When `true`, the system prompt instructs the model to detect credentials shared in conversation and — per user preference — offer to save them with `vault_add`. |
|
|
143
|
+
| `lockTimeoutSeconds` | Auto-lock: after this many seconds of inactivity the vault re-locks (key wiped) and every read/write requires `vault_unlock`. `0`/absent disables. |
|
|
144
|
+
| `exportPasswordEnv` | Environment variable holding the export/import password for `vault_export`/`vault_import` (never pass it as a model argument). |
|
|
135
145
|
|
|
136
146
|
Example:
|
|
137
147
|
|
package/lib/client.js
CHANGED
|
@@ -7,7 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
//#region \0dsh-css:/Users/zhipeng/Desktop/work/dsh test/dsh-vault/src/client/VaultSection.module.css.mjs
|
|
10
|
-
const css = ".t_j5va_section{flex-direction:column;gap:12px;padding:16px 0;display:flex}.t_j5va_intro{opacity:.75;margin:0;font-size:13px}.t_j5va_toolbar{align-items:center;gap:8px;display:flex}.t_j5va_searchBox{flex:1;display:flex}.t_j5va_searchBox input{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;flex:1;padding:6px 10px;font-size:13px}.t_j5va_addButton,.t_j5va_saveButton{background:var(--color-accent,#4f8cff);color:#fff;cursor:pointer;border:1px solid #0000;border-radius:6px;padding:6px 12px;font-size:13px}.t_j5va_addButton:disabled,.t_j5va_saveButton:disabled,.t_j5va_rowActions button:disabled{opacity:.5;cursor:not-allowed}.t_j5va_list{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.t_j5va_row{border:1px solid var(--color-border,#333);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 10px;display:flex}.t_j5va_rowMain{flex-direction:column;gap:2px;min-width:0;display:flex}.t_j5va_title{font-size:14px;font-weight:600}.t_j5va_identity{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.t_j5va_totp{font-family:var(--font-mono,monospace);color:var(--color-accent,#4f8cff);font-size:13px}.t_j5va_rowActions{flex-shrink:0;align-items:center;gap:6px;display:flex}.t_j5va_rowActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:4px 8px;font-size:12px}.t_j5va_deleteButton{color:#e5484d}.t_j5va_copied{color:#30a46c;font-size:12px}.t_j5va_empty,.t_j5va_status,.t_j5va_error{opacity:.8;font-size:13px}.t_j5va_modeReadonly,.t_j5va_modeAsk,.t_j5va_modeAuto{border-radius:999px;width:fit-content;margin:0;padding:4px 10px;font-size:12px;display:inline-block}.t_j5va_modeReadonly{color:#e5484d;border:1px solid #e5484d80}.t_j5va_modeAsk{color:#f5a524;border:1px solid #f5a52480}.t_j5va_modeAuto{color:#30a46c;border:1px solid #30a46c80}.t_j5va_policyBar{flex-direction:column;align-items:flex-start;gap:6px;display:flex}.t_j5va_policyField{align-items:center;gap:8px;font-size:13px;display:flex}.t_j5va_policyField select{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:4px 8px;font-size:13px}.t_j5va_emptyBox{flex-direction:column;gap:4px;display:flex}.t_j5va_emptyHint{opacity:.65;margin:0;font-size:12px}.t_j5va_error{color:#e5484d}.t_j5va_editor{border:1px solid var(--color-border,#444);border-radius:8px;padding:12px}.t_j5va_editorBody{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px 12px;display:grid}.t_j5va_field{flex-direction:column;gap:4px;font-size:12px;display:flex}.t_j5va_field input,.t_j5va_field select{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:5px 8px;font-size:13px}.t_j5va_editorActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.t_j5va_editorActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:6px 12px;font-size:13px}";
|
|
10
|
+
const css = ".t_j5va_section{flex-direction:column;gap:12px;padding:16px 0;display:flex}.t_j5va_intro{opacity:.75;margin:0;font-size:13px}.t_j5va_toolbar{align-items:center;gap:8px;display:flex}.t_j5va_searchBox{flex:1;display:flex}.t_j5va_searchBox input{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;flex:1;padding:6px 10px;font-size:13px}.t_j5va_addButton,.t_j5va_saveButton{background:var(--color-accent,#4f8cff);color:#fff;cursor:pointer;border:1px solid #0000;border-radius:6px;padding:6px 12px;font-size:13px}.t_j5va_addButton:disabled,.t_j5va_saveButton:disabled,.t_j5va_rowActions button:disabled{opacity:.5;cursor:not-allowed}.t_j5va_list{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.t_j5va_row{border:1px solid var(--color-border,#333);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 10px;display:flex}.t_j5va_rowMain{flex-direction:column;gap:2px;min-width:0;display:flex}.t_j5va_title{font-size:14px;font-weight:600}.t_j5va_identity{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.t_j5va_totp{font-family:var(--font-mono,monospace);color:var(--color-accent,#4f8cff);font-size:13px}.t_j5va_rowActions{flex-shrink:0;align-items:center;gap:6px;display:flex}.t_j5va_rowActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:4px 8px;font-size:12px}.t_j5va_deleteButton{color:#e5484d}.t_j5va_copied{color:#30a46c;font-size:12px}.t_j5va_empty,.t_j5va_status,.t_j5va_error{opacity:.8;font-size:13px}.t_j5va_modeReadonly,.t_j5va_modeAsk,.t_j5va_modeAuto{border-radius:999px;width:fit-content;margin:0;padding:4px 10px;font-size:12px;display:inline-block}.t_j5va_modeReadonly{color:#e5484d;border:1px solid #e5484d80}.t_j5va_modeAsk{color:#f5a524;border:1px solid #f5a52480}.t_j5va_modeAuto{color:#30a46c;border:1px solid #30a46c80}.t_j5va_policyBar{flex-direction:column;align-items:flex-start;gap:6px;display:flex}.t_j5va_policyField{align-items:center;gap:8px;font-size:13px;display:flex}.t_j5va_policyField select{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:4px 8px;font-size:13px}.t_j5va_emptyBox{flex-direction:column;gap:4px;display:flex}.t_j5va_emptyHint{opacity:.65;margin:0;font-size:12px}.t_j5va_error{color:#e5484d}.t_j5va_editor{border:1px solid var(--color-border,#444);border-radius:8px;padding:12px}.t_j5va_editorBody{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px 12px;display:grid}.t_j5va_field{flex-direction:column;gap:4px;font-size:12px;display:flex}.t_j5va_field input,.t_j5va_field select{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:5px 8px;font-size:13px}.t_j5va_editorActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.t_j5va_editorActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:6px 12px;font-size:13px}.t_j5va_reportBox{border:1px solid #f5a52466;border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;font-size:12px;display:flex}.t_j5va_reportTitle{margin:0;font-weight:600}.t_j5va_reportLine{opacity:.8;margin:0}.t_j5va_trashButton{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;align-self:flex-start;padding:4px 8px;font-size:12px}";
|
|
11
11
|
const tagId = "dsh-vault/VaultSection.module.css";
|
|
12
12
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
13
13
|
const tag = document.createElement("style");
|
|
@@ -17,35 +17,39 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
document.head.appendChild(tag);
|
|
18
18
|
}
|
|
19
19
|
var VaultSection_module_css_default = {
|
|
20
|
-
"emptyBox": "t_j5va_emptyBox",
|
|
21
|
-
"modeAuto": "t_j5va_modeAuto",
|
|
22
|
-
"modeAsk": "t_j5va_modeAsk",
|
|
23
|
-
"rowActions": "t_j5va_rowActions",
|
|
24
20
|
"field": "t_j5va_field",
|
|
21
|
+
"deleteButton": "t_j5va_deleteButton",
|
|
25
22
|
"totp": "t_j5va_totp",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"section": "t_j5va_section",
|
|
29
|
-
"identity": "t_j5va_identity",
|
|
30
|
-
"modeReadonly": "t_j5va_modeReadonly",
|
|
31
|
-
"searchBox": "t_j5va_searchBox",
|
|
32
|
-
"policyField": "t_j5va_policyField",
|
|
23
|
+
"trashButton": "t_j5va_trashButton",
|
|
24
|
+
"empty": "t_j5va_empty",
|
|
33
25
|
"saveButton": "t_j5va_saveButton",
|
|
34
|
-
"deleteButton": "t_j5va_deleteButton",
|
|
35
26
|
"copied": "t_j5va_copied",
|
|
36
|
-
"editorBody": "t_j5va_editorBody",
|
|
37
27
|
"addButton": "t_j5va_addButton",
|
|
38
|
-
"
|
|
28
|
+
"identity": "t_j5va_identity",
|
|
29
|
+
"row": "t_j5va_row",
|
|
30
|
+
"emptyHint": "t_j5va_emptyHint",
|
|
31
|
+
"modeAsk": "t_j5va_modeAsk",
|
|
32
|
+
"policyBar": "t_j5va_policyBar",
|
|
39
33
|
"status": "t_j5va_status",
|
|
34
|
+
"modeAuto": "t_j5va_modeAuto",
|
|
35
|
+
"editor": "t_j5va_editor",
|
|
36
|
+
"editorBody": "t_j5va_editorBody",
|
|
37
|
+
"reportTitle": "t_j5va_reportTitle",
|
|
38
|
+
"rowActions": "t_j5va_rowActions",
|
|
39
|
+
"reportLine": "t_j5va_reportLine",
|
|
40
|
+
"section": "t_j5va_section",
|
|
41
|
+
"reportBox": "t_j5va_reportBox",
|
|
40
42
|
"error": "t_j5va_error",
|
|
43
|
+
"searchBox": "t_j5va_searchBox",
|
|
41
44
|
"list": "t_j5va_list",
|
|
42
45
|
"rowMain": "t_j5va_rowMain",
|
|
43
|
-
"
|
|
46
|
+
"title": "t_j5va_title",
|
|
47
|
+
"modeReadonly": "t_j5va_modeReadonly",
|
|
48
|
+
"emptyBox": "t_j5va_emptyBox",
|
|
44
49
|
"intro": "t_j5va_intro",
|
|
45
50
|
"editorActions": "t_j5va_editorActions",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"emptyHint": "t_j5va_emptyHint"
|
|
51
|
+
"toolbar": "t_j5va_toolbar",
|
|
52
|
+
"policyField": "t_j5va_policyField"
|
|
49
53
|
};
|
|
50
54
|
//#endregion
|
|
51
55
|
//#region src/client/VaultSection.tsx
|
|
@@ -138,7 +142,7 @@ window.__ModuleLoader__.load({
|
|
|
138
142
|
}
|
|
139
143
|
/** Render the Vault settings section. */
|
|
140
144
|
function VaultSection(props) {
|
|
141
|
-
const { t, config, setAccessMode, list, search, get, add, update, remove, totp } = props;
|
|
145
|
+
const { t, config, setAccessMode, list, search, get, add, update, remove, trash, rotation, health, restore, totp } = props;
|
|
142
146
|
const searchId = (0, react.useId)();
|
|
143
147
|
const [query, setQuery] = (0, react.useState)("");
|
|
144
148
|
const [state, setState] = (0, react.useState)({ status: "loading" });
|
|
@@ -150,6 +154,9 @@ window.__ModuleLoader__.load({
|
|
|
150
154
|
const [codeMap, setCodeMap] = (0, react.useState)({});
|
|
151
155
|
const [tagsDraft, setTagsDraft] = (0, react.useState)("");
|
|
152
156
|
const [policy, setPolicy] = (0, react.useState)(null);
|
|
157
|
+
const [showTrash, setShowTrash] = (0, react.useState)(false);
|
|
158
|
+
const [trashEntries, setTrashEntries] = (0, react.useState)([]);
|
|
159
|
+
const [report, setReport] = (0, react.useState)(null);
|
|
153
160
|
const readonly = policy?.accessMode === "readonly";
|
|
154
161
|
(0, react.useEffect)(() => {
|
|
155
162
|
let current = true;
|
|
@@ -424,6 +431,80 @@ window.__ModuleLoader__.load({
|
|
|
424
431
|
children: readonly ? t("emptyHintReadonly") : t("emptyHint")
|
|
425
432
|
})]
|
|
426
433
|
}),
|
|
434
|
+
report !== null && (report.rotation.length > 0 || report.weak.length > 0 || report.reused.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
435
|
+
className: VaultSection_module_css_default.reportBox,
|
|
436
|
+
children: [
|
|
437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
438
|
+
className: VaultSection_module_css_default.reportTitle,
|
|
439
|
+
children: t("reportTitle")
|
|
440
|
+
}),
|
|
441
|
+
report.rotation.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
442
|
+
className: VaultSection_module_css_default.reportLine,
|
|
443
|
+
children: [
|
|
444
|
+
t("reportRotation"),
|
|
445
|
+
": ",
|
|
446
|
+
report.rotation.length
|
|
447
|
+
]
|
|
448
|
+
}),
|
|
449
|
+
report.weak.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
450
|
+
className: VaultSection_module_css_default.reportLine,
|
|
451
|
+
children: [
|
|
452
|
+
t("reportWeak"),
|
|
453
|
+
": ",
|
|
454
|
+
report.weak.length
|
|
455
|
+
]
|
|
456
|
+
}),
|
|
457
|
+
report.reused.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
458
|
+
className: VaultSection_module_css_default.reportLine,
|
|
459
|
+
children: [
|
|
460
|
+
t("reportReused"),
|
|
461
|
+
": ",
|
|
462
|
+
report.reused.length
|
|
463
|
+
]
|
|
464
|
+
})
|
|
465
|
+
]
|
|
466
|
+
}),
|
|
467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
468
|
+
type: "button",
|
|
469
|
+
className: VaultSection_module_css_default.trashButton,
|
|
470
|
+
onClick: () => {
|
|
471
|
+
if (!showTrash) trash().then(setTrashEntries);
|
|
472
|
+
setShowTrash(!showTrash);
|
|
473
|
+
},
|
|
474
|
+
children: [showTrash ? t("hideTrash") : t("showTrash"), trashEntries.length > 0 ? ` (${trashEntries.length})` : ""]
|
|
475
|
+
}),
|
|
476
|
+
showTrash && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
477
|
+
className: VaultSection_module_css_default.list,
|
|
478
|
+
children: [trashEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
479
|
+
className: VaultSection_module_css_default.row,
|
|
480
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
481
|
+
className: VaultSection_module_css_default.rowMain,
|
|
482
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
483
|
+
className: VaultSection_module_css_default.title,
|
|
484
|
+
children: entry.title
|
|
485
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
486
|
+
className: VaultSection_module_css_default.identity,
|
|
487
|
+
children: t("trashed")
|
|
488
|
+
})]
|
|
489
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
490
|
+
className: VaultSection_module_css_default.rowActions,
|
|
491
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
492
|
+
type: "button",
|
|
493
|
+
onClick: () => {
|
|
494
|
+
restore(entry.id).then(() => {
|
|
495
|
+
trash().then(setTrashEntries);
|
|
496
|
+
refresh();
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
disabled: busy || readonly,
|
|
500
|
+
children: t("restore")
|
|
501
|
+
})
|
|
502
|
+
})]
|
|
503
|
+
}, entry.id)), trashEntries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
504
|
+
className: VaultSection_module_css_default.empty,
|
|
505
|
+
children: t("trashEmpty")
|
|
506
|
+
})]
|
|
507
|
+
}),
|
|
427
508
|
state.status === "ready" && state.entries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
428
509
|
className: VaultSection_module_css_default.list,
|
|
429
510
|
children: state.entries.map((entry) => {
|
|
@@ -564,6 +645,15 @@ window.__ModuleLoader__.load({
|
|
|
564
645
|
modeAutoHint: "Automatic read-write — no per-call prompt",
|
|
565
646
|
autoCaptureOn: "auto-capture on",
|
|
566
647
|
autoCaptureOff: "auto-capture off",
|
|
648
|
+
reportTitle: "Health & rotation",
|
|
649
|
+
reportRotation: "expired / due for rotation",
|
|
650
|
+
reportWeak: "weak passwords",
|
|
651
|
+
reportReused: "credentials reused across entries",
|
|
652
|
+
showTrash: "Trash",
|
|
653
|
+
hideTrash: "Hide trash",
|
|
654
|
+
trashed: "in trash",
|
|
655
|
+
trashEmpty: "Trash is empty",
|
|
656
|
+
restore: "Restore",
|
|
567
657
|
add: "Add credential",
|
|
568
658
|
cancel: "Cancel",
|
|
569
659
|
save: "Save",
|
|
@@ -616,6 +706,15 @@ window.__ModuleLoader__.load({
|
|
|
616
706
|
modeAutoHint: "自动读写 —— 无需逐次确认",
|
|
617
707
|
autoCaptureOn: "自动捕获已开启",
|
|
618
708
|
autoCaptureOff: "自动捕获已关闭",
|
|
709
|
+
reportTitle: "健康与轮换",
|
|
710
|
+
reportRotation: "已过期 / 待轮换",
|
|
711
|
+
reportWeak: "弱密码",
|
|
712
|
+
reportReused: "跨条目复用的凭据",
|
|
713
|
+
showTrash: "回收站",
|
|
714
|
+
hideTrash: "隐藏回收站",
|
|
715
|
+
trashed: "已删除",
|
|
716
|
+
trashEmpty: "回收站为空",
|
|
717
|
+
restore: "恢复",
|
|
619
718
|
add: "新增凭据",
|
|
620
719
|
cancel: "取消",
|
|
621
720
|
save: "保存",
|
|
@@ -693,6 +792,10 @@ window.__ModuleLoader__.load({
|
|
|
693
792
|
patch
|
|
694
793
|
}),
|
|
695
794
|
remove: (id) => invoke("delete", { id }),
|
|
795
|
+
trash: () => invoke("trash").then((r) => r.entries),
|
|
796
|
+
rotation: () => invoke("rotation").then((r) => r.entries),
|
|
797
|
+
health: () => invoke("health"),
|
|
798
|
+
restore: (id) => invoke("restore", { id }),
|
|
696
799
|
totp: (id) => invoke("totp", { id })
|
|
697
800
|
});
|
|
698
801
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["css"],"sources":["../src/client/VaultSection.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Vault settings section: list, search, add, edit, delete, and copy encrypted\n * credentials. All mutations go through the host VaultGateway remote; secrets\n * are shown only inside the edit form or a copy action, never in the list.\n */\n\nimport { useEffect, useId, useMemo, useState, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime, InjectFace, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { VaultLocaleKey } from './locales.ts'\nimport css from './VaultSection.module.css'\n\n/** Wire shapes shared with the host gateway (mirror of src/index.ts types). */\nexport interface VaultSummaryWire {\n id: string\n title: string\n kind?: string\n username?: string\n email?: string\n phone?: string\n host?: string\n port?: string\n url?: string\n tags?: string[]\n}\n\nexport interface VaultFullWire {\n id: string\n title: string\n kind?: string\n username?: string\n email?: string\n phone?: string\n password?: string\n host?: string\n port?: string\n privateKey?: string\n apiKey?: string\n secret?: string\n accessToken?: string\n refreshToken?: string\n expiresAt?: number\n otpSecret?: string\n url?: string\n notes?: string\n tags?: string[]\n fields?: Record<string, unknown>\n}\n\n/** Patch shape accepted by vault.add / vault.update. */\nexport type VaultPatch = Partial<Omit<VaultFullWire, 'id'>>\n\n/** Registration-side business face supplied by the plugin entry. */\nexport interface VaultSectionInjected {\n t: TranslateNS<'settings.vault'>\n config: () => Promise<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }>\n setAccessMode: (mode: 'readonly' | 'ask' | 'auto') => Promise<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }>\n list: () => Promise<VaultSummaryWire[]>\n search: (query: string, limit?: number) => Promise<VaultSummaryWire[]>\n get: (id: string) => Promise<{ found: boolean; entry?: VaultFullWire }>\n add: (patch: VaultPatch & { title: string }) => Promise<VaultSummaryWire>\n update: (id: string, patch: VaultPatch) => Promise<{ found: boolean; entry?: VaultSummaryWire }>\n remove: (id: string) => Promise<{ deleted: boolean }>\n totp: (id: string) => Promise<{ code: string; label?: string; secondsRemaining: number }>\n}\n\n/** Type-level alias so consumers can reference the wire shapes without values. */\nexport type VaultSectionTypes = {\n entries: VaultSummaryWire[]\n fullEntry: VaultFullWire\n summaryEntry: VaultSummaryWire\n config: { accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }\n accessModes: Array<'readonly' | 'ask' | 'auto'>\n}\n\n/** Full component props assembled by the Settings slot renderer. */\nexport type VaultSectionProps =\n PropsRuntime<'settings.section'>\n & PropsLocale<'settings.vault'>\n & InjectFace<VaultSectionInjected>\n\n/** The editable form fields we render (subset of the full wire). */\ntype FormFields = Pick<\n VaultFullWire, 'title' | 'kind' | 'username' | 'email' | 'phone' | 'password' | 'host' | 'port'\n | 'privateKey' | 'apiKey' | 'secret' | 'accessToken' | 'refreshToken' | 'otpSecret' | 'url' | 'notes'\n>\n\nconst FORM_FIELDS: Array<{ key: keyof FormFields; label: VaultLocaleKey }> = [\n { key: 'title', label: 'fieldTitle' },\n { key: 'kind', label: 'fieldKind' },\n { key: 'username', label: 'fieldUsername' },\n { key: 'email', label: 'fieldEmail' },\n { key: 'phone', label: 'fieldPhone' },\n { key: 'password', label: 'fieldPassword' },\n { key: 'host', label: 'fieldHost' },\n { key: 'port', label: 'fieldPort' },\n { key: 'apiKey', label: 'fieldApiKey' },\n { key: 'secret', label: 'fieldSecret' },\n { key: 'accessToken', label: 'fieldAccessToken' },\n { key: 'refreshToken', label: 'fieldRefreshToken' },\n { key: 'otpSecret', label: 'fieldOtpSecret' },\n { key: 'url', label: 'fieldUrl' },\n { key: 'notes', label: 'fieldNotes' },\n]\n\nconst KIND_KEYS: Record<string, VaultLocaleKey> = {\n login: 'kindLogin',\n ssh: 'kindSsh',\n 'api-key': 'kindApiKey',\n secret: 'kindSecret',\n oauth: 'kindOauth',\n custom: 'kindCustom',\n}\n\ntype ViewState =\n | { readonly status: 'loading' }\n | { readonly status: 'error' }\n | { readonly status: 'ready'; readonly entries: VaultSummaryWire[] }\n\ntype EditorState =\n | { readonly status: 'closed' }\n | { readonly status: 'creating' }\n | { readonly status: 'editing'; readonly entry: VaultFullWire }\n\nfunction emptyForm(): FormFields {\n return { title: '', kind: 'login', username: '', email: '', phone: '', password: '', host: '', port: '' }\n}\n\n/** Render the Vault settings section. */\nexport function VaultSection(props: VaultSectionProps): ReactNode {\n const { t, config, setAccessMode, list, search, get, add, update, remove, totp } = props\n const searchId = useId()\n const [query, setQuery] = useState('')\n const [state, setState] = useState<ViewState>({ status: 'loading' })\n const [editor, setEditor] = useState<EditorState>({ status: 'closed' })\n const [form, setForm] = useState<FormFields>(emptyForm())\n const [busy, setBusy] = useState(false)\n const [message, setMessage] = useState<string | null>(null)\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [codeMap, setCodeMap] = useState<Record<string, string>>({})\n const [tagsDraft, setTagsDraft] = useState('')\n const [policy, setPolicy] = useState<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean } | null>(null)\n\n const readonly = policy?.accessMode === 'readonly'\n\n useEffect(() => {\n let current = true\n void config().then(\n value => { if (current) setPolicy(value) },\n () => { /* policy is informational; ignore failures */ },\n )\n return () => { current = false }\n }, [config])\n\n const refresh = useMemo(() => async () => {\n setState({ status: 'loading' })\n try {\n const entries = query.trim().length === 0\n ? await list()\n : await search(query.trim())\n setState({ status: 'ready', entries })\n } catch {\n setState({ status: 'error' })\n }\n }, [list, search, query])\n\n useEffect(() => {\n let current = true\n void Promise.resolve().then(refresh).then(\n () => { /* state already set inside refresh */ },\n () => { if (current) setState({ status: 'error' }) },\n )\n return () => { current = false }\n // refresh is memoized on query; list/search are stable inject faces.\n }, [refresh])\n\n /** Open the editor for a new entry. */\n function startCreate(): void {\n setForm(emptyForm())\n setTagsDraft('')\n setMessage(null)\n setEditor({ status: 'creating' })\n }\n\n /** Open the editor for an existing entry (fetches full secrets). */\n async function startEdit(id: string): Promise<void> {\n setBusy(true)\n setMessage(null)\n try {\n const result = await get(id)\n if (!result.found || result.entry === undefined) {\n setMessage(t('error'))\n return\n }\n const entry = result.entry\n setForm({\n title: entry.title ?? '',\n kind: entry.kind ?? 'login',\n username: entry.username ?? '',\n email: entry.email ?? '',\n phone: entry.phone ?? '',\n password: entry.password ?? '',\n host: entry.host ?? '',\n port: entry.port ?? '',\n privateKey: entry.privateKey ?? '',\n apiKey: entry.apiKey ?? '',\n secret: entry.secret ?? '',\n accessToken: entry.accessToken ?? '',\n refreshToken: entry.refreshToken ?? '',\n otpSecret: entry.otpSecret ?? '',\n url: entry.url ?? '',\n notes: entry.notes ?? '',\n })\n setTagsDraft((entry.tags ?? []).join(', '))\n setEditor({ status: 'editing', entry })\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Save the current form (create or update). */\n async function save(): Promise<void> {\n if (!form.title.trim()) {\n setMessage(t('error'))\n return\n }\n setBusy(true)\n setMessage(null)\n try {\n const tags = tagsDraft.split(',').map(x => x.trim()).filter(x => x.length > 0)\n const patch: VaultPatch = {\n title: form.title.trim(),\n ...(form.kind !== undefined ? { kind: form.kind } : {}),\n ...(form.username !== undefined ? { username: form.username } : {}),\n ...(form.email !== undefined ? { email: form.email } : {}),\n ...(form.phone !== undefined ? { phone: form.phone } : {}),\n ...(form.password !== undefined ? { password: form.password } : {}),\n ...(form.host !== undefined ? { host: form.host } : {}),\n ...(form.port !== undefined ? { port: form.port } : {}),\n ...(form.privateKey !== undefined ? { privateKey: form.privateKey } : {}),\n ...(form.apiKey !== undefined ? { apiKey: form.apiKey } : {}),\n ...(form.secret !== undefined ? { secret: form.secret } : {}),\n ...(form.accessToken !== undefined ? { accessToken: form.accessToken } : {}),\n ...(form.refreshToken !== undefined ? { refreshToken: form.refreshToken } : {}),\n ...(form.otpSecret !== undefined ? { otpSecret: form.otpSecret } : {}),\n ...(form.url !== undefined ? { url: form.url } : {}),\n ...(form.notes !== undefined ? { notes: form.notes } : {}),\n // Always send tags: an empty array clears them, and the host store\n // treats empty string values as \"clear this field\" too.\n tags,\n }\n if (editor.status === 'creating') {\n await add(patch as VaultPatch & { title: string })\n } else if (editor.status === 'editing') {\n await update(editor.entry.id, patch)\n }\n setEditor({ status: 'closed' })\n await refresh()\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Delete an entry after confirmation. */\n async function removeEntry(id: string): Promise<void> {\n if (!window.confirm(t('deleteConfirm'))) return\n setBusy(true)\n try {\n await remove(id)\n await refresh()\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Copy a field value to the clipboard and flash the row. */\n async function copyValue(id: string, value: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(value)\n } catch {\n // Clipboard may be blocked; fall back to a temporary textarea.\n const textarea = document.createElement('textarea')\n textarea.value = value\n document.body.appendChild(textarea)\n textarea.select()\n document.execCommand('copy')\n textarea.remove()\n }\n setCopiedId(id)\n window.setTimeout(() => setCopiedId(null), 1500)\n }\n\n /** Fetch and display a TOTP code for an entry with an otpSecret. */\n async function showTotp(id: string): Promise<void> {\n setBusy(true)\n try {\n const result = await totp(id)\n setCodeMap(previous => ({ ...previous, [id]: `${result.code} (${result.secondsRemaining}s)` }))\n } catch {\n setCodeMap(previous => ({ ...previous, [id]: t('error') }))\n } finally {\n setBusy(false)\n }\n }\n\n /** Field-set summary line for one entry (non-secret). */\n function identityLine(entry: VaultSummaryWire): string {\n const parts = [\n entry.kind !== undefined ? t(KIND_KEYS[entry.kind] ?? 'kindCustom') : t('kindLogin'),\n entry.username,\n entry.email,\n entry.phone,\n entry.host !== undefined ? `${entry.host}${entry.port !== undefined ? `:${entry.port}` : ''}` : undefined,\n entry.url,\n ].filter((value): value is string => value !== undefined && value.length > 0)\n return parts.join(' · ')\n }\n\n return (\n <section className={css.section} aria-labelledby=\"vault-heading\">\n <h2 id=\"vault-heading\">{t('heading')}</h2>\n <p className={css.intro}>{t('intro')}</p>\n\n <div className={css.toolbar}>\n <label className={css.searchBox}>\n <span className=\"sr-only\">{t('searchPlaceholder')}</span>\n <input\n id={searchId}\n type=\"search\"\n placeholder={t('searchPlaceholder')}\n value={query}\n onChange={event => setQuery(event.target.value)}\n />\n </label>\n <button type=\"button\" className={css.addButton} onClick={startCreate} disabled={busy || readonly}>\n + {t('add')}\n </button>\n </div>\n\n {policy !== null && (\n <div className={css.policyBar}>\n <label className={css.policyField}>\n <span>{t('modeLabel')}</span>\n <select\n value={policy.accessMode}\n disabled={busy}\n onChange={event => {\n const next = event.target.value as 'readonly' | 'ask' | 'auto'\n setBusy(true)\n setMessage(null)\n void setAccessMode(next).then(\n value => { setPolicy(value); setBusy(false) },\n () => { setMessage(t('error')); setBusy(false) },\n )\n }}\n >\n <option value=\"readonly\">{t('modeReadonly')}</option>\n <option value=\"ask\">{t('modeAsk')}</option>\n <option value=\"auto\">{t('modeAuto')}</option>\n </select>\n </label>\n <p className={policy.accessMode === 'readonly' ? css.modeReadonly : policy.accessMode === 'ask' ? css.modeAsk : css.modeAuto}>\n {policy.accessMode === 'readonly'\n ? t('modeReadonlyHint')\n : policy.accessMode === 'ask'\n ? t('modeAskHint')\n : t('modeAutoHint')}\n {policy.autoCapture ? ` · ${t('autoCaptureOn')}` : ` · ${t('autoCaptureOff')}`}\n </p>\n </div>\n )}\n\n {message !== null && <p role=\"alert\" className={css.error}>{message}</p>}\n\n {state.status === 'loading' && <p className={css.status}>{t('loading')}</p>}\n {state.status === 'error' && <p role=\"alert\" className={css.error}>{t('error')}</p>}\n {state.status === 'ready' && state.entries.length === 0 && (\n <div className={css.emptyBox}>\n <p className={css.empty}>{t('empty')}</p>\n <p className={css.emptyHint}>{readonly ? t('emptyHintReadonly') : t('emptyHint')}</p>\n </div>\n )}\n\n {state.status === 'ready' && state.entries.length > 0 && (\n <ul className={css.list}>\n {state.entries.map(entry => {\n const code = codeMap[entry.id]\n return (\n <li key={entry.id} className={css.row}>\n <div className={css.rowMain}>\n <span className={css.title}>{entry.title}</span>\n <span className={css.identity}>{identityLine(entry)}</span>\n {code !== undefined && <span className={css.totp}>{code}</span>}\n </div>\n <div className={css.rowActions}>\n {copiedId === entry.id && <span className={css.copied}>{t('copied')}</span>}\n <button\n type=\"button\"\n onClick={() => void copyValue(entry.id, entry.username ?? entry.title)}\n disabled={busy}\n >{t('copy')}</button>\n <button type=\"button\" onClick={() => void showTotp(entry.id)} disabled={busy}>{t('totp')}</button>\n <button type=\"button\" onClick={() => void startEdit(entry.id)} disabled={busy || readonly}>{t('edit')}</button>\n <button\n type=\"button\"\n className={css.deleteButton}\n onClick={() => void removeEntry(entry.id)}\n disabled={busy || readonly}\n >{t('delete')}</button>\n </div>\n </li>\n )\n })}\n </ul>\n )}\n\n {editor.status !== 'closed' && (\n <div className={css.editor} role=\"dialog\" aria-label={editor.status === 'creating' ? t('add') : t('edit')}>\n <div className={css.editorBody}>\n {FORM_FIELDS.map(field => (\n <label key={field.key} className={css.field}>\n <span>{t(field.label)}</span>\n {field.key === 'kind' ? (\n <select\n value={form.kind ?? 'login'}\n onChange={event => setForm(previous => ({ ...previous, kind: event.target.value }))}\n >\n {Object.entries(KIND_KEYS).map(([value, key]) => (\n <option key={value} value={value}>{t(key)}</option>\n ))}\n </select>\n ) : field.key === 'title' ? (\n <input\n value={form.title ?? ''}\n onChange={event => setForm(previous => ({ ...previous, title: event.target.value }))}\n />\n ) : (\n <input\n type={field.key === 'password' || field.key === 'otpSecret' ? 'password' : 'text'}\n value={(form[field.key] as string | undefined) ?? ''}\n onChange={event => setForm(previous => ({ ...previous, [field.key]: event.target.value }))}\n />\n )}\n </label>\n ))}\n <label className={css.field}>\n <span>{t('fieldTags')}</span>\n <input\n value={tagsDraft}\n onChange={event => setTagsDraft(event.target.value)}\n placeholder=\"dev, prod\"\n />\n </label>\n </div>\n <div className={css.editorActions}>\n <button type=\"button\" onClick={() => setEditor({ status: 'closed' })} disabled={busy}>{t('cancel')}</button>\n <button type=\"button\" className={css.saveButton} onClick={() => void save()} disabled={busy}>{t('save')}</button>\n </div>\n </div>\n )}\n </section>\n )\n}\n","/** Copy dictionaries for the Vault settings section. */\n\n/** English strings (the key-set source of truth for this pair). */\nexport const en = {\n nav: 'Vault',\n heading: 'Vault',\n intro: 'Encrypted credentials stored on this machine.',\n searchPlaceholder: 'Search title, username, email, host…',\n loading: 'Loading…',\n empty: 'No credentials yet. Add one to get started.',\n emptyHint: 'Tip: you can ask the assistant to save an API key or token you share in chat — with auto-capture on it offers to store it for you.',\n emptyHintReadonly: 'The vault is in readonly mode — credentials can be read but not added here. Switch accessMode to \"readwrite\" to enable writes.',\n modeLabel: 'Access mode',\n modeReadonly: 'Read-only',\n modeAsk: 'Ask before writing',\n modeAuto: 'Automatic read-write',\n modeReadonlyHint: 'Read-only — mutations disabled',\n modeAskHint: 'Prompt before writing — every add/update/delete asks you',\n modeAutoHint: 'Automatic read-write — no per-call prompt',\n autoCaptureOn: 'auto-capture on',\n autoCaptureOff: 'auto-capture off',\n add: 'Add credential',\n cancel: 'Cancel',\n save: 'Save',\n edit: 'Edit',\n delete: 'Delete',\n deleteConfirm: 'Delete this entry? This cannot be undone.',\n copy: 'Copy',\n copied: 'Copied',\n totp: 'Code',\n fieldTitle: 'Title',\n fieldKind: 'Kind',\n fieldUsername: 'Username',\n fieldEmail: 'Email',\n fieldPhone: 'Phone',\n fieldPassword: 'Password',\n fieldHost: 'Host',\n fieldPort: 'Port',\n fieldApiKey: 'API key',\n fieldSecret: 'Secret',\n fieldAccessToken: 'Access token',\n fieldRefreshToken: 'Refresh token',\n fieldOtpSecret: 'OTP secret',\n fieldUrl: 'URL',\n fieldNotes: 'Notes',\n fieldTags: 'Tags',\n error: 'Something went wrong.',\n kindLogin: 'Login',\n kindSsh: 'SSH',\n kindApiKey: 'API key',\n kindSecret: 'Secret',\n kindOauth: 'OAuth',\n kindCustom: 'Custom',\n} as const\n\nexport type VaultLocaleKey = keyof typeof en\n\n/** Chinese strings (same keys as English). */\nexport const zh: Record<VaultLocaleKey, string> = {\n nav: '凭据库',\n heading: 'Vault 凭据库',\n intro: '本机加密存储的凭据。',\n searchPlaceholder: '搜索标题、用户名、邮箱、主机…',\n loading: '加载中…',\n empty: '还没有凭据,添加一条开始使用。',\n emptyHint: '提示:你可以在对话中把 API Key 或 token 交给助手保存——开启自动捕获后,它会在你同意时帮你存入本库。',\n emptyHintReadonly: '当前为只读模式——只能读取凭据,不能在此添加。将 accessMode 改为 \"readwrite\" 即可启用写入。',\n modeLabel: '访问模式',\n modeReadonly: '只读',\n modeAsk: '写入前询问',\n modeAuto: '自动读写',\n modeReadonlyHint: '只读 —— 变更已禁用',\n modeAskHint: '写入前询问 —— 每次增/改/删都会征求你的确认',\n modeAutoHint: '自动读写 —— 无需逐次确认',\n autoCaptureOn: '自动捕获已开启',\n autoCaptureOff: '自动捕获已关闭',\n add: '新增凭据',\n cancel: '取消',\n save: '保存',\n edit: '编辑',\n delete: '删除',\n deleteConfirm: '确定删除该条目?此操作不可撤销。',\n copy: '复制',\n copied: '已复制',\n totp: '验证码',\n fieldTitle: '标题',\n fieldKind: '类型',\n fieldUsername: '用户名',\n fieldEmail: '邮箱',\n fieldPhone: '手机号',\n fieldPassword: '密码',\n fieldHost: '主机',\n fieldPort: '端口',\n fieldApiKey: 'API 密钥',\n fieldSecret: 'Secret',\n fieldAccessToken: '访问令牌',\n fieldRefreshToken: '刷新令牌',\n fieldOtpSecret: 'TOTP 密钥',\n fieldUrl: 'URL',\n fieldNotes: '备注',\n fieldTags: '标签',\n error: '操作失败。',\n kindLogin: '登录',\n kindSsh: 'SSH',\n kindApiKey: 'API 密钥',\n kindSecret: 'Secret',\n kindOauth: 'OAuth',\n kindCustom: '自定义',\n}\n","/**\n * dsh-vault browser half: a Vault settings page managing encrypted\n * credentials — list, search, add, edit, delete, and copy — through the host\n * VaultGateway remote over the /api RPC channel.\n *\n * @module dsh-vault/client\n */\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { VaultSection, type VaultSectionInjected } from './VaultSection.tsx'\nimport type { VaultSectionTypes } from './VaultSection.tsx'\nimport { en, zh, type VaultLocaleKey } from './locales.ts'\n\nexport type { VaultSectionInjected, VaultSectionProps } from './VaultSection.tsx'\nexport type { VaultSectionTypes } from './VaultSection.tsx'\nexport type { VaultLocaleKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Vault settings page copy. */\n 'settings.vault': VaultLocaleKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'settings.vault'\n\n/** Services required by the Settings registration and the RPC face. */\nexport const inject = ['slots', 'locale', 'connection']\n\n/**\n * Register the Vault settings section once the `settings.section` declaration\n * is on the ledger. The section talks to the host through the connection RPC\n * channel (endpoints `vault/*` served by the host VaultGateway remote).\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-vault: dictionaries')\n\n const t = ctx.locale.bind(NS)\n const connection = ctx.get('connection') as ConnectionHandle\n\n /** Invoke one host vault remote and normalize the RPC envelope. */\n async function invoke<T>(method: string, args: Record<string, unknown> = {}): Promise<T> {\n const result = await connection.rpc.call('/api', `vault/${method}`, { args })\n if (!result.ok) {\n throw new Error(`vault.${method} failed: ${result.error.code}: ${result.error.message}`)\n }\n return result.value as T\n }\n\n const injected = (): VaultSectionInjected => ({\n t,\n config: () => invoke<VaultSectionTypes['config']>('config'),\n setAccessMode: (mode) => invoke<VaultSectionTypes['config']>('setAccessMode', { mode }),\n list: () => invoke<{ entries: VaultSectionTypes['entries'] }>('list').then(r => r.entries),\n search: (query, limit) => invoke<{ entries: VaultSectionTypes['entries'] }>('search', { query, limit: limit ?? 50 }).then(r => r.entries),\n get: (id) => invoke<{ found: boolean; entry?: VaultSectionTypes['fullEntry'] }>('get', { id }),\n add: (patch) => invoke<VaultSectionTypes['summaryEntry']>('add', { patch }),\n update: (id, patch) => invoke<{ found: boolean; entry?: VaultSectionTypes['summaryEntry'] }>('update', { id, patch }),\n remove: (id) => invoke<{ deleted: boolean }>('delete', { id }),\n totp: (id) => invoke<{ code: string; label?: string; secondsRemaining: number }>('totp', { id }),\n })\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'vault',\n order: 30,\n label: () => t('nav'),\n inject: injected,\n }, VaultSection))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsFA,MAAM,cAAuE;GAC3E;IAAE,KAAK;IAAS,OAAO;GAAa;GACpC;IAAE,KAAK;IAAQ,OAAO;GAAY;GAClC;IAAE,KAAK;IAAY,OAAO;GAAgB;GAC1C;IAAE,KAAK;IAAS,OAAO;GAAa;GACpC;IAAE,KAAK;IAAS,OAAO;GAAa;GACpC;IAAE,KAAK;IAAY,OAAO;GAAgB;GAC1C;IAAE,KAAK;IAAQ,OAAO;GAAY;GAClC;IAAE,KAAK;IAAQ,OAAO;GAAY;GAClC;IAAE,KAAK;IAAU,OAAO;GAAc;GACtC;IAAE,KAAK;IAAU,OAAO;GAAc;GACtC;IAAE,KAAK;IAAe,OAAO;GAAmB;GAChD;IAAE,KAAK;IAAgB,OAAO;GAAoB;GAClD;IAAE,KAAK;IAAa,OAAO;GAAiB;GAC5C;IAAE,KAAK;IAAO,OAAO;GAAW;GAChC;IAAE,KAAK;IAAS,OAAO;GAAa;EACtC;EAEA,MAAM,YAA4C;GAChD,OAAO;GACP,KAAK;GACL,WAAW;GACX,QAAQ;GACR,OAAO;GACP,QAAQ;EACV;EAYA,SAAS,YAAwB;GAC/B,OAAO;IAAE,OAAO;IAAI,MAAM;IAAS,UAAU;IAAI,OAAO;IAAI,OAAO;IAAI,UAAU;IAAI,MAAM;IAAI,MAAM;GAAG;EAC1G;;EAGA,SAAgB,aAAa,OAAqC;GAChE,MAAM,EAAE,GAAG,QAAQ,eAAe,MAAM,QAAQ,KAAK,KAAK,QAAQ,QAAQ,SAAS;GACnF,MAAM,YAAA,GAAA,MAAA,MAAA,CAAiB;GACvB,MAAM,CAAC,OAAO,aAAA,GAAA,MAAA,SAAA,CAAqB,EAAE;GACrC,MAAM,CAAC,OAAO,aAAA,GAAA,MAAA,SAAA,CAAgC,EAAE,QAAQ,UAAU,CAAC;GACnE,MAAM,CAAC,QAAQ,cAAA,GAAA,MAAA,SAAA,CAAmC,EAAE,QAAQ,SAAS,CAAC;GACtE,MAAM,CAAC,MAAM,YAAA,GAAA,MAAA,SAAA,CAAgC,UAAU,CAAC;GACxD,MAAM,CAAC,MAAM,YAAA,GAAA,MAAA,SAAA,CAAoB,KAAK;GACtC,MAAM,CAAC,SAAS,eAAA,GAAA,MAAA,SAAA,CAAsC,IAAI;GAC1D,MAAM,CAAC,UAAU,gBAAA,GAAA,MAAA,SAAA,CAAuC,IAAI;GAC5D,MAAM,CAAC,SAAS,eAAA,GAAA,MAAA,SAAA,CAA+C,CAAC,CAAC;GACjE,MAAM,CAAC,WAAW,iBAAA,GAAA,MAAA,SAAA,CAAyB,EAAE;GAC7C,MAAM,CAAC,QAAQ,cAAA,GAAA,MAAA,SAAA,CAAgG,IAAI;GAEnH,MAAM,WAAW,QAAQ,eAAe;GAExC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,UAAU;IACd,OAAY,CAAC,CAAC,MACZ,UAAS;KAAE,IAAI,SAAS,UAAU,KAAK;IAAE,SACnC,CAAiD,CACzD;IACA,aAAa;KAAE,UAAU;IAAM;GACjC,GAAG,CAAC,MAAM,CAAC;GAEX,MAAM,WAAA,GAAA,MAAA,QAAA,OAAwB,YAAY;IACxC,SAAS,EAAE,QAAQ,UAAU,CAAC;IAC9B,IAAI;KAIF,SAAS;MAAE,QAAQ;MAAS,SAHZ,MAAM,KAAK,CAAC,CAAC,WAAW,IACpC,MAAM,KAAK,IACX,MAAM,OAAO,MAAM,KAAK,CAAC;KACO,CAAC;IACvC,QAAQ;KACN,SAAS,EAAE,QAAQ,QAAQ,CAAC;IAC9B;GACF,GAAG;IAAC;IAAM;IAAQ;GAAK,CAAC;GAExB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,UAAU;IACd,QAAa,QAAQ,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,WAC7B,CAAyC,SACzC;KAAE,IAAI,SAAS,SAAS,EAAE,QAAQ,QAAQ,CAAC;IAAE,CACrD;IACA,aAAa;KAAE,UAAU;IAAM;GAEjC,GAAG,CAAC,OAAO,CAAC;;GAGZ,SAAS,cAAoB;IAC3B,QAAQ,UAAU,CAAC;IACnB,aAAa,EAAE;IACf,WAAW,IAAI;IACf,UAAU,EAAE,QAAQ,WAAW,CAAC;GAClC;;GAGA,eAAe,UAAU,IAA2B;IAClD,QAAQ,IAAI;IACZ,WAAW,IAAI;IACf,IAAI;KACF,MAAM,SAAS,MAAM,IAAI,EAAE;KAC3B,IAAI,CAAC,OAAO,SAAS,OAAO,UAAU,KAAA,GAAW;MAC/C,WAAW,EAAE,OAAO,CAAC;MACrB;KACF;KACA,MAAM,QAAQ,OAAO;KACrB,QAAQ;MACN,OAAO,MAAM,SAAS;MACtB,MAAM,MAAM,QAAQ;MACpB,UAAU,MAAM,YAAY;MAC5B,OAAO,MAAM,SAAS;MACtB,OAAO,MAAM,SAAS;MACtB,UAAU,MAAM,YAAY;MAC5B,MAAM,MAAM,QAAQ;MACpB,MAAM,MAAM,QAAQ;MACpB,YAAY,MAAM,cAAc;MAChC,QAAQ,MAAM,UAAU;MACxB,QAAQ,MAAM,UAAU;MACxB,aAAa,MAAM,eAAe;MAClC,cAAc,MAAM,gBAAgB;MACpC,WAAW,MAAM,aAAa;MAC9B,KAAK,MAAM,OAAO;MAClB,OAAO,MAAM,SAAS;KACxB,CAAC;KACD,cAAc,MAAM,QAAQ,CAAC,EAAA,CAAG,KAAK,IAAI,CAAC;KAC1C,UAAU;MAAE,QAAQ;MAAW;KAAM,CAAC;IACxC,QAAQ;KACN,WAAW,EAAE,OAAO,CAAC;IACvB,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,eAAe,OAAsB;IACnC,IAAI,CAAC,KAAK,MAAM,KAAK,GAAG;KACtB,WAAW,EAAE,OAAO,CAAC;KACrB;IACF;IACA,QAAQ,IAAI;IACZ,WAAW,IAAI;IACf,IAAI;KACF,MAAM,OAAO,UAAU,MAAM,GAAG,CAAC,CAAC,KAAI,MAAK,EAAE,KAAK,CAAC,CAAC,CAAC,QAAO,MAAK,EAAE,SAAS,CAAC;KAC7E,MAAM,QAAoB;MACxB,OAAO,KAAK,MAAM,KAAK;MACvB,GAAI,KAAK,SAAS,KAAA,IAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;MACrD,GAAI,KAAK,aAAa,KAAA,IAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;MACjE,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;MACxD,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;MACxD,GAAI,KAAK,aAAa,KAAA,IAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;MACjE,GAAI,KAAK,SAAS,KAAA,IAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;MACrD,GAAI,KAAK,SAAS,KAAA,IAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;MACrD,GAAI,KAAK,eAAe,KAAA,IAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;MACvE,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;MAC3D,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;MAC3D,GAAI,KAAK,gBAAgB,KAAA,IAAY,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC;MAC1E,GAAI,KAAK,iBAAiB,KAAA,IAAY,EAAE,cAAc,KAAK,aAAa,IAAI,CAAC;MAC7E,GAAI,KAAK,cAAc,KAAA,IAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;MACpE,GAAI,KAAK,QAAQ,KAAA,IAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;MAClD,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;MAGxD;KACF;KACA,IAAI,OAAO,WAAW,YACpB,MAAM,IAAI,KAAuC;UAC5C,IAAI,OAAO,WAAW,WAC3B,MAAM,OAAO,OAAO,MAAM,IAAI,KAAK;KAErC,UAAU,EAAE,QAAQ,SAAS,CAAC;KAC9B,MAAM,QAAQ;IAChB,QAAQ;KACN,WAAW,EAAE,OAAO,CAAC;IACvB,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,eAAe,YAAY,IAA2B;IACpD,IAAI,CAAC,OAAO,QAAQ,EAAE,eAAe,CAAC,GAAG;IACzC,QAAQ,IAAI;IACZ,IAAI;KACF,MAAM,OAAO,EAAE;KACf,MAAM,QAAQ;IAChB,QAAQ;KACN,WAAW,EAAE,OAAO,CAAC;IACvB,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,eAAe,UAAU,IAAY,OAA8B;IACjE,IAAI;KACF,MAAM,UAAU,UAAU,UAAU,KAAK;IAC3C,QAAQ;KAEN,MAAM,WAAW,SAAS,cAAc,UAAU;KAClD,SAAS,QAAQ;KACjB,SAAS,KAAK,YAAY,QAAQ;KAClC,SAAS,OAAO;KAChB,SAAS,YAAY,MAAM;KAC3B,SAAS,OAAO;IAClB;IACA,YAAY,EAAE;IACd,OAAO,iBAAiB,YAAY,IAAI,GAAG,IAAI;GACjD;;GAGA,eAAe,SAAS,IAA2B;IACjD,QAAQ,IAAI;IACZ,IAAI;KACF,MAAM,SAAS,MAAM,KAAK,EAAE;KAC5B,YAAW,cAAa;MAAE,GAAG;OAAW,KAAK,GAAG,OAAO,KAAK,IAAI,OAAO,iBAAiB;KAAI,EAAE;IAChG,QAAQ;KACN,YAAW,cAAa;MAAE,GAAG;OAAW,KAAK,EAAE,OAAO;KAAE,EAAE;IAC5D,UAAU;KACR,QAAQ,KAAK;IACf;GACF;;GAGA,SAAS,aAAa,OAAiC;IASrD,OARc;KACZ,MAAM,SAAS,KAAA,IAAY,EAAE,UAAU,MAAM,SAAS,YAAY,IAAI,EAAE,WAAW;KACnF,MAAM;KACN,MAAM;KACN,MAAM;KACN,MAAM,SAAS,KAAA,IAAY,GAAG,MAAM,OAAO,MAAM,SAAS,KAAA,IAAY,IAAI,MAAM,SAAS,OAAO,KAAA;KAChG,MAAM;IACR,CAAC,CAAC,QAAQ,UAA2B,UAAU,KAAA,KAAa,MAAM,SAAS,CAChE,CAAC,CAAC,KAAK,KAAK;GACzB;GAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;IAAS,WAAWA,gCAAI;IAAS,mBAAgB;cAAjD;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;MAAI,IAAG;gBAAiB,EAAE,SAAS;KAAM,CAAA;KACzC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,WAAWA,gCAAI;gBAAQ,EAAE,OAAO;KAAK,CAAA;KAExC,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,gCAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;OAAO,WAAWA,gCAAI;iBAAtB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;kBAAW,EAAE,mBAAmB;OAAQ,CAAA,GACxD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;QACE,IAAI;QACJ,MAAK;QACL,aAAa,EAAE,mBAAmB;QAClC,OAAO;QACP,WAAU,UAAS,SAAS,MAAM,OAAO,KAAK;OAC/C,CAAA,CACI;UACP,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;OAAQ,MAAK;OAAS,WAAWA,gCAAI;OAAW,SAAS;OAAa,UAAU,QAAQ;iBAAxF,CAAkG,MAC7F,EAAE,KAAK,CACJ;QACL;;KAEJ,WAAW,QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,gCAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;OAAO,WAAWA,gCAAI;iBAAtB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,WAAW,EAAQ,CAAA,GAC5B,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;QACE,OAAO,OAAO;QACd,UAAU;QACV,WAAU,UAAS;SACjB,MAAM,OAAO,MAAM,OAAO;SAC1B,QAAQ,IAAI;SACZ,WAAW,IAAI;SACf,cAAmB,IAAI,CAAC,CAAC,MACvB,UAAS;UAAE,UAAU,KAAK;UAAG,QAAQ,KAAK;SAAE,SACtC;UAAE,WAAW,EAAE,OAAO,CAAC;UAAG,QAAQ,KAAK;SAAE,CACjD;QACF;kBAXF;SAaE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,OAAM;oBAAY,EAAE,cAAc;SAAU,CAAA;SACpD,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,OAAM;oBAAO,EAAE,SAAS;SAAU,CAAA;SAC1C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,OAAM;oBAAQ,EAAE,UAAU;SAAU,CAAA;QACtC;SACH;UACP,iBAAA,GAAA,kBAAA,KAAA,CAAC,KAAD;OAAG,WAAW,OAAO,eAAe,aAAaA,gCAAI,eAAe,OAAO,eAAe,QAAQA,gCAAI,UAAUA,gCAAI;iBAApH,CACG,OAAO,eAAe,aACnB,EAAE,kBAAkB,IACpB,OAAO,eAAe,QACpB,EAAE,aAAa,IACf,EAAE,cAAc,GACrB,OAAO,cAAc,MAAM,EAAE,eAAe,MAAM,MAAM,EAAE,gBAAgB,GAC1E;QACA;;KAGN,YAAY,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,MAAK;MAAQ,WAAWA,gCAAI;gBAAQ;KAAW,CAAA;KAEtE,MAAM,WAAW,aAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,WAAWA,gCAAI;gBAAS,EAAE,SAAS;KAAK,CAAA;KACzE,MAAM,WAAW,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,MAAK;MAAQ,WAAWA,gCAAI;gBAAQ,EAAE,OAAO;KAAK,CAAA;KACjF,MAAM,WAAW,WAAW,MAAM,QAAQ,WAAW,KACpD,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,gCAAI;gBAApB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAWA,gCAAI;iBAAQ,EAAE,OAAO;MAAK,CAAA,GACxC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAWA,gCAAI;iBAAY,WAAW,EAAE,mBAAmB,IAAI,EAAE,WAAW;MAAK,CAAA,CACjF;;KAGN,MAAM,WAAW,WAAW,MAAM,QAAQ,SAAS,KAClD,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;MAAI,WAAWA,gCAAI;gBAChB,MAAM,QAAQ,KAAI,UAAS;OAC1B,MAAM,OAAO,QAAQ,MAAM;OAC3B,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;QAAmB,WAAWA,gCAAI;kBAAlC,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,gCAAI;mBAApB;UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,gCAAI;qBAAQ,MAAM;UAAY,CAAA;UAC/C,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,gCAAI;qBAAW,aAAa,KAAK;UAAQ,CAAA;UACzD,SAAS,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,gCAAI;qBAAO;UAAW,CAAA;SAC3D;YACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAWA,gCAAI;mBAApB;UACG,aAAa,MAAM,MAAM,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;WAAM,WAAWA,gCAAI;qBAAS,EAAE,QAAQ;UAAQ,CAAA;UAC1E,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WACE,MAAK;WACL,eAAe,KAAK,UAAU,MAAM,IAAI,MAAM,YAAY,MAAM,KAAK;WACrE,UAAU;qBACV,EAAE,MAAM;UAAU,CAAA;UACpB,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WAAQ,MAAK;WAAS,eAAe,KAAK,SAAS,MAAM,EAAE;WAAG,UAAU;qBAAO,EAAE,MAAM;UAAU,CAAA;UACjG,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WAAQ,MAAK;WAAS,eAAe,KAAK,UAAU,MAAM,EAAE;WAAG,UAAU,QAAQ;qBAAW,EAAE,MAAM;UAAU,CAAA;UAC9G,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WACE,MAAK;WACL,WAAWA,gCAAI;WACf,eAAe,KAAK,YAAY,MAAM,EAAE;WACxC,UAAU,QAAQ;qBAClB,EAAE,QAAQ;UAAU,CAAA;SACnB;UACH;UAtBK,MAAM,EAsBX;MAER,CAAC;KACC,CAAA;KAGL,OAAO,WAAW,YACjB,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAWA,gCAAI;MAAQ,MAAK;MAAS,cAAY,OAAO,WAAW,aAAa,EAAE,KAAK,IAAI,EAAE,MAAM;gBAAxG,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAWA,gCAAI;iBAApB,CACG,YAAY,KAAI,UACf,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;QAAuB,WAAWA,gCAAI;kBAAtC,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,MAAM,KAAK,EAAQ,CAAA,GAC3B,MAAM,QAAQ,SACb,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;SACE,OAAO,KAAK,QAAQ;SACpB,WAAU,UAAS,SAAQ,cAAa;UAAE,GAAG;UAAU,MAAM,MAAM,OAAO;SAAM,EAAE;mBAEjF,OAAO,QAAQ,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,SACtC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAA2B;oBAAQ,EAAE,GAAG;SAAU,GAArC,KAAqC,CACnD;QACK,CAAA,IACN,MAAM,QAAQ,UAChB,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;SACE,OAAO,KAAK,SAAS;SACrB,WAAU,UAAS,SAAQ,cAAa;UAAE,GAAG;UAAU,OAAO,MAAM,OAAO;SAAM,EAAE;QACpF,CAAA,IAED,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;SACE,MAAM,MAAM,QAAQ,cAAc,MAAM,QAAQ,cAAc,aAAa;SAC3E,OAAQ,KAAK,MAAM,QAA+B;SAClD,WAAU,UAAS,SAAQ,cAAa;UAAE,GAAG;WAAW,MAAM,MAAM,MAAM,OAAO;SAAM,EAAE;QAC1F,CAAA,CAEE;UAvBK,MAAM,GAuBX,CACR,GACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;QAAO,WAAWA,gCAAI;kBAAtB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,EAAE,WAAW,EAAQ,CAAA,GAC5B,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;SACE,OAAO;SACP,WAAU,UAAS,aAAa,MAAM,OAAO,KAAK;SAClD,aAAY;QACb,CAAA,CACI;SACJ;UACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAWA,gCAAI;iBAApB,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QAAQ,MAAK;QAAS,eAAe,UAAU,EAAE,QAAQ,SAAS,CAAC;QAAG,UAAU;kBAAO,EAAE,QAAQ;OAAU,CAAA,GAC3G,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QAAQ,MAAK;QAAS,WAAWA,gCAAI;QAAY,eAAe,KAAK,KAAK;QAAG,UAAU;kBAAO,EAAE,MAAM;OAAU,CAAA,CAC7G;QACF;;IAEA;;EAEb;;;;;EChdA,MAAa,KAAK;GAChB,KAAK;GACL,SAAS;GACT,OAAO;GACP,mBAAmB;GACnB,SAAS;GACT,OAAO;GACP,WAAW;GACX,mBAAmB;GACnB,WAAW;GACX,cAAc;GACd,SAAS;GACT,UAAU;GACV,kBAAkB;GAClB,aAAa;GACb,cAAc;GACd,eAAe;GACf,gBAAgB;GAChB,KAAK;GACL,QAAQ;GACR,MAAM;GACN,MAAM;GACN,QAAQ;GACR,eAAe;GACf,MAAM;GACN,QAAQ;GACR,MAAM;GACN,YAAY;GACZ,WAAW;GACX,eAAe;GACf,YAAY;GACZ,YAAY;GACZ,eAAe;GACf,WAAW;GACX,WAAW;GACX,aAAa;GACb,aAAa;GACb,kBAAkB;GAClB,mBAAmB;GACnB,gBAAgB;GAChB,UAAU;GACV,YAAY;GACZ,WAAW;GACX,OAAO;GACP,WAAW;GACX,SAAS;GACT,YAAY;GACZ,YAAY;GACZ,WAAW;GACX,YAAY;EACd;;EAKA,MAAa,KAAqC;GAChD,KAAK;GACL,SAAS;GACT,OAAO;GACP,mBAAmB;GACnB,SAAS;GACT,OAAO;GACP,WAAW;GACX,mBAAmB;GACnB,WAAW;GACX,cAAc;GACd,SAAS;GACT,UAAU;GACV,kBAAkB;GAClB,aAAa;GACb,cAAc;GACd,eAAe;GACf,gBAAgB;GAChB,KAAK;GACL,QAAQ;GACR,MAAM;GACN,MAAM;GACN,QAAQ;GACR,eAAe;GACf,MAAM;GACN,QAAQ;GACR,MAAM;GACN,YAAY;GACZ,WAAW;GACX,eAAe;GACf,YAAY;GACZ,YAAY;GACZ,eAAe;GACf,WAAW;GACX,WAAW;GACX,aAAa;GACb,aAAa;GACb,kBAAkB;GAClB,mBAAmB;GACnB,gBAAgB;GAChB,UAAU;GACV,YAAY;GACZ,WAAW;GACX,OAAO;GACP,WAAW;GACX,SAAS;GACT,YAAY;GACZ,YAAY;GACZ,WAAW;GACX,YAAY;EACd;;;;EChFA,MAAa,KAAK;;EAGlB,MAAa,SAAS;GAAC;GAAS;GAAU;EAAY;;;;;;EAOtD,SAAgB,MAAM,KAA0B;GAC9C,IAAI,aAAa,IAAI,OAAO,SAAS,IAAI;IAAE;IAAI;GAAG,CAAC,GAAG,yBAAyB;GAE/E,MAAM,IAAI,IAAI,OAAO,KAAK,EAAE;GAC5B,MAAM,aAAa,IAAI,IAAI,YAAY;;GAGvC,eAAe,OAAU,QAAgB,OAAgC,CAAC,GAAe;IACvF,MAAM,SAAS,MAAM,WAAW,IAAI,KAAK,QAAQ,SAAS,UAAU,EAAE,KAAK,CAAC;IAC5E,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MAAM,SAAS,OAAO,WAAW,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM,SAAS;IAEzF,OAAO,OAAO;GAChB;GAEA,MAAM,kBAAwC;IAC5C;IACA,cAAc,OAAoC,QAAQ;IAC1D,gBAAgB,SAAS,OAAoC,iBAAiB,EAAE,KAAK,CAAC;IACtF,YAAY,OAAkD,MAAM,CAAC,CAAC,MAAK,MAAK,EAAE,OAAO;IACzF,SAAS,OAAO,UAAU,OAAkD,UAAU;KAAE;KAAO,OAAO,SAAS;IAAG,CAAC,CAAC,CAAC,MAAK,MAAK,EAAE,OAAO;IACxI,MAAM,OAAO,OAAmE,OAAO,EAAE,GAAG,CAAC;IAC7F,MAAM,UAAU,OAA0C,OAAO,EAAE,MAAM,CAAC;IAC1E,SAAS,IAAI,UAAU,OAAsE,UAAU;KAAE;KAAI;IAAM,CAAC;IACpH,SAAS,OAAO,OAA6B,UAAU,EAAE,GAAG,CAAC;IAC7D,OAAO,OAAO,OAAmE,QAAQ,EAAE,GAAG,CAAC;GACjG;GAEA,IAAI,MAAM,OAAO,0BAA0B,IAAI,MAAM,SAAS;IAC5D,MAAM;IACN,IAAI;IACJ,OAAO;IACP,aAAa,EAAE,KAAK;IACpB,QAAQ;GACV,GAAG,YAAY,CAAC;EAClB"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["css"],"sources":["../src/client/VaultSection.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Vault settings section: list, search, add, edit, delete, and copy encrypted\n * credentials. All mutations go through the host VaultGateway remote; secrets\n * are shown only inside the edit form or a copy action, never in the list.\n */\n\nimport { useEffect, useId, useMemo, useState, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime, InjectFace, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { VaultLocaleKey } from './locales.ts'\nimport css from './VaultSection.module.css'\n\n/** Wire shapes shared with the host gateway (mirror of src/index.ts types). */\nexport interface VaultSummaryWire {\n id: string\n title: string\n kind?: string\n username?: string\n email?: string\n phone?: string\n host?: string\n port?: string\n url?: string\n tags?: string[]\n}\n\nexport interface VaultFullWire {\n id: string\n title: string\n kind?: string\n username?: string\n email?: string\n phone?: string\n password?: string\n host?: string\n port?: string\n privateKey?: string\n apiKey?: string\n secret?: string\n accessToken?: string\n refreshToken?: string\n expiresAt?: number\n otpSecret?: string\n url?: string\n notes?: string\n tags?: string[]\n fields?: Record<string, unknown>\n}\n\n/** Patch shape accepted by vault.add / vault.update. */\nexport type VaultPatch = Partial<Omit<VaultFullWire, 'id'>>\n\n/** Registration-side business face supplied by the plugin entry. */\nexport interface VaultSectionInjected {\n t: TranslateNS<'settings.vault'>\n config: () => Promise<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }>\n setAccessMode: (mode: 'readonly' | 'ask' | 'auto') => Promise<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }>\n list: () => Promise<VaultSummaryWire[]>\n search: (query: string, limit?: number) => Promise<VaultSummaryWire[]>\n get: (id: string) => Promise<{ found: boolean; entry?: VaultFullWire }>\n add: (patch: VaultPatch & { title: string }) => Promise<VaultSummaryWire>\n update: (id: string, patch: VaultPatch) => Promise<{ found: boolean; entry?: VaultSummaryWire }>\n remove: (id: string) => Promise<{ deleted: boolean }>\n trash: () => Promise<VaultSummaryWire[]>\n rotation: () => Promise<unknown[]>\n health: () => Promise<{ weak: unknown[]; reused: unknown[] }>\n restore: (id: string) => Promise<{ restored: boolean }>\n totp: (id: string) => Promise<{ code: string; label?: string; secondsRemaining: number }>\n}\n\n/** Type-level alias so consumers can reference the wire shapes without values. */\nexport type VaultSectionTypes = {\n entries: VaultSummaryWire[]\n fullEntry: VaultFullWire\n summaryEntry: VaultSummaryWire\n config: { accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean }\n accessModes: Array<'readonly' | 'ask' | 'auto'>\n}\n\n/** Full component props assembled by the Settings slot renderer. */\nexport type VaultSectionProps =\n PropsRuntime<'settings.section'>\n & PropsLocale<'settings.vault'>\n & InjectFace<VaultSectionInjected>\n\n/** The editable form fields we render (subset of the full wire). */\ntype FormFields = Pick<\n VaultFullWire, 'title' | 'kind' | 'username' | 'email' | 'phone' | 'password' | 'host' | 'port'\n | 'privateKey' | 'apiKey' | 'secret' | 'accessToken' | 'refreshToken' | 'otpSecret' | 'url' | 'notes'\n>\n\nconst FORM_FIELDS: Array<{ key: keyof FormFields; label: VaultLocaleKey }> = [\n { key: 'title', label: 'fieldTitle' },\n { key: 'kind', label: 'fieldKind' },\n { key: 'username', label: 'fieldUsername' },\n { key: 'email', label: 'fieldEmail' },\n { key: 'phone', label: 'fieldPhone' },\n { key: 'password', label: 'fieldPassword' },\n { key: 'host', label: 'fieldHost' },\n { key: 'port', label: 'fieldPort' },\n { key: 'apiKey', label: 'fieldApiKey' },\n { key: 'secret', label: 'fieldSecret' },\n { key: 'accessToken', label: 'fieldAccessToken' },\n { key: 'refreshToken', label: 'fieldRefreshToken' },\n { key: 'otpSecret', label: 'fieldOtpSecret' },\n { key: 'url', label: 'fieldUrl' },\n { key: 'notes', label: 'fieldNotes' },\n]\n\nconst KIND_KEYS: Record<string, VaultLocaleKey> = {\n login: 'kindLogin',\n ssh: 'kindSsh',\n 'api-key': 'kindApiKey',\n secret: 'kindSecret',\n oauth: 'kindOauth',\n custom: 'kindCustom',\n}\n\ntype ViewState =\n | { readonly status: 'loading' }\n | { readonly status: 'error' }\n | { readonly status: 'ready'; readonly entries: VaultSummaryWire[] }\n\ntype EditorState =\n | { readonly status: 'closed' }\n | { readonly status: 'creating' }\n | { readonly status: 'editing'; readonly entry: VaultFullWire }\n\nfunction emptyForm(): FormFields {\n return { title: '', kind: 'login', username: '', email: '', phone: '', password: '', host: '', port: '' }\n}\n\n/** Render the Vault settings section. */\nexport function VaultSection(props: VaultSectionProps): ReactNode {\n const { t, config, setAccessMode, list, search, get, add, update, remove, trash, rotation, health, restore, totp } = props\n const searchId = useId()\n const [query, setQuery] = useState('')\n const [state, setState] = useState<ViewState>({ status: 'loading' })\n const [editor, setEditor] = useState<EditorState>({ status: 'closed' })\n const [form, setForm] = useState<FormFields>(emptyForm())\n const [busy, setBusy] = useState(false)\n const [message, setMessage] = useState<string | null>(null)\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [codeMap, setCodeMap] = useState<Record<string, string>>({})\n const [tagsDraft, setTagsDraft] = useState('')\n const [policy, setPolicy] = useState<{ accessMode: 'readonly' | 'ask' | 'auto'; autoCapture: boolean } | null>(null)\n const [showTrash, setShowTrash] = useState(false)\n const [trashEntries, setTrashEntries] = useState<VaultSummaryWire[]>([])\n const [report, setReport] = useState<{ rotation: unknown[]; weak: unknown[]; reused: unknown[] } | null>(null)\n\n const readonly = policy?.accessMode === 'readonly'\n\n useEffect(() => {\n let current = true\n void config().then(\n value => { if (current) setPolicy(value) },\n () => { /* policy is informational; ignore failures */ },\n )\n return () => { current = false }\n }, [config])\n\n const refresh = useMemo(() => async () => {\n setState({ status: 'loading' })\n try {\n const entries = query.trim().length === 0\n ? await list()\n : await search(query.trim())\n setState({ status: 'ready', entries })\n } catch {\n setState({ status: 'error' })\n }\n }, [list, search, query])\n\n useEffect(() => {\n let current = true\n void Promise.resolve().then(refresh).then(\n () => { /* state already set inside refresh */ },\n () => { if (current) setState({ status: 'error' }) },\n )\n return () => { current = false }\n // refresh is memoized on query; list/search are stable inject faces.\n }, [refresh])\n\n /** Open the editor for a new entry. */\n function startCreate(): void {\n setForm(emptyForm())\n setTagsDraft('')\n setMessage(null)\n setEditor({ status: 'creating' })\n }\n\n /** Open the editor for an existing entry (fetches full secrets). */\n async function startEdit(id: string): Promise<void> {\n setBusy(true)\n setMessage(null)\n try {\n const result = await get(id)\n if (!result.found || result.entry === undefined) {\n setMessage(t('error'))\n return\n }\n const entry = result.entry\n setForm({\n title: entry.title ?? '',\n kind: entry.kind ?? 'login',\n username: entry.username ?? '',\n email: entry.email ?? '',\n phone: entry.phone ?? '',\n password: entry.password ?? '',\n host: entry.host ?? '',\n port: entry.port ?? '',\n privateKey: entry.privateKey ?? '',\n apiKey: entry.apiKey ?? '',\n secret: entry.secret ?? '',\n accessToken: entry.accessToken ?? '',\n refreshToken: entry.refreshToken ?? '',\n otpSecret: entry.otpSecret ?? '',\n url: entry.url ?? '',\n notes: entry.notes ?? '',\n })\n setTagsDraft((entry.tags ?? []).join(', '))\n setEditor({ status: 'editing', entry })\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Save the current form (create or update). */\n async function save(): Promise<void> {\n if (!form.title.trim()) {\n setMessage(t('error'))\n return\n }\n setBusy(true)\n setMessage(null)\n try {\n const tags = tagsDraft.split(',').map(x => x.trim()).filter(x => x.length > 0)\n const patch: VaultPatch = {\n title: form.title.trim(),\n ...(form.kind !== undefined ? { kind: form.kind } : {}),\n ...(form.username !== undefined ? { username: form.username } : {}),\n ...(form.email !== undefined ? { email: form.email } : {}),\n ...(form.phone !== undefined ? { phone: form.phone } : {}),\n ...(form.password !== undefined ? { password: form.password } : {}),\n ...(form.host !== undefined ? { host: form.host } : {}),\n ...(form.port !== undefined ? { port: form.port } : {}),\n ...(form.privateKey !== undefined ? { privateKey: form.privateKey } : {}),\n ...(form.apiKey !== undefined ? { apiKey: form.apiKey } : {}),\n ...(form.secret !== undefined ? { secret: form.secret } : {}),\n ...(form.accessToken !== undefined ? { accessToken: form.accessToken } : {}),\n ...(form.refreshToken !== undefined ? { refreshToken: form.refreshToken } : {}),\n ...(form.otpSecret !== undefined ? { otpSecret: form.otpSecret } : {}),\n ...(form.url !== undefined ? { url: form.url } : {}),\n ...(form.notes !== undefined ? { notes: form.notes } : {}),\n // Always send tags: an empty array clears them, and the host store\n // treats empty string values as \"clear this field\" too.\n tags,\n }\n if (editor.status === 'creating') {\n await add(patch as VaultPatch & { title: string })\n } else if (editor.status === 'editing') {\n await update(editor.entry.id, patch)\n }\n setEditor({ status: 'closed' })\n await refresh()\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Delete an entry after confirmation. */\n async function removeEntry(id: string): Promise<void> {\n if (!window.confirm(t('deleteConfirm'))) return\n setBusy(true)\n try {\n await remove(id)\n await refresh()\n } catch {\n setMessage(t('error'))\n } finally {\n setBusy(false)\n }\n }\n\n /** Copy a field value to the clipboard and flash the row. */\n async function copyValue(id: string, value: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(value)\n } catch {\n // Clipboard may be blocked; fall back to a temporary textarea.\n const textarea = document.createElement('textarea')\n textarea.value = value\n document.body.appendChild(textarea)\n textarea.select()\n document.execCommand('copy')\n textarea.remove()\n }\n setCopiedId(id)\n window.setTimeout(() => setCopiedId(null), 1500)\n }\n\n /** Fetch and display a TOTP code for an entry with an otpSecret. */\n async function showTotp(id: string): Promise<void> {\n setBusy(true)\n try {\n const result = await totp(id)\n setCodeMap(previous => ({ ...previous, [id]: `${result.code} (${result.secondsRemaining}s)` }))\n } catch {\n setCodeMap(previous => ({ ...previous, [id]: t('error') }))\n } finally {\n setBusy(false)\n }\n }\n\n /** Field-set summary line for one entry (non-secret). */\n function identityLine(entry: VaultSummaryWire): string {\n const parts = [\n entry.kind !== undefined ? t(KIND_KEYS[entry.kind] ?? 'kindCustom') : t('kindLogin'),\n entry.username,\n entry.email,\n entry.phone,\n entry.host !== undefined ? `${entry.host}${entry.port !== undefined ? `:${entry.port}` : ''}` : undefined,\n entry.url,\n ].filter((value): value is string => value !== undefined && value.length > 0)\n return parts.join(' · ')\n }\n\n return (\n <section className={css.section} aria-labelledby=\"vault-heading\">\n <h2 id=\"vault-heading\">{t('heading')}</h2>\n <p className={css.intro}>{t('intro')}</p>\n\n <div className={css.toolbar}>\n <label className={css.searchBox}>\n <span className=\"sr-only\">{t('searchPlaceholder')}</span>\n <input\n id={searchId}\n type=\"search\"\n placeholder={t('searchPlaceholder')}\n value={query}\n onChange={event => setQuery(event.target.value)}\n />\n </label>\n <button type=\"button\" className={css.addButton} onClick={startCreate} disabled={busy || readonly}>\n + {t('add')}\n </button>\n </div>\n\n {policy !== null && (\n <div className={css.policyBar}>\n <label className={css.policyField}>\n <span>{t('modeLabel')}</span>\n <select\n value={policy.accessMode}\n disabled={busy}\n onChange={event => {\n const next = event.target.value as 'readonly' | 'ask' | 'auto'\n setBusy(true)\n setMessage(null)\n void setAccessMode(next).then(\n value => { setPolicy(value); setBusy(false) },\n () => { setMessage(t('error')); setBusy(false) },\n )\n }}\n >\n <option value=\"readonly\">{t('modeReadonly')}</option>\n <option value=\"ask\">{t('modeAsk')}</option>\n <option value=\"auto\">{t('modeAuto')}</option>\n </select>\n </label>\n <p className={policy.accessMode === 'readonly' ? css.modeReadonly : policy.accessMode === 'ask' ? css.modeAsk : css.modeAuto}>\n {policy.accessMode === 'readonly'\n ? t('modeReadonlyHint')\n : policy.accessMode === 'ask'\n ? t('modeAskHint')\n : t('modeAutoHint')}\n {policy.autoCapture ? ` · ${t('autoCaptureOn')}` : ` · ${t('autoCaptureOff')}`}\n </p>\n </div>\n )}\n\n {message !== null && <p role=\"alert\" className={css.error}>{message}</p>}\n\n {state.status === 'loading' && <p className={css.status}>{t('loading')}</p>}\n {state.status === 'error' && <p role=\"alert\" className={css.error}>{t('error')}</p>}\n {state.status === 'ready' && state.entries.length === 0 && (\n <div className={css.emptyBox}>\n <p className={css.empty}>{t('empty')}</p>\n <p className={css.emptyHint}>{readonly ? t('emptyHintReadonly') : t('emptyHint')}</p>\n </div>\n )}\n\n {report !== null && (report.rotation.length > 0 || report.weak.length > 0 || report.reused.length > 0) && (\n <div className={css.reportBox}>\n <p className={css.reportTitle}>{t('reportTitle')}</p>\n {report.rotation.length > 0 && (\n <p className={css.reportLine}>{t('reportRotation')}: {report.rotation.length}</p>\n )}\n {report.weak.length > 0 && (\n <p className={css.reportLine}>{t('reportWeak')}: {report.weak.length}</p>\n )}\n {report.reused.length > 0 && (\n <p className={css.reportLine}>{t('reportReused')}: {report.reused.length}</p>\n )}\n </div>\n )}\n\n <button\n type=\"button\"\n className={css.trashButton}\n onClick={() => {\n if (!showTrash) void trash().then(setTrashEntries)\n setShowTrash(!showTrash)\n }}\n >{showTrash ? t('hideTrash') : t('showTrash')}{trashEntries.length > 0 ? ` (${trashEntries.length})` : ''}</button>\n\n {showTrash && (\n <ul className={css.list}>\n {trashEntries.map(entry => (\n <li key={entry.id} className={css.row}>\n <div className={css.rowMain}>\n <span className={css.title}>{entry.title}</span>\n <span className={css.identity}>{t('trashed')}</span>\n </div>\n <div className={css.rowActions}>\n <button\n type=\"button\"\n onClick={() => { void restore(entry.id).then(() => { void trash().then(setTrashEntries); void refresh() }) }}\n disabled={busy || readonly}\n >{t('restore')}</button>\n </div>\n </li>\n ))}\n {trashEntries.length === 0 && <p className={css.empty}>{t('trashEmpty')}</p>}\n </ul>\n )}\n\n {state.status === 'ready' && state.entries.length > 0 && (\n <ul className={css.list}>\n {state.entries.map(entry => {\n const code = codeMap[entry.id]\n return (\n <li key={entry.id} className={css.row}>\n <div className={css.rowMain}>\n <span className={css.title}>{entry.title}</span>\n <span className={css.identity}>{identityLine(entry)}</span>\n {code !== undefined && <span className={css.totp}>{code}</span>}\n </div>\n <div className={css.rowActions}>\n {copiedId === entry.id && <span className={css.copied}>{t('copied')}</span>}\n <button\n type=\"button\"\n onClick={() => void copyValue(entry.id, entry.username ?? entry.title)}\n disabled={busy}\n >{t('copy')}</button>\n <button type=\"button\" onClick={() => void showTotp(entry.id)} disabled={busy}>{t('totp')}</button>\n <button type=\"button\" onClick={() => void startEdit(entry.id)} disabled={busy || readonly}>{t('edit')}</button>\n <button\n type=\"button\"\n className={css.deleteButton}\n onClick={() => void removeEntry(entry.id)}\n disabled={busy || readonly}\n >{t('delete')}</button>\n </div>\n </li>\n )\n })}\n </ul>\n )}\n\n {editor.status !== 'closed' && (\n <div className={css.editor} role=\"dialog\" aria-label={editor.status === 'creating' ? t('add') : t('edit')}>\n <div className={css.editorBody}>\n {FORM_FIELDS.map(field => (\n <label key={field.key} className={css.field}>\n <span>{t(field.label)}</span>\n {field.key === 'kind' ? (\n <select\n value={form.kind ?? 'login'}\n onChange={event => setForm(previous => ({ ...previous, kind: event.target.value }))}\n >\n {Object.entries(KIND_KEYS).map(([value, key]) => (\n <option key={value} value={value}>{t(key)}</option>\n ))}\n </select>\n ) : field.key === 'title' ? (\n <input\n value={form.title ?? ''}\n onChange={event => setForm(previous => ({ ...previous, title: event.target.value }))}\n />\n ) : (\n <input\n type={field.key === 'password' || field.key === 'otpSecret' ? 'password' : 'text'}\n value={(form[field.key] as string | undefined) ?? ''}\n onChange={event => setForm(previous => ({ ...previous, [field.key]: event.target.value }))}\n />\n )}\n </label>\n ))}\n <label className={css.field}>\n <span>{t('fieldTags')}</span>\n <input\n value={tagsDraft}\n onChange={event => setTagsDraft(event.target.value)}\n placeholder=\"dev, prod\"\n />\n </label>\n </div>\n <div className={css.editorActions}>\n <button type=\"button\" onClick={() => setEditor({ status: 'closed' })} disabled={busy}>{t('cancel')}</button>\n <button type=\"button\" className={css.saveButton} onClick={() => void save()} disabled={busy}>{t('save')}</button>\n </div>\n </div>\n )}\n </section>\n )\n}\n","/** Copy dictionaries for the Vault settings section. */\n\n/** English strings (the key-set source of truth for this pair). */\nexport const en = {\n nav: 'Vault',\n heading: 'Vault',\n intro: 'Encrypted credentials stored on this machine.',\n searchPlaceholder: 'Search title, username, email, host…',\n loading: 'Loading…',\n empty: 'No credentials yet. Add one to get started.',\n emptyHint: 'Tip: you can ask the assistant to save an API key or token you share in chat — with auto-capture on it offers to store it for you.',\n emptyHintReadonly: 'The vault is in readonly mode — credentials can be read but not added here. Switch accessMode to \"readwrite\" to enable writes.',\n modeLabel: 'Access mode',\n modeReadonly: 'Read-only',\n modeAsk: 'Ask before writing',\n modeAuto: 'Automatic read-write',\n modeReadonlyHint: 'Read-only — mutations disabled',\n modeAskHint: 'Prompt before writing — every add/update/delete asks you',\n modeAutoHint: 'Automatic read-write — no per-call prompt',\n autoCaptureOn: 'auto-capture on',\n autoCaptureOff: 'auto-capture off',\n reportTitle: 'Health & rotation',\n reportRotation: 'expired / due for rotation',\n reportWeak: 'weak passwords',\n reportReused: 'credentials reused across entries',\n showTrash: 'Trash',\n hideTrash: 'Hide trash',\n trashed: 'in trash',\n trashEmpty: 'Trash is empty',\n restore: 'Restore',\n add: 'Add credential',\n cancel: 'Cancel',\n save: 'Save',\n edit: 'Edit',\n delete: 'Delete',\n deleteConfirm: 'Delete this entry? This cannot be undone.',\n copy: 'Copy',\n copied: 'Copied',\n totp: 'Code',\n fieldTitle: 'Title',\n fieldKind: 'Kind',\n fieldUsername: 'Username',\n fieldEmail: 'Email',\n fieldPhone: 'Phone',\n fieldPassword: 'Password',\n fieldHost: 'Host',\n fieldPort: 'Port',\n fieldApiKey: 'API key',\n fieldSecret: 'Secret',\n fieldAccessToken: 'Access token',\n fieldRefreshToken: 'Refresh token',\n fieldOtpSecret: 'OTP secret',\n fieldUrl: 'URL',\n fieldNotes: 'Notes',\n fieldTags: 'Tags',\n error: 'Something went wrong.',\n kindLogin: 'Login',\n kindSsh: 'SSH',\n kindApiKey: 'API key',\n kindSecret: 'Secret',\n kindOauth: 'OAuth',\n kindCustom: 'Custom',\n} as const\n\nexport type VaultLocaleKey = keyof typeof en\n\n/** Chinese strings (same keys as English). */\nexport const zh: Record<VaultLocaleKey, string> = {\n nav: '凭据库',\n heading: 'Vault 凭据库',\n intro: '本机加密存储的凭据。',\n searchPlaceholder: '搜索标题、用户名、邮箱、主机…',\n loading: '加载中…',\n empty: '还没有凭据,添加一条开始使用。',\n emptyHint: '提示:你可以在对话中把 API Key 或 token 交给助手保存——开启自动捕获后,它会在你同意时帮你存入本库。',\n emptyHintReadonly: '当前为只读模式——只能读取凭据,不能在此添加。将 accessMode 改为 \"readwrite\" 即可启用写入。',\n modeLabel: '访问模式',\n modeReadonly: '只读',\n modeAsk: '写入前询问',\n modeAuto: '自动读写',\n modeReadonlyHint: '只读 —— 变更已禁用',\n modeAskHint: '写入前询问 —— 每次增/改/删都会征求你的确认',\n modeAutoHint: '自动读写 —— 无需逐次确认',\n autoCaptureOn: '自动捕获已开启',\n autoCaptureOff: '自动捕获已关闭',\n reportTitle: '健康与轮换',\n reportRotation: '已过期 / 待轮换',\n reportWeak: '弱密码',\n reportReused: '跨条目复用的凭据',\n showTrash: '回收站',\n hideTrash: '隐藏回收站',\n trashed: '已删除',\n trashEmpty: '回收站为空',\n restore: '恢复',\n add: '新增凭据',\n cancel: '取消',\n save: '保存',\n edit: '编辑',\n delete: '删除',\n deleteConfirm: '确定删除该条目?此操作不可撤销。',\n copy: '复制',\n copied: '已复制',\n totp: '验证码',\n fieldTitle: '标题',\n fieldKind: '类型',\n fieldUsername: '用户名',\n fieldEmail: '邮箱',\n fieldPhone: '手机号',\n fieldPassword: '密码',\n fieldHost: '主机',\n fieldPort: '端口',\n fieldApiKey: 'API 密钥',\n fieldSecret: 'Secret',\n fieldAccessToken: '访问令牌',\n fieldRefreshToken: '刷新令牌',\n fieldOtpSecret: 'TOTP 密钥',\n fieldUrl: 'URL',\n fieldNotes: '备注',\n fieldTags: '标签',\n error: '操作失败。',\n kindLogin: '登录',\n kindSsh: 'SSH',\n kindApiKey: 'API 密钥',\n kindSecret: 'Secret',\n kindOauth: 'OAuth',\n kindCustom: '自定义',\n}\n","/**\n * dsh-vault browser half: a Vault settings page managing encrypted\n * credentials — list, search, add, edit, delete, and copy — through the host\n * VaultGateway remote over the /api RPC channel.\n *\n * @module dsh-vault/client\n */\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { VaultSection, type VaultSectionInjected } from './VaultSection.tsx'\nimport type { VaultSectionTypes } from './VaultSection.tsx'\nimport { en, zh, type VaultLocaleKey } from './locales.ts'\n\nexport type { VaultSectionInjected, VaultSectionProps } from './VaultSection.tsx'\nexport type { VaultSectionTypes } from './VaultSection.tsx'\nexport type { VaultLocaleKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Vault settings page copy. */\n 'settings.vault': VaultLocaleKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'settings.vault'\n\n/** Services required by the Settings registration and the RPC face. */\nexport const inject = ['slots', 'locale', 'connection']\n\n/**\n * Register the Vault settings section once the `settings.section` declaration\n * is on the ledger. The section talks to the host through the connection RPC\n * channel (endpoints `vault/*` served by the host VaultGateway remote).\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-vault: dictionaries')\n\n const t = ctx.locale.bind(NS)\n const connection = ctx.get('connection') as ConnectionHandle\n\n /** Invoke one host vault remote and normalize the RPC envelope. */\n async function invoke<T>(method: string, args: Record<string, unknown> = {}): Promise<T> {\n const result = await connection.rpc.call('/api', `vault/${method}`, { args })\n if (!result.ok) {\n throw new Error(`vault.${method} failed: ${result.error.code}: ${result.error.message}`)\n }\n return result.value as T\n }\n\n const injected = (): VaultSectionInjected => ({\n t,\n config: () => invoke<VaultSectionTypes['config']>('config'),\n setAccessMode: (mode) => invoke<VaultSectionTypes['config']>('setAccessMode', { mode }),\n list: () => invoke<{ entries: VaultSectionTypes['entries'] }>('list').then(r => r.entries),\n search: (query, limit) => invoke<{ entries: VaultSectionTypes['entries'] }>('search', { query, limit: limit ?? 50 }).then(r => r.entries),\n get: (id) => invoke<{ found: boolean; entry?: VaultSectionTypes['fullEntry'] }>('get', { id }),\n add: (patch) => invoke<VaultSectionTypes['summaryEntry']>('add', { patch }),\n update: (id, patch) => invoke<{ found: boolean; entry?: VaultSectionTypes['summaryEntry'] }>('update', { id, patch }),\n remove: (id) => invoke<{ deleted: boolean }>('delete', { id }),\n trash: () => invoke<{ entries: VaultSectionTypes['entries'] }>('trash').then(r => r.entries),\n rotation: () => invoke<{ entries: unknown[] }>('rotation').then(r => r.entries),\n health: () => invoke<{ weak: unknown[]; reused: unknown[] }>('health'),\n restore: (id) => invoke<{ restored: boolean }>('restore', { id }),\n totp: (id) => invoke<{ code: string; label?: string; secondsRemaining: number }>('totp', { id }),\n })\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'vault',\n order: 30,\n label: () => t('nav'),\n inject: injected,\n }, VaultSection))\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -71,10 +71,13 @@ export const Config = Schema.object({
|
|
|
71
71
|
Schema.const('auto'),
|
|
72
72
|
]),
|
|
73
73
|
autoCapture: Schema.boolean(),
|
|
74
|
+
lockTimeoutSeconds: Schema.number(),
|
|
75
|
+
exportPasswordEnv: Schema.string(),
|
|
74
76
|
});
|
|
75
77
|
export async function apply(ctx, config) {
|
|
76
78
|
const masterPassword = resolveMasterPassword(config);
|
|
77
79
|
const WRITE_TOOLS = new Set(['vault_add', 'vault_update', 'vault_delete']);
|
|
80
|
+
const lockTimeoutSeconds = config.lockTimeoutSeconds ?? 0;
|
|
78
81
|
/** Shared access policy; resolved once, mutated by the UI via setAccessMode. */
|
|
79
82
|
const policy = await sharedAccessPolicy(config);
|
|
80
83
|
/** Reject mutations when the vault is in readonly mode. */
|
|
@@ -98,11 +101,29 @@ export async function apply(ctx, config) {
|
|
|
98
101
|
/** Ensure the shared store is open (lazily on first use, so a missing
|
|
99
102
|
* master password fails at the first tool call with a clear message). */
|
|
100
103
|
async function ensureStore() {
|
|
101
|
-
|
|
104
|
+
const store = await sharedVaultStore(masterPassword, config);
|
|
105
|
+
// Install the auto-lock policy once per store instance.
|
|
106
|
+
if (lockTimeoutSeconds > 0)
|
|
107
|
+
store.setAutoLock(lockTimeoutSeconds * 1000);
|
|
108
|
+
return store;
|
|
102
109
|
}
|
|
103
|
-
/**
|
|
110
|
+
/** Guard every tool: enforce auto-lock (relock when idle, refuse when
|
|
111
|
+
* locked) and touch the activity timestamp. */
|
|
112
|
+
async function guardStore() {
|
|
113
|
+
const store = await ensureStore();
|
|
114
|
+
if (store.expired) {
|
|
115
|
+
store.lock();
|
|
116
|
+
throw new Error('vault is locked (idle timeout) — call vault_unlock to re-open it');
|
|
117
|
+
}
|
|
118
|
+
if (store.isLocked) {
|
|
119
|
+
throw new Error('vault is locked — call vault_unlock to re-open it');
|
|
120
|
+
}
|
|
121
|
+
store.touch();
|
|
122
|
+
return store;
|
|
123
|
+
}
|
|
124
|
+
/** Read a full entry (with secrets) by id (respects locking). */
|
|
104
125
|
async function readEntry(id) {
|
|
105
|
-
const s = await
|
|
126
|
+
const s = await guardStore();
|
|
106
127
|
return s.get(id);
|
|
107
128
|
}
|
|
108
129
|
// System prompt guidance: tells the model how the vault works, what the
|
|
@@ -186,7 +207,7 @@ export async function apply(ctx, config) {
|
|
|
186
207
|
assertWritable('vault_add');
|
|
187
208
|
if (!args.title.trim())
|
|
188
209
|
throw new Error('vault_add: title must not be empty');
|
|
189
|
-
const s = await
|
|
210
|
+
const s = await guardStore();
|
|
190
211
|
const entry = await s.add({
|
|
191
212
|
title: args.title.trim(),
|
|
192
213
|
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
@@ -267,7 +288,7 @@ export async function apply(ctx, config) {
|
|
|
267
288
|
}],
|
|
268
289
|
},
|
|
269
290
|
async execute(args) {
|
|
270
|
-
const s = await
|
|
291
|
+
const s = await guardStore();
|
|
271
292
|
const limit = validateLimit(args.limit, 'vault_search');
|
|
272
293
|
const results = s.search(args.query, limit);
|
|
273
294
|
return { results, total: results.length };
|
|
@@ -321,7 +342,7 @@ export async function apply(ctx, config) {
|
|
|
321
342
|
},
|
|
322
343
|
async execute(args) {
|
|
323
344
|
assertWritable('vault_update');
|
|
324
|
-
const s = await
|
|
345
|
+
const s = await guardStore();
|
|
325
346
|
const patch = {};
|
|
326
347
|
for (const key of [
|
|
327
348
|
'title', 'kind', 'username', 'email', 'phone', 'password', 'host', 'port', 'privateKey',
|
|
@@ -358,7 +379,7 @@ export async function apply(ctx, config) {
|
|
|
358
379
|
},
|
|
359
380
|
async execute(args) {
|
|
360
381
|
assertWritable('vault_delete');
|
|
361
|
-
const s = await
|
|
382
|
+
const s = await guardStore();
|
|
362
383
|
const deleted = await s.delete(args.id);
|
|
363
384
|
return { deleted, message: deleted ? 'entry deleted' : 'entry not found' };
|
|
364
385
|
},
|
|
@@ -448,6 +469,194 @@ export async function apply(ctx, config) {
|
|
|
448
469
|
return { password, length: password.length };
|
|
449
470
|
},
|
|
450
471
|
}));
|
|
472
|
+
// ── vault_lock / vault_unlock: explicit lock & unlock ──────────────────────
|
|
473
|
+
ctx.tools.register(defineTool({
|
|
474
|
+
name: 'vault_lock',
|
|
475
|
+
description: 'Lock the vault immediately: wipe the derived key from memory so every '
|
|
476
|
+
+ 'subsequent read/write requires vault_unlock. Use when leaving the machine.',
|
|
477
|
+
parameters: {},
|
|
478
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { locked: { type: 'boolean', required: true } } }, render: (_a, v) => [{ type: 'text', text: v.locked ? 'vault locked' : 'vault not unlocked' }] },
|
|
479
|
+
async execute() {
|
|
480
|
+
const s = await ensureStore();
|
|
481
|
+
const was = s.isLocked;
|
|
482
|
+
s.lock();
|
|
483
|
+
return { locked: !was };
|
|
484
|
+
},
|
|
485
|
+
}));
|
|
486
|
+
ctx.tools.register(defineTool({
|
|
487
|
+
name: 'vault_unlock',
|
|
488
|
+
description: 'Unlock the vault with the master password (the deployment owns the password; '
|
|
489
|
+
+ 'the model never supplies it). Needed after an explicit vault_lock or an auto-lock idle timeout.',
|
|
490
|
+
parameters: {},
|
|
491
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { unlocked: { type: 'boolean', required: true } } }, render: (_a, v) => [{ type: 'text', text: v.unlocked ? 'vault unlocked' : 'vault already unlocked' }] },
|
|
492
|
+
async execute() {
|
|
493
|
+
const s = await ensureStore();
|
|
494
|
+
if (!s.isLocked)
|
|
495
|
+
return { unlocked: false };
|
|
496
|
+
await s.unlock();
|
|
497
|
+
return { unlocked: true };
|
|
498
|
+
},
|
|
499
|
+
}));
|
|
500
|
+
// ── vault_rotation: expiry / rotation report ───────────────────────────────
|
|
501
|
+
ctx.tools.register(defineTool({
|
|
502
|
+
name: 'vault_rotation',
|
|
503
|
+
description: 'Report credentials that are expired, due for rotation (rotationDays elapsed), '
|
|
504
|
+
+ 'or expiring within 7 days. Returns summaries with a due state — never secrets.',
|
|
505
|
+
parameters: {},
|
|
506
|
+
output: {
|
|
507
|
+
schema: { type: 'object', additionalProperties: false, properties: { entries: { type: 'array', required: true, items: { type: 'json' } } } },
|
|
508
|
+
render: (_a, v) => [{ type: 'text', text: v.entries.length === 0 ? 'no rotation items' : JSON.stringify(v.entries) }],
|
|
509
|
+
},
|
|
510
|
+
async execute() {
|
|
511
|
+
const s = await guardStore();
|
|
512
|
+
return { entries: s.rotationReport() };
|
|
513
|
+
},
|
|
514
|
+
}));
|
|
515
|
+
// ── vault_health: weak / reused credential scan ────────────────────────────
|
|
516
|
+
ctx.tools.register(defineTool({
|
|
517
|
+
name: 'vault_health',
|
|
518
|
+
description: 'Scan the vault for weak passwords (shorter than 12 chars) and credentials reused '
|
|
519
|
+
+ 'across entries. Returns non-secret findings (entry summaries grouped by the reused value).',
|
|
520
|
+
parameters: {},
|
|
521
|
+
output: {
|
|
522
|
+
schema: { type: 'object', additionalProperties: false, properties: { weak: { type: 'array', required: true, items: { type: 'json' } }, reused: { type: 'array', required: true, items: { type: 'json' } } } },
|
|
523
|
+
render: (_a, v) => [{ type: 'text', text: `weak: ${v.weak.length}, reused groups: ${v.reused.length}` }],
|
|
524
|
+
},
|
|
525
|
+
async execute() {
|
|
526
|
+
const s = await guardStore();
|
|
527
|
+
return s.health();
|
|
528
|
+
},
|
|
529
|
+
}));
|
|
530
|
+
// ── vault_restore / vault_purge: trash management ──────────────────────────
|
|
531
|
+
ctx.tools.register(defineTool({
|
|
532
|
+
name: 'vault_restore',
|
|
533
|
+
description: 'Restore a soft-deleted entry from the vault trash back into the active set.',
|
|
534
|
+
parameters: { id: { type: 'string', required: true, description: 'The trashed entry id.' } },
|
|
535
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { restored: { type: 'boolean', required: true } } }, render: (_a, v) => [{ type: 'text', text: v.restored ? 'entry restored' : 'entry not found in trash' }] },
|
|
536
|
+
async execute(args) {
|
|
537
|
+
assertWritable('vault_restore');
|
|
538
|
+
const s = await guardStore();
|
|
539
|
+
return { restored: await s.restore(args.id) };
|
|
540
|
+
},
|
|
541
|
+
}));
|
|
542
|
+
ctx.tools.register(defineTool({
|
|
543
|
+
name: 'vault_purge',
|
|
544
|
+
description: 'Permanently delete an entry (active or trashed). Cannot be undone — prefer vault_delete '
|
|
545
|
+
+ '(soft delete) unless the entry must be removed from disk.',
|
|
546
|
+
parameters: { id: { type: 'string', required: true, description: 'The entry id to purge.' } },
|
|
547
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { purged: { type: 'boolean', required: true } } }, render: (_a, v) => [{ type: 'text', text: v.purged ? 'entry purged' : 'entry not found' }] },
|
|
548
|
+
async execute(args) {
|
|
549
|
+
assertWritable('vault_purge');
|
|
550
|
+
const s = await guardStore();
|
|
551
|
+
return { purged: await s.purge(args.id) };
|
|
552
|
+
},
|
|
553
|
+
}));
|
|
554
|
+
// ── vault_export / vault_import: portable encrypted transfer ───────────────
|
|
555
|
+
ctx.tools.register(defineTool({
|
|
556
|
+
name: 'vault_export',
|
|
557
|
+
description: 'Export the entire vault (including trash) as a single encrypted document under a '
|
|
558
|
+
+ 'separate export password (from the exportPasswordEnv config). Use for backup or migration; '
|
|
559
|
+
+ 'the export can be re-imported with vault_import. Never pass the password as an argument.',
|
|
560
|
+
parameters: {},
|
|
561
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { exported: { type: 'boolean', required: true }, note: { type: 'string', required: true } } }, render: (_a, v) => [{ type: 'text', text: v.note }] },
|
|
562
|
+
async execute() {
|
|
563
|
+
const exportPassword = resolveExportPassword(config);
|
|
564
|
+
const s = await guardStore();
|
|
565
|
+
const blob = await s.exportEncrypted(exportPassword);
|
|
566
|
+
const file = join(dirname(resolveVaultPath(config)), `vault-export-${Date.now()}.json`);
|
|
567
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
568
|
+
await writeFile(file, blob, { mode: 0o600 });
|
|
569
|
+
return { exported: true, note: `vault exported to ${file}` };
|
|
570
|
+
},
|
|
571
|
+
}));
|
|
572
|
+
ctx.tools.register(defineTool({
|
|
573
|
+
name: 'vault_import',
|
|
574
|
+
description: 'Import a previously exported vault document (see vault_export), merging entries by '
|
|
575
|
+
+ 'id. Pass the document path; the export password comes from the exportPasswordEnv config.',
|
|
576
|
+
parameters: { path: { type: 'string', required: true, description: 'Absolute path of the exported vault JSON file.' } },
|
|
577
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { imported: { type: 'integer', required: true } } }, render: (_a, v) => [{ type: 'text', text: `imported ${v.imported} entries` }] },
|
|
578
|
+
async execute(args) {
|
|
579
|
+
assertWritable('vault_import');
|
|
580
|
+
const exportPassword = resolveExportPassword(config);
|
|
581
|
+
const s = await guardStore();
|
|
582
|
+
const blob = await readFile(args.path, 'utf8');
|
|
583
|
+
const count = await s.importEncrypted(blob, exportPassword);
|
|
584
|
+
return { imported: count };
|
|
585
|
+
},
|
|
586
|
+
}));
|
|
587
|
+
// ── vault_fill: find the credential that fits a target ─────────────────────
|
|
588
|
+
ctx.tools.register(defineTool({
|
|
589
|
+
name: 'vault_fill',
|
|
590
|
+
description: 'Find the vault entry that fits a target (host/URL/username/title) and return the '
|
|
591
|
+
+ 'ready-to-use credentials (secrets included, as the caller needs them for the actual login). '
|
|
592
|
+
+ 'Use instead of vault_search+vault_get when you know what you are connecting to.',
|
|
593
|
+
parameters: {
|
|
594
|
+
target: { type: 'string', required: true, description: 'Host, URL, username, or title to match.' },
|
|
595
|
+
},
|
|
596
|
+
output: {
|
|
597
|
+
schema: { type: 'object', additionalProperties: false, properties: { found: { type: 'boolean', required: true }, entry: { type: 'json' } } },
|
|
598
|
+
render: (_a, v) => [{ type: 'text', text: v.found ? `matched: ${v.entry?.title ?? 'entry'}` : 'no matching entry' }],
|
|
599
|
+
},
|
|
600
|
+
async execute(args) {
|
|
601
|
+
const s = await guardStore();
|
|
602
|
+
const needle = args.target.trim().toLowerCase();
|
|
603
|
+
for (const entry of s.list()) {
|
|
604
|
+
const haystack = [entry.title, entry.host, entry.url, entry.username, entry.email].filter(Boolean).join(' ').toLowerCase();
|
|
605
|
+
if (needle.length > 0 && haystack.includes(needle)) {
|
|
606
|
+
return { found: true, entry: stripTimestamps(entry) };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return { found: false };
|
|
610
|
+
},
|
|
611
|
+
}));
|
|
612
|
+
// ── vault_env: environment-variable export ─────────────────────────────────
|
|
613
|
+
ctx.tools.register(defineTool({
|
|
614
|
+
name: 'vault_env',
|
|
615
|
+
description: 'Render entries flagged for environment export (tags contain "env") as KEY=VALUE lines '
|
|
616
|
+
+ 'suitable for .env or export statements. Keys derive from the title + field name; values are the '
|
|
617
|
+
+ 'secrets. Returns the lines so the caller can write them to a file (user-authorized).',
|
|
618
|
+
parameters: {},
|
|
619
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { lines: { type: 'array', required: true, items: { type: 'string' } } } }, render: (_a, v) => [{ type: 'text', text: v.lines.join('\n') }] },
|
|
620
|
+
async execute() {
|
|
621
|
+
const s = await guardStore();
|
|
622
|
+
const lines = [];
|
|
623
|
+
const keyOf = (title, field) => title.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '') + '_' + field.toUpperCase();
|
|
624
|
+
for (const entry of s.list()) {
|
|
625
|
+
if (!(entry.tags ?? []).includes('env'))
|
|
626
|
+
continue;
|
|
627
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
628
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
629
|
+
continue;
|
|
630
|
+
if (['id', 'title', 'kind', 'sensitivity', 'host', 'url', 'notes', 'createdAt', 'updatedAt', 'deletedAt'].includes(field))
|
|
631
|
+
continue;
|
|
632
|
+
if (['username', 'email', 'phone', 'port', 'tags'].includes(field))
|
|
633
|
+
continue;
|
|
634
|
+
lines.push(`${keyOf(entry.title, field)}=${value}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return { lines };
|
|
638
|
+
},
|
|
639
|
+
}));
|
|
640
|
+
// ── vault_templates: field templates by kind ───────────────────────────────
|
|
641
|
+
const TEMPLATES = {
|
|
642
|
+
login: { username: 'account username', email: 'account email', password: 'account password' },
|
|
643
|
+
ssh: { host: 'server host', port: 'port (e.g. 22)', username: 'login user', password: 'password or passphrase', privateKey: 'PEM private key' },
|
|
644
|
+
'api-key': { apiKey: 'the API key', url: 'API base URL', username: 'owner/account (optional)' },
|
|
645
|
+
oauth: { accessToken: 'access token', refreshToken: 'refresh token', expiresAt: 'expiry epoch millis', clientId: 'client id (via fields)' },
|
|
646
|
+
secret: { secret: 'the shared secret', notes: 'what it is for' },
|
|
647
|
+
custom: { fields: 'arbitrary key/value pairs' },
|
|
648
|
+
};
|
|
649
|
+
ctx.tools.register(defineTool({
|
|
650
|
+
name: 'vault_templates',
|
|
651
|
+
description: 'Return the recommended fields for a credential kind, so vault_add can be called with '
|
|
652
|
+
+ 'the right field names (e.g. kind ssh → host/port/username/password/privateKey).',
|
|
653
|
+
parameters: { kind: { type: 'string', description: 'Entry kind; default login.', enum: ['login', 'ssh', 'api-key', 'secret', 'oauth', 'custom'] } },
|
|
654
|
+
output: { schema: { type: 'object', additionalProperties: false, properties: { kind: { type: 'string', required: true }, fields: { type: 'json', required: true } } }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v.fields) }] },
|
|
655
|
+
async execute(args) {
|
|
656
|
+
const kind = args.kind ?? 'login';
|
|
657
|
+
return { kind, fields: TEMPLATES[kind] ?? TEMPLATES.login };
|
|
658
|
+
},
|
|
659
|
+
}));
|
|
451
660
|
// UI-facing Remote gateway: the browser Settings Vault page talks to these
|
|
452
661
|
// methods through the /api RPC channel (loopback-trusted), bypassing the
|
|
453
662
|
// model-tool layer entirely. Secrets are returned because the UI is the
|
|
@@ -472,6 +681,10 @@ let VaultGateway = (() => {
|
|
|
472
681
|
let _config_decorators;
|
|
473
682
|
let _setAccessMode_decorators;
|
|
474
683
|
let _list_decorators;
|
|
684
|
+
let _trash_decorators;
|
|
685
|
+
let _restore_decorators;
|
|
686
|
+
let _rotation_decorators;
|
|
687
|
+
let _health_decorators;
|
|
475
688
|
let _get_decorators;
|
|
476
689
|
let _search_decorators;
|
|
477
690
|
let _add_decorators;
|
|
@@ -484,6 +697,10 @@ let VaultGateway = (() => {
|
|
|
484
697
|
_config_decorators = [Remote('config')];
|
|
485
698
|
_setAccessMode_decorators = [Remote('setAccessMode')];
|
|
486
699
|
_list_decorators = [Remote('list')];
|
|
700
|
+
_trash_decorators = [Remote('trash')];
|
|
701
|
+
_restore_decorators = [Remote('restore')];
|
|
702
|
+
_rotation_decorators = [Remote('rotation')];
|
|
703
|
+
_health_decorators = [Remote('health')];
|
|
487
704
|
_get_decorators = [Remote('get')];
|
|
488
705
|
_search_decorators = [Remote('search')];
|
|
489
706
|
_add_decorators = [Remote('add')];
|
|
@@ -493,6 +710,10 @@ let VaultGateway = (() => {
|
|
|
493
710
|
__esDecorate(this, null, _config_decorators, { kind: "method", name: "config", static: false, private: false, access: { has: obj => "config" in obj, get: obj => obj.config }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
494
711
|
__esDecorate(this, null, _setAccessMode_decorators, { kind: "method", name: "setAccessMode", static: false, private: false, access: { has: obj => "setAccessMode" in obj, get: obj => obj.setAccessMode }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
495
712
|
__esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
713
|
+
__esDecorate(this, null, _trash_decorators, { kind: "method", name: "trash", static: false, private: false, access: { has: obj => "trash" in obj, get: obj => obj.trash }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
714
|
+
__esDecorate(this, null, _restore_decorators, { kind: "method", name: "restore", static: false, private: false, access: { has: obj => "restore" in obj, get: obj => obj.restore }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
715
|
+
__esDecorate(this, null, _rotation_decorators, { kind: "method", name: "rotation", static: false, private: false, access: { has: obj => "rotation" in obj, get: obj => obj.rotation }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
716
|
+
__esDecorate(this, null, _health_decorators, { kind: "method", name: "health", static: false, private: false, access: { has: obj => "health" in obj, get: obj => obj.health }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
496
717
|
__esDecorate(this, null, _get_decorators, { kind: "method", name: "get", static: false, private: false, access: { has: obj => "get" in obj, get: obj => obj.get }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
497
718
|
__esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
498
719
|
__esDecorate(this, null, _add_decorators, { kind: "method", name: "add", static: false, private: false, access: { has: obj => "add" in obj, get: obj => obj.add }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -552,6 +773,27 @@ let VaultGateway = (() => {
|
|
|
552
773
|
const store = await this.ensureStore();
|
|
553
774
|
return { entries: store.list().map(toSummary) };
|
|
554
775
|
}
|
|
776
|
+
/** List trashed (soft-deleted) entries as non-secret summaries. */
|
|
777
|
+
async trash() {
|
|
778
|
+
const store = await this.ensureStore();
|
|
779
|
+
return { entries: store.listTrash().map(toSummary) };
|
|
780
|
+
}
|
|
781
|
+
/** Restore a trashed entry (non-secret summary returned). */
|
|
782
|
+
async restore(id) {
|
|
783
|
+
this.assertWritable('restore');
|
|
784
|
+
const store = await this.ensureStore();
|
|
785
|
+
return { restored: await store.restore(id) };
|
|
786
|
+
}
|
|
787
|
+
/** Rotation/expiry report (no secrets). */
|
|
788
|
+
async rotation() {
|
|
789
|
+
const store = await this.ensureStore();
|
|
790
|
+
return { entries: store.rotationReport() };
|
|
791
|
+
}
|
|
792
|
+
/** Health scan findings (no secrets). */
|
|
793
|
+
async health() {
|
|
794
|
+
const store = await this.ensureStore();
|
|
795
|
+
return store.health();
|
|
796
|
+
}
|
|
555
797
|
/** Read one full entry (including secrets) by id. */
|
|
556
798
|
async get(id) {
|
|
557
799
|
const store = await this.ensureStore();
|
|
@@ -651,6 +893,18 @@ function resolveMasterPassword(config) {
|
|
|
651
893
|
}
|
|
652
894
|
throw new Error('dsh-vault: configure masterPassword or masterPasswordEnv to unlock the vault');
|
|
653
895
|
}
|
|
896
|
+
/** Resolve the export/import password from the configured environment
|
|
897
|
+
* variable, or fail loudly (the model must never pass it as an argument). */
|
|
898
|
+
function resolveExportPassword(config) {
|
|
899
|
+
if (config.exportPasswordEnv !== undefined) {
|
|
900
|
+
const fromEnv = process.env[config.exportPasswordEnv];
|
|
901
|
+
if (fromEnv === undefined || fromEnv.length === 0) {
|
|
902
|
+
throw new Error(`dsh-vault: environment variable ${config.exportPasswordEnv} is not set (needed for vault_export/import)`);
|
|
903
|
+
}
|
|
904
|
+
return fromEnv;
|
|
905
|
+
}
|
|
906
|
+
throw new Error('dsh-vault: configure exportPasswordEnv to use vault_export / vault_import');
|
|
907
|
+
}
|
|
654
908
|
/**
|
|
655
909
|
* Shared vault-store instances keyed by resolved path + master password. The
|
|
656
910
|
* model tools and the UI-facing VaultGateway must observe ONE store so writes
|
|
@@ -698,6 +952,40 @@ async function sharedAccessPolicy(config) {
|
|
|
698
952
|
sharedAccessPolicies.set(path, policy);
|
|
699
953
|
return policy;
|
|
700
954
|
}
|
|
955
|
+
const MAX_ATTEMPTS = 5;
|
|
956
|
+
const LOCKOUT_MS = 5 * 60 * 1000;
|
|
957
|
+
/** `<vault dir>/meta.json` — failed-attempt counter and lockout window. */
|
|
958
|
+
function metaFile(config) {
|
|
959
|
+
return join(dirname(resolveVaultPath(config)), 'meta.json');
|
|
960
|
+
}
|
|
961
|
+
async function readMeta(config) {
|
|
962
|
+
try {
|
|
963
|
+
const raw = await readFile(metaFile(config), 'utf8');
|
|
964
|
+
return JSON.parse(raw);
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
return { failedAttempts: 0 };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function writeMeta(config, meta) {
|
|
971
|
+
const file = metaFile(config);
|
|
972
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
973
|
+
await writeFile(file, JSON.stringify(meta), { mode: 0o600 });
|
|
974
|
+
}
|
|
975
|
+
/** Record one failed unlock: increment the counter, start a lockout when the
|
|
976
|
+
* threshold is crossed. */
|
|
977
|
+
async function recordFailedAttempt(config) {
|
|
978
|
+
const meta = await readMeta(config);
|
|
979
|
+
meta.failedAttempts = (meta.failedAttempts ?? 0) + 1;
|
|
980
|
+
if (meta.failedAttempts >= MAX_ATTEMPTS) {
|
|
981
|
+
meta.lockedUntil = Date.now() + LOCKOUT_MS;
|
|
982
|
+
}
|
|
983
|
+
await writeMeta(config, meta);
|
|
984
|
+
}
|
|
985
|
+
/** Clear the counter after a successful unlock. */
|
|
986
|
+
async function clearFailedAttempts(config) {
|
|
987
|
+
await writeMeta(config, { failedAttempts: 0 });
|
|
988
|
+
}
|
|
701
989
|
/**
|
|
702
990
|
* Open (or reuse) the vault store for one deployment configuration. All
|
|
703
991
|
* callers within the process share the same instance for the same path and
|
|
@@ -706,22 +994,34 @@ async function sharedAccessPolicy(config) {
|
|
|
706
994
|
*/
|
|
707
995
|
async function sharedVaultStore(masterPassword, config) {
|
|
708
996
|
const path = resolveVaultPath(config);
|
|
997
|
+
// Brute-force guard: refuse to even attempt while a lockout window is open.
|
|
998
|
+
const meta = await readMeta(config);
|
|
999
|
+
if (meta.lockedUntil !== undefined && Date.now() < meta.lockedUntil) {
|
|
1000
|
+
const minutes = Math.ceil((meta.lockedUntil - Date.now()) / 60000);
|
|
1001
|
+
throw new Error(`vault is temporarily locked after ${MAX_ATTEMPTS} failed password attempts — retry in ~${minutes} min`);
|
|
1002
|
+
}
|
|
709
1003
|
// The master password is part of the identity: a different password must
|
|
710
1004
|
// open its own store (and fail authentication) rather than reuse a store
|
|
711
1005
|
// unlocked with another password.
|
|
712
1006
|
const cacheKey = `${path}\0${masterPassword}`;
|
|
713
1007
|
const existing = sharedVaultStores.get(cacheKey);
|
|
714
|
-
if (existing !== undefined)
|
|
1008
|
+
if (existing !== undefined) {
|
|
1009
|
+
// A successful re-open of a cached store clears the failure counter.
|
|
1010
|
+
await clearFailedAttempts(config);
|
|
715
1011
|
return existing;
|
|
1012
|
+
}
|
|
716
1013
|
const opening = openVault({
|
|
717
1014
|
masterPassword,
|
|
718
1015
|
path,
|
|
719
|
-
}).catch((error) => {
|
|
720
|
-
// A failed open must not poison the cache for later retries
|
|
1016
|
+
}).catch(async (error) => {
|
|
1017
|
+
// A failed open must not poison the cache for later retries, and counts
|
|
1018
|
+
// toward the brute-force lockout.
|
|
721
1019
|
sharedVaultStores.delete(cacheKey);
|
|
1020
|
+
await recordFailedAttempt(config);
|
|
722
1021
|
throw error;
|
|
723
1022
|
});
|
|
724
1023
|
sharedVaultStores.set(cacheKey, opening);
|
|
1024
|
+
await clearFailedAttempts(config);
|
|
725
1025
|
return opening;
|
|
726
1026
|
}
|
|
727
1027
|
/** Validate a model-supplied result limit: a positive integer capped at 100. */
|
package/lib/store.js
CHANGED
|
@@ -57,10 +57,55 @@ export class VaultStore {
|
|
|
57
57
|
* contend for the cross-process file lock and every write sees the latest
|
|
58
58
|
* in-memory state. */
|
|
59
59
|
persistChain = Promise.resolve();
|
|
60
|
+
/** Auto-lock: after this many ms of inactivity the vault re-locks (key
|
|
61
|
+
* wiped). `0`/undefined disables auto-lock. */
|
|
62
|
+
lockTimeoutMs;
|
|
63
|
+
/** Epoch millis of the last operation; used for the auto-lock timer. */
|
|
64
|
+
lastActivity = Date.now();
|
|
65
|
+
/** Whether the vault is currently locked (key wiped, requires re-unlock). */
|
|
66
|
+
locked = false;
|
|
67
|
+
/** Weak-password heuristic: too short or in a tiny common list. */
|
|
68
|
+
static MIN_PASSWORD_LENGTH = 12;
|
|
60
69
|
constructor(path, masterPassword) {
|
|
61
70
|
this.path = path;
|
|
62
71
|
this.masterPassword = masterPassword;
|
|
63
72
|
}
|
|
73
|
+
/** Enable auto-lock with the given idle timeout (ms). */
|
|
74
|
+
setAutoLock(timeoutMs) {
|
|
75
|
+
this.lockTimeoutMs = timeoutMs;
|
|
76
|
+
this.lastActivity = Date.now();
|
|
77
|
+
}
|
|
78
|
+
/** Whether the vault is currently locked. */
|
|
79
|
+
get isLocked() {
|
|
80
|
+
return this.locked;
|
|
81
|
+
}
|
|
82
|
+
/** Touch the activity timestamp; call before every read/write. */
|
|
83
|
+
touch() {
|
|
84
|
+
this.lastActivity = Date.now();
|
|
85
|
+
}
|
|
86
|
+
/** Whether the auto-lock idle window has elapsed. */
|
|
87
|
+
get expired() {
|
|
88
|
+
if (this.lockTimeoutMs === undefined || this.lockTimeoutMs <= 0)
|
|
89
|
+
return false;
|
|
90
|
+
return Date.now() - this.lastActivity > this.lockTimeoutMs;
|
|
91
|
+
}
|
|
92
|
+
/** Lock the vault: wipe the derived key and require re-unlock. */
|
|
93
|
+
lock() {
|
|
94
|
+
this.key?.fill(0);
|
|
95
|
+
this.key = undefined;
|
|
96
|
+
this.locked = true;
|
|
97
|
+
this.lastActivity = Date.now();
|
|
98
|
+
}
|
|
99
|
+
/** Re-derive the key from the master password (after a lock). */
|
|
100
|
+
async unlock() {
|
|
101
|
+
if (this.kdf === undefined) {
|
|
102
|
+
await this.load();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.key = await deriveKey(this.masterPassword, this.kdf);
|
|
106
|
+
this.locked = false;
|
|
107
|
+
this.lastActivity = Date.now();
|
|
108
|
+
}
|
|
64
109
|
/**
|
|
65
110
|
* Load the vault document from disk (creating an empty one on first use)
|
|
66
111
|
* and derive the vault key from the document's fixed KDF parameters.
|
|
@@ -79,6 +124,8 @@ export class VaultStore {
|
|
|
79
124
|
// First run: persist the empty document so the file exists with the
|
|
80
125
|
// chosen KDF (and its verify envelope) before any entry is added.
|
|
81
126
|
await this.persist();
|
|
127
|
+
this.locked = false;
|
|
128
|
+
this.lastActivity = Date.now();
|
|
82
129
|
return;
|
|
83
130
|
}
|
|
84
131
|
for (const blob of file.entries) {
|
|
@@ -91,17 +138,28 @@ export class VaultStore {
|
|
|
91
138
|
if (!safeEqual(verify, Buffer.from(VERIFY_PLAINTEXT, 'utf8'))) {
|
|
92
139
|
throw new Error('vault master password is incorrect');
|
|
93
140
|
}
|
|
141
|
+
this.locked = false;
|
|
142
|
+
this.lastActivity = Date.now();
|
|
94
143
|
}
|
|
95
144
|
/** The vault file path (useful for messages and debugging). */
|
|
96
145
|
get filePath() {
|
|
97
146
|
return this.path;
|
|
98
147
|
}
|
|
99
|
-
/** All entries, in insertion order. */
|
|
148
|
+
/** All active (non-trashed) entries, in insertion order. */
|
|
100
149
|
list() {
|
|
101
|
-
return [...this.entries.values()];
|
|
150
|
+
return [...this.entries.values()].filter(entry => entry.deletedAt === undefined);
|
|
151
|
+
}
|
|
152
|
+
/** All trashed entries (soft-deleted, awaiting purge or restore). */
|
|
153
|
+
listTrash() {
|
|
154
|
+
return [...this.entries.values()].filter(entry => entry.deletedAt !== undefined);
|
|
102
155
|
}
|
|
103
|
-
/** Read one entry by id. */
|
|
156
|
+
/** Read one active entry by id. */
|
|
104
157
|
get(id) {
|
|
158
|
+
const entry = this.entries.get(id);
|
|
159
|
+
return entry !== undefined && entry.deletedAt === undefined ? entry : undefined;
|
|
160
|
+
}
|
|
161
|
+
/** Read one entry by id, including trashed entries. */
|
|
162
|
+
getIncludingTrash(id) {
|
|
105
163
|
return this.entries.get(id);
|
|
106
164
|
}
|
|
107
165
|
/** Search entries across text fields; returns summaries without secrets. */
|
|
@@ -163,8 +221,27 @@ export class VaultStore {
|
|
|
163
221
|
await this.persist();
|
|
164
222
|
return updated;
|
|
165
223
|
}
|
|
166
|
-
/**
|
|
224
|
+
/** Soft-delete an entry (move to trash); returns true when it existed.
|
|
225
|
+
* The entry stays encrypted on disk until purged or restored. */
|
|
167
226
|
async delete(id) {
|
|
227
|
+
const entry = this.entries.get(id);
|
|
228
|
+
if (!entry || entry.deletedAt !== undefined)
|
|
229
|
+
return false;
|
|
230
|
+
entry.deletedAt = Date.now();
|
|
231
|
+
await this.persist();
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
/** Restore a trashed entry; returns true when it existed in trash. */
|
|
235
|
+
async restore(id) {
|
|
236
|
+
const entry = this.entries.get(id);
|
|
237
|
+
if (!entry || entry.deletedAt === undefined)
|
|
238
|
+
return false;
|
|
239
|
+
delete entry.deletedAt;
|
|
240
|
+
await this.persist();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
/** Permanently remove a trashed (or active) entry; returns true when it existed. */
|
|
244
|
+
async purge(id) {
|
|
168
245
|
const existed = this.entries.delete(id);
|
|
169
246
|
if (existed)
|
|
170
247
|
await this.persist();
|
|
@@ -178,6 +255,128 @@ export class VaultStore {
|
|
|
178
255
|
get unlocked() {
|
|
179
256
|
return this.key !== undefined;
|
|
180
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Rotation & expiry report: entries whose `rotationDays` elapsed since
|
|
260
|
+
* their last update, or whose `expiresAt` is near/past. Returns only
|
|
261
|
+
* summaries plus the computed due state (no secrets).
|
|
262
|
+
*/
|
|
263
|
+
rotationReport(now = Date.now()) {
|
|
264
|
+
const report = [];
|
|
265
|
+
for (const entry of this.list()) {
|
|
266
|
+
const base = entry.updatedAt ?? entry.createdAt;
|
|
267
|
+
const rotationAt = entry.rotationDays !== undefined ? base + entry.rotationDays * 86_400_000 : undefined;
|
|
268
|
+
const expiresAt = entry.expiresAt;
|
|
269
|
+
let due;
|
|
270
|
+
let daysLeft;
|
|
271
|
+
if (rotationAt !== undefined && now >= rotationAt) {
|
|
272
|
+
due = 'due';
|
|
273
|
+
daysLeft = 0;
|
|
274
|
+
}
|
|
275
|
+
else if (expiresAt !== undefined && now >= expiresAt) {
|
|
276
|
+
due = 'expired';
|
|
277
|
+
daysLeft = 0;
|
|
278
|
+
}
|
|
279
|
+
else if (expiresAt !== undefined) {
|
|
280
|
+
daysLeft = Math.ceil((expiresAt - now) / 86_400_000);
|
|
281
|
+
if (daysLeft <= 7) {
|
|
282
|
+
due = 'soon';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (rotationAt !== undefined) {
|
|
286
|
+
daysLeft = Math.ceil((rotationAt - now) / 86_400_000);
|
|
287
|
+
if (daysLeft <= 7) {
|
|
288
|
+
due = 'soon';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (due === undefined)
|
|
295
|
+
continue;
|
|
296
|
+
report.push({ ...toSummary(entry), due, daysLeft });
|
|
297
|
+
}
|
|
298
|
+
return report;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Health scan: weak passwords (too short), and passwords/API keys reused
|
|
302
|
+
* across entries. Returns non-secret findings keyed by entry id.
|
|
303
|
+
*/
|
|
304
|
+
health() {
|
|
305
|
+
const weak = [];
|
|
306
|
+
const passwordCounts = new Map();
|
|
307
|
+
const keyCounts = new Map();
|
|
308
|
+
for (const entry of this.list()) {
|
|
309
|
+
const summary = toSummary(entry);
|
|
310
|
+
if (entry.password !== undefined) {
|
|
311
|
+
if (entry.password.length < VaultStore.MIN_PASSWORD_LENGTH)
|
|
312
|
+
weak.push(summary);
|
|
313
|
+
const list = passwordCounts.get(entry.password) ?? [];
|
|
314
|
+
list.push(summary);
|
|
315
|
+
passwordCounts.set(entry.password, list);
|
|
316
|
+
}
|
|
317
|
+
for (const key of [entry.apiKey, entry.accessToken, entry.refreshToken, entry.secret]) {
|
|
318
|
+
if (key === undefined)
|
|
319
|
+
continue;
|
|
320
|
+
const list = keyCounts.get(key) ?? [];
|
|
321
|
+
list.push(summary);
|
|
322
|
+
keyCounts.set(key, list);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const reused = [
|
|
326
|
+
...[...passwordCounts.entries()].filter(([, v]) => v.length > 1),
|
|
327
|
+
...[...keyCounts.entries()].filter(([, v]) => v.length > 1),
|
|
328
|
+
].map(([value, entries]) => ({ value, entries }));
|
|
329
|
+
return { weak, reused };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Export the whole vault (including trash) as a single encrypted blob under
|
|
333
|
+
* a separate export password: a portable, machine-independent document that
|
|
334
|
+
* can be re-imported elsewhere. Returns the armored JSON string.
|
|
335
|
+
*/
|
|
336
|
+
async exportEncrypted(exportPassword, now = Date.now()) {
|
|
337
|
+
if (exportPassword.length === 0)
|
|
338
|
+
throw new Error('vault: export password must not be empty');
|
|
339
|
+
const exportKdf = newKdfParams();
|
|
340
|
+
const exportKey = await deriveKey(exportPassword, exportKdf);
|
|
341
|
+
const payload = {
|
|
342
|
+
exportedAt: now,
|
|
343
|
+
kdf: exportKdf,
|
|
344
|
+
entries: [...this.entries.values()].map(entry => ({
|
|
345
|
+
id: entry.id,
|
|
346
|
+
...encrypt(Buffer.from(JSON.stringify(entry), 'utf8'), exportKey),
|
|
347
|
+
})),
|
|
348
|
+
};
|
|
349
|
+
exportKey.fill(0);
|
|
350
|
+
return JSON.stringify(payload);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Import an exported vault blob, merging entries by id (existing entries win
|
|
354
|
+
* unless `overwrite`). Returns the number of entries added.
|
|
355
|
+
*/
|
|
356
|
+
async importEncrypted(blob, exportPassword, overwrite = false) {
|
|
357
|
+
if (exportPassword.length === 0)
|
|
358
|
+
throw new Error('vault: export password must not be empty');
|
|
359
|
+
const parsed = JSON.parse(blob);
|
|
360
|
+
if (!parsed.kdf || !Array.isArray(parsed.entries)) {
|
|
361
|
+
throw new Error('vault: invalid export document');
|
|
362
|
+
}
|
|
363
|
+
const exportKey = await deriveKey(exportPassword, parsed.kdf);
|
|
364
|
+
let added = 0;
|
|
365
|
+
for (const blobEntry of parsed.entries) {
|
|
366
|
+
const plaintext = decrypt(blobEntry, exportKey);
|
|
367
|
+
const entry = JSON.parse(plaintext.toString('utf8'));
|
|
368
|
+
const existing = this.entries.get(entry.id);
|
|
369
|
+
if (existing !== undefined && !overwrite)
|
|
370
|
+
continue;
|
|
371
|
+
// Imported entries come back as active (clear any soft-delete marker).
|
|
372
|
+
const { deletedAt, ...active } = entry;
|
|
373
|
+
this.entries.set(entry.id, { ...active, updatedAt: Date.now() });
|
|
374
|
+
added++;
|
|
375
|
+
}
|
|
376
|
+
exportKey.fill(0);
|
|
377
|
+
await this.persist();
|
|
378
|
+
return added;
|
|
379
|
+
}
|
|
181
380
|
/**
|
|
182
381
|
* Persist the current in-memory state: encrypt every entry under the vault
|
|
183
382
|
* key, then atomically replace the document under the cross-process lock.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Encrypted credential vault for DeepSeek Harness: store and retrieve usernames, emails, phone numbers, passwords, TOTP secrets, SSH/API-key/OAuth developer credentials through model tools and a Settings UI page.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|