dsh-vault 0.3.0 → 0.5.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 CHANGED
@@ -41,11 +41,24 @@ dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件:把你在使
41
41
  |---|---|
42
42
  | `vault_add` | 新增条目(上述字段任意组合;空字符串/空数组字段会被忽略) |
43
43
  | `vault_get` | 按 id 读取完整条目(含全部密钥) |
44
- | `vault_search` | 跨标题/分类/用户名/邮箱/手机/主机/端口/URL/备注/标签/自定义字段(含数字/布尔/嵌套值)检索,返回无密摘要;`limit` 须为 1–100 的整数 |
44
+ | `vault_search` | 跨标题/分类/用户名/邮箱/手机/主机/端口/URL/备注/标签/自定义字段(含数字/布尔/嵌套值;空格分隔多词 OR 命中)检索,返回无密摘要;`limit` 须为 1–100 的整数 |
45
45
  | `vault_update` | 按 id 更新字段(未提供的字段保留;空字符串清除该字段;`title` 可改名) |
46
- | `vault_delete` | id 删除条目(不可恢复) |
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_strength` | 零依赖密码强度评估(0–100 分,weak/fair/strong/very strong) |
52
+ | `vault_rekey` | 原地升级 scrypt KDF 参数并重加密 |
53
+ | `vault_import_csv` | 从 CSV 批量导入凭据(自定义列变为 fields) |
54
+ | `vault_totp_uri` | 为存储的或裸 TOTP 密钥生成 otpauth:// 配置 URI |
55
+ | `vault_switch` / `vault_list` | 按名称切换当前保险库 / 列出可用保险库 |
56
+ | `vault_rotation` | 报告已过期 / 待轮换 / 即将过期的凭据(不含密钥) |
57
+ | `vault_health` | 扫描弱密码与跨条目复用凭据(不含密钥) |
58
+ | `vault_export` / `vault_import` | 整库加密备份/迁移(独立导出密码) |
59
+ | `vault_fill` | 按 host/URL/用户名/标题匹配条目并返回其凭据 |
60
+ | `vault_env` | 把标记 env 的条目(tags 含 `env`)渲染为 `KEY=VALUE` 行 |
61
+ | `vault_templates` | 返回某类凭据(ssh/api-key/oauth 等)的推荐字段 |
49
62
 
50
63
  **典型开发场景**:存一条 SSH 凭据(`kind: ssh` + host/port/username/password 或 privateKey),开发时让模型 `vault_search` 找主机、`vault_get` 取连接信息;存 API 网关的 `api-key`/`oauth` 条目管理 access/refresh token 轮换。
51
64
 
@@ -130,6 +143,8 @@ tarball 自带预构建 `lib/` 产物,无需构建或 allowBuilds。
130
143
  | `name` | 保险库名,用于默认路径(如 `name: work` → `$DSH_HOME/vault/work.json`) |
131
144
  | `accessMode` | 模型工具的访问策略,三态:`readonly`(只读,工具与设置页的增/改/删全部被拒绝)、`ask`(默认——写入前询问,每次增/改/删都会走 harness 审批通道请你确认)、`auto`(自动读写,无需逐次确认)。设置页提供同样的三选一下拉并持久化到 `<vault 目录>/access.json`。 |
132
145
  | `autoCapture` | `false`(默认)。设为 `true` 时,系统提示词会指导模型识别对话中出现的凭据,并**按用户偏好**用 `vault_add` 提供保存。 |
146
+ | `lockTimeoutSeconds` | 自动锁库:空闲超过该秒数后自动重新锁定(清空内存密钥),之后每次读写需 `vault_unlock`。`0`/缺省为禁用。 |
147
+ | `exportPasswordEnv` | 存放 `vault_export`/`vault_import` 导出密码的环境变量名(绝不能作为模型参数传入)。 |
133
148
 
134
149
  示例:
135
150
 
@@ -142,7 +157,7 @@ tarball 自带预构建 `lib/` 产物,无需构建或 allowBuilds。
142
157
  autoCapture: true
143
158
  ```
144
159
 
145
- `autoCapture: true` 时,当你在对话中分享凭据(如 "我的 npm token 是 npm_…"),助手会提议存入;你同意后立即调用 `vault_add`。`autoCapture` 关闭时,只有你明确要求才保存。设置页显示当前模式(只读 / 写入前询问 / 自动读写),可用下拉切换,空库提示会引导你开始存入密钥。
160
+ `autoCapture: true` 时,当你在对话中分享凭据(如 "我的 npm token 是 npm_…"),助手会提议存入;你同意后立即调用 `vault_add`。`autoCapture` 关闭时,只有你明确要求才保存。设置页显示当前模式(只读 / 写入前询问 / 自动读写)可用下拉切换,还有类型筛选、健康与轮换摘要、回收站视图、以及默认遮罩的密钥字段(显示/隐藏切换)。
146
161
 
147
162
  首次调用任一工具时自动创建保险库;之后每次启动用主密码重新解锁。**忘记主密码 = 数据永久丢失**(无后门,这是设计使然)。
148
163
 
package/README.md CHANGED
@@ -43,11 +43,24 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
43
43
  |---|---|
44
44
  | `vault_add` | Add an entry (any combination of fields; empty strings/arrays are ignored) |
45
45
  | `vault_get` | Read a full entry by id (including all secrets) |
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 |
46
+ | `vault_search` | Search titles/categories/usernames/emails/phones/hosts/ports/URLs/notes/tags/custom fields (incl. numeric/boolean/nested values; whitespace-separated terms OR-match); 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` | Delete an entry by id (irreversible) |
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_strength` | Zero-dependency password strength estimate (score 0–100, weak/fair/strong/very strong) |
54
+ | `vault_rekey` | Upgrade the vault to fresh scrypt KDF parameters in place |
55
+ | `vault_import_csv` | Bulk-import credentials from a CSV file (custom columns become fields) |
56
+ | `vault_totp_uri` | Build an otpauth:// provisioning URI for a stored or bare TOTP secret |
57
+ | `vault_switch` / `vault_list` | Switch the active vault by name / list available vaults |
58
+ | `vault_rotation` | Report expired / due-for-rotation / expiring-soon credentials (no secrets) |
59
+ | `vault_health` | Scan for weak passwords and credentials reused across entries (no secrets) |
60
+ | `vault_export` / `vault_import` | Portable encrypted backup/migration of the whole vault (separate export password) |
61
+ | `vault_fill` | Find the entry matching a host/URL/username/title and return its credentials |
62
+ | `vault_env` | Render env-flagged entries (tags contain `env`) as `KEY=VALUE` lines |
63
+ | `vault_templates` | List recommended fields for a credential kind (ssh / api-key / oauth / …) |
51
64
 
52
65
  **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
66
 
@@ -132,6 +145,8 @@ The tarball ships prebuilt `lib/` artifacts, so no build step or `allowBuilds` i
132
145
  | `name` | Vault name for the default path (e.g. `name: work` → `$DSH_HOME/vault/work.json`) |
133
146
  | `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
147
  | `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`. |
148
+ | `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. |
149
+ | `exportPasswordEnv` | Environment variable holding the export/import password for `vault_export`/`vault_import` (never pass it as a model argument). |
135
150
 
136
151
  Example:
137
152
 
@@ -144,7 +159,7 @@ Example:
144
159
  autoCapture: true
145
160
  ```
146
161
 
147
- With `autoCapture: true`, when you share a credential in chat (e.g. "my npm token is npm_…"), the assistant offers to store it; on your consent it calls `vault_add` immediately. With `autoCapture` off, credentials are only saved when you explicitly ask. The Settings UI shows the current mode (read-only / ask-before-write / automatic read-write) with a dropdown to switch it, and the empty-vault hint tells you how to get started.
162
+ With `autoCapture: true`, when you share a credential in chat (e.g. "my npm token is npm_…"), the assistant offers to store it; on your consent it calls `vault_add` immediately. With `autoCapture` off, credentials are only saved when you explicitly ask. The Settings UI shows the current mode (read-only / ask-before-write / automatic read-write) with a dropdown to switch it, a kind filter, a health & rotation summary, a trash view, and masked secret fields with a Show/Hide toggle.
148
163
 
149
164
  The vault is created automatically on first tool use; every launch re-unlocks with the master password. **Forgetting the master password = permanent data loss** (no backdoor — by design).
150
165
 
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}.t_j5va_secretField{gap:4px;display:flex}.t_j5va_secretField input{flex:1;min-width:0}.t_j5va_revealButton{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:0 8px;font-size:12px}.t_j5va_kindFilter{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:6px 8px;font-size:13px}.t_j5va_highBadge{color:#e5484d;vertical-align:middle;background:#e5484d26;border-radius:4px;margin-left:6px;padding:1px 6px;font-size:10px;font-weight:600}";
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,43 @@ 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
- "field": "t_j5va_field",
25
- "totp": "t_j5va_totp",
26
- "toolbar": "t_j5va_toolbar",
27
- "row": "t_j5va_row",
20
+ "reportTitle": "t_j5va_reportTitle",
21
+ "secretField": "t_j5va_secretField",
28
22
  "section": "t_j5va_section",
29
- "identity": "t_j5va_identity",
30
- "modeReadonly": "t_j5va_modeReadonly",
31
- "searchBox": "t_j5va_searchBox",
32
- "policyField": "t_j5va_policyField",
33
- "saveButton": "t_j5va_saveButton",
34
- "deleteButton": "t_j5va_deleteButton",
23
+ "totp": "t_j5va_totp",
24
+ "highBadge": "t_j5va_highBadge",
25
+ "editor": "t_j5va_editor",
26
+ "addButton": "t_j5va_addButton",
35
27
  "copied": "t_j5va_copied",
36
28
  "editorBody": "t_j5va_editorBody",
37
- "addButton": "t_j5va_addButton",
29
+ "toolbar": "t_j5va_toolbar",
38
30
  "empty": "t_j5va_empty",
31
+ "kindFilter": "t_j5va_kindFilter",
32
+ "rowActions": "t_j5va_rowActions",
39
33
  "status": "t_j5va_status",
40
- "error": "t_j5va_error",
34
+ "title": "t_j5va_title",
35
+ "modeAuto": "t_j5va_modeAuto",
36
+ "searchBox": "t_j5va_searchBox",
37
+ "modeReadonly": "t_j5va_modeReadonly",
38
+ "deleteButton": "t_j5va_deleteButton",
39
+ "emptyBox": "t_j5va_emptyBox",
40
+ "editorActions": "t_j5va_editorActions",
41
+ "intro": "t_j5va_intro",
42
+ "field": "t_j5va_field",
41
43
  "list": "t_j5va_list",
44
+ "identity": "t_j5va_identity",
45
+ "error": "t_j5va_error",
46
+ "policyField": "t_j5va_policyField",
47
+ "reportLine": "t_j5va_reportLine",
48
+ "saveButton": "t_j5va_saveButton",
49
+ "reportBox": "t_j5va_reportBox",
50
+ "emptyHint": "t_j5va_emptyHint",
51
+ "revealButton": "t_j5va_revealButton",
42
52
  "rowMain": "t_j5va_rowMain",
53
+ "row": "t_j5va_row",
43
54
  "policyBar": "t_j5va_policyBar",
44
- "intro": "t_j5va_intro",
45
- "editorActions": "t_j5va_editorActions",
46
- "editor": "t_j5va_editor",
47
- "title": "t_j5va_title",
48
- "emptyHint": "t_j5va_emptyHint"
55
+ "trashButton": "t_j5va_trashButton",
56
+ "modeAsk": "t_j5va_modeAsk"
49
57
  };
50
58
  //#endregion
51
59
  //#region src/client/VaultSection.tsx
@@ -138,7 +146,7 @@ window.__ModuleLoader__.load({
138
146
  }
139
147
  /** Render the Vault settings section. */
140
148
  function VaultSection(props) {
141
- const { t, config, setAccessMode, list, search, get, add, update, remove, totp } = props;
149
+ const { t, config, setAccessMode, list, search, get, add, update, remove, trash, rotation, health, restore, totp } = props;
142
150
  const searchId = (0, react.useId)();
143
151
  const [query, setQuery] = (0, react.useState)("");
144
152
  const [state, setState] = (0, react.useState)({ status: "loading" });
@@ -149,7 +157,12 @@ window.__ModuleLoader__.load({
149
157
  const [copiedId, setCopiedId] = (0, react.useState)(null);
150
158
  const [codeMap, setCodeMap] = (0, react.useState)({});
151
159
  const [tagsDraft, setTagsDraft] = (0, react.useState)("");
160
+ const [revealed, setRevealed] = (0, react.useState)({});
161
+ const [kindFilter, setKindFilter] = (0, react.useState)("");
152
162
  const [policy, setPolicy] = (0, react.useState)(null);
163
+ const [showTrash, setShowTrash] = (0, react.useState)(false);
164
+ const [trashEntries, setTrashEntries] = (0, react.useState)([]);
165
+ const [report, setReport] = (0, react.useState)(null);
153
166
  const readonly = policy?.accessMode === "readonly";
154
167
  (0, react.useEffect)(() => {
155
168
  let current = true;
@@ -341,25 +354,41 @@ window.__ModuleLoader__.load({
341
354
  }),
342
355
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
343
356
  className: VaultSection_module_css_default.toolbar,
344
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
345
- className: VaultSection_module_css_default.searchBox,
346
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
347
- className: "sr-only",
348
- children: t("searchPlaceholder")
349
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
350
- id: searchId,
351
- type: "search",
352
- placeholder: t("searchPlaceholder"),
353
- value: query,
354
- onChange: (event) => setQuery(event.target.value)
355
- })]
356
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
357
- type: "button",
358
- className: VaultSection_module_css_default.addButton,
359
- onClick: startCreate,
360
- disabled: busy || readonly,
361
- children: ["+ ", t("add")]
362
- })]
357
+ children: [
358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
359
+ className: VaultSection_module_css_default.searchBox,
360
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
361
+ className: "sr-only",
362
+ children: t("searchPlaceholder")
363
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
364
+ id: searchId,
365
+ type: "search",
366
+ placeholder: t("searchPlaceholder"),
367
+ value: query,
368
+ onChange: (event) => setQuery(event.target.value)
369
+ })]
370
+ }),
371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
372
+ className: VaultSection_module_css_default.kindFilter,
373
+ value: kindFilter,
374
+ onChange: (e) => setKindFilter(e.target.value),
375
+ "aria-label": t("fieldKind"),
376
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
377
+ value: "",
378
+ children: t("allKinds")
379
+ }), Object.entries(KIND_KEYS).map(([value, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
380
+ value,
381
+ children: t(key)
382
+ }, value))]
383
+ }),
384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
385
+ type: "button",
386
+ className: VaultSection_module_css_default.addButton,
387
+ onClick: startCreate,
388
+ disabled: busy || readonly,
389
+ children: ["+ ", t("add")]
390
+ })
391
+ ]
363
392
  }),
364
393
  policy !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
365
394
  className: VaultSection_module_css_default.policyBar,
@@ -424,18 +453,95 @@ window.__ModuleLoader__.load({
424
453
  children: readonly ? t("emptyHintReadonly") : t("emptyHint")
425
454
  })]
426
455
  }),
456
+ report !== null && (report.rotation.length > 0 || report.weak.length > 0 || report.reused.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
457
+ className: VaultSection_module_css_default.reportBox,
458
+ children: [
459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
460
+ className: VaultSection_module_css_default.reportTitle,
461
+ children: t("reportTitle")
462
+ }),
463
+ report.rotation.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
464
+ className: VaultSection_module_css_default.reportLine,
465
+ children: [
466
+ t("reportRotation"),
467
+ ": ",
468
+ report.rotation.length
469
+ ]
470
+ }),
471
+ report.weak.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
472
+ className: VaultSection_module_css_default.reportLine,
473
+ children: [
474
+ t("reportWeak"),
475
+ ": ",
476
+ report.weak.length
477
+ ]
478
+ }),
479
+ report.reused.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
480
+ className: VaultSection_module_css_default.reportLine,
481
+ children: [
482
+ t("reportReused"),
483
+ ": ",
484
+ report.reused.length
485
+ ]
486
+ })
487
+ ]
488
+ }),
489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
490
+ type: "button",
491
+ className: VaultSection_module_css_default.trashButton,
492
+ onClick: () => {
493
+ if (!showTrash) trash().then(setTrashEntries);
494
+ setShowTrash(!showTrash);
495
+ },
496
+ children: [showTrash ? t("hideTrash") : t("showTrash"), trashEntries.length > 0 ? ` (${trashEntries.length})` : ""]
497
+ }),
498
+ showTrash && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
499
+ className: VaultSection_module_css_default.list,
500
+ children: [trashEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
501
+ className: VaultSection_module_css_default.row,
502
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
503
+ className: VaultSection_module_css_default.rowMain,
504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
505
+ className: VaultSection_module_css_default.title,
506
+ children: entry.title
507
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
508
+ className: VaultSection_module_css_default.identity,
509
+ children: t("trashed")
510
+ })]
511
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
512
+ className: VaultSection_module_css_default.rowActions,
513
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
514
+ type: "button",
515
+ onClick: () => {
516
+ restore(entry.id).then(() => {
517
+ trash().then(setTrashEntries);
518
+ refresh();
519
+ });
520
+ },
521
+ disabled: busy || readonly,
522
+ children: t("restore")
523
+ })
524
+ })]
525
+ }, entry.id)), trashEntries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
526
+ className: VaultSection_module_css_default.empty,
527
+ children: t("trashEmpty")
528
+ })]
529
+ }),
427
530
  state.status === "ready" && state.entries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
428
531
  className: VaultSection_module_css_default.list,
429
- children: state.entries.map((entry) => {
532
+ children: state.entries.filter((entry) => kindFilter === "" || entry.kind === kindFilter).map((entry) => {
430
533
  const code = codeMap[entry.id];
431
534
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
432
535
  className: VaultSection_module_css_default.row,
433
536
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
434
537
  className: VaultSection_module_css_default.rowMain,
435
538
  children: [
436
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
437
540
  className: VaultSection_module_css_default.title,
438
- children: entry.title
541
+ children: [entry.title, entry.sensitivity === "high" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
542
+ className: VaultSection_module_css_default.highBadge,
543
+ children: t("highSensitivity")
544
+ })]
439
545
  }),
440
546
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
441
547
  className: VaultSection_module_css_default.identity,
@@ -507,8 +613,26 @@ window.__ModuleLoader__.load({
507
613
  ...previous,
508
614
  title: event.target.value
509
615
  }))
616
+ }) : field.key === "password" || field.key === "otpSecret" || field.key === "apiKey" || field.key === "secret" || field.key === "accessToken" || field.key === "refreshToken" || field.key === "privateKey" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
617
+ className: VaultSection_module_css_default.secretField,
618
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
619
+ type: revealed[field.key] ? "text" : "password",
620
+ value: form[field.key] ?? "",
621
+ onChange: (event) => setForm((previous) => ({
622
+ ...previous,
623
+ [field.key]: event.target.value
624
+ }))
625
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
626
+ type: "button",
627
+ className: VaultSection_module_css_default.revealButton,
628
+ onClick: () => setRevealed((previous) => ({
629
+ ...previous,
630
+ [field.key]: !previous[field.key]
631
+ })),
632
+ children: revealed[field.key] ? t("hide") : t("show")
633
+ })]
510
634
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
511
- type: field.key === "password" || field.key === "otpSecret" ? "password" : "text",
635
+ type: "text",
512
636
  value: form[field.key] ?? "",
513
637
  onChange: (event) => setForm((previous) => ({
514
638
  ...previous,
@@ -564,6 +688,19 @@ window.__ModuleLoader__.load({
564
688
  modeAutoHint: "Automatic read-write — no per-call prompt",
565
689
  autoCaptureOn: "auto-capture on",
566
690
  autoCaptureOff: "auto-capture off",
691
+ reportTitle: "Health & rotation",
692
+ reportRotation: "expired / due for rotation",
693
+ reportWeak: "weak passwords",
694
+ reportReused: "credentials reused across entries",
695
+ showTrash: "Trash",
696
+ hideTrash: "Hide trash",
697
+ trashed: "in trash",
698
+ trashEmpty: "Trash is empty",
699
+ restore: "Restore",
700
+ show: "Show",
701
+ hide: "Hide",
702
+ allKinds: "All kinds",
703
+ highSensitivity: "HIGH",
567
704
  add: "Add credential",
568
705
  cancel: "Cancel",
569
706
  save: "Save",
@@ -616,6 +753,19 @@ window.__ModuleLoader__.load({
616
753
  modeAutoHint: "自动读写 —— 无需逐次确认",
617
754
  autoCaptureOn: "自动捕获已开启",
618
755
  autoCaptureOff: "自动捕获已关闭",
756
+ reportTitle: "健康与轮换",
757
+ reportRotation: "已过期 / 待轮换",
758
+ reportWeak: "弱密码",
759
+ reportReused: "跨条目复用的凭据",
760
+ showTrash: "回收站",
761
+ hideTrash: "隐藏回收站",
762
+ trashed: "已删除",
763
+ trashEmpty: "回收站为空",
764
+ restore: "恢复",
765
+ show: "显示",
766
+ hide: "隐藏",
767
+ allKinds: "全部类型",
768
+ highSensitivity: "高敏",
619
769
  add: "新增凭据",
620
770
  cancel: "取消",
621
771
  save: "保存",
@@ -693,6 +843,10 @@ window.__ModuleLoader__.load({
693
843
  patch
694
844
  }),
695
845
  remove: (id) => invoke("delete", { id }),
846
+ trash: () => invoke("trash").then((r) => r.entries),
847
+ rotation: () => invoke("rotation").then((r) => r.entries),
848
+ health: () => invoke("health"),
849
+ restore: (id) => invoke("restore", { id }),
696
850
  totp: (id) => invoke("totp", { id })
697
851
  });
698
852
  ctx.slots.inject("settings.section", () => ctx.slots.register({