dsh-vault 1.10.73 → 1.10.74-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README-zh.md CHANGED
@@ -8,6 +8,8 @@
8
8
  [![已收录: awesome-dsh-plugin](https://img.shields.io/badge/已收录-awesome--dsh--plugin-2ea44f)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/README.zh.md#L2869)
9
9
  [![已收录: awesome-deepseek-harness](https://img.shields.io/badge/已收录-awesome--deepseek--harness-2ea44f)](https://github.com/Dominic789654/awesome-deepseek-harness/blob/main/README.zh-CN.md#L903)
10
10
 
11
+ [English](README.md) | **中文**
12
+
11
13
  dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件:把你在使用 AI 过程中产生的**用户名、邮箱、手机号、密码、二次动态密钥(TOTP)** 以及开发工作流中常用的 **SSH 连接、API Key、Secret、OAuth access/refresh token** 等敏感凭据加密存储,并通过模型工具提供增删改查、检索、密码生成与动态验证码生成能力。
12
14
 
13
15
  **安全性与实现**
@@ -225,7 +227,7 @@ echo "$DEMO_API_KEY"
225
227
  两个前提要知道:
226
228
 
227
229
  - **只有打了 `env` 标签的条目才会被导出。** 一个都没打时,输出的是
228
- "no entries are tagged for environment export";先在 UI 里给条目的标签加上 `env`,或让助手执行 `vault_update { id, tags: ["env"] }`。
230
+ `dsh-vault: nothing to export — no entry carries the "env" tag.`;先在 UI 里给条目的标签加上 `env`,或让助手执行 `vault_update { id, tags: ["env"] }`。
229
231
  - 名字默认由标题 + 字段推导(`DASHSCOPE` + `apiKey` → `DASHSCOPE_API_KEY`);想固定名字就在条目的 `envKeys` 里指定 —— 见[环境变量导出](#环境变量导出)。
230
232
 
231
233
 
@@ -237,7 +239,7 @@ echo "$DEMO_API_KEY"
237
239
  | `env` | **命令** | 打印带该标签的条目 |
238
240
  | `envKeys` | 条目的**字段**(可选) | 固定确切名字,例如 `["DASHSCOPE_API_KEY"]`;旧的单数写法 `envKey` 仍作为"只写一个"的简写接受 |
239
241
 
240
- 除此之外没有任何叫 `env*` 的东西:`list --json` / `show` 只报 `envKeys`(该条目会导出的名字,也就是 `get` 接受的名字)与 `envTagged`(是否被 `env` 包含)。
242
+ 除此之外没有任何叫 `env*` 的东西:`list --json` / `show` 只报 `envKeys`(该条目会导出的名字,也就是 `get` 接受的名字)、`envTagged`(是否被 `env` 包含)与 `hasTotp`(`totp` 能否为它生成动态码)。
241
243
 
242
244
  ### 2. 有哪些命令
243
245
 
@@ -251,6 +253,8 @@ dsh-vault get my-entry # 取该条目的主密钥,仅 stdout
251
253
  dsh-vault get my-entry --field apiKey # 取指定字段
252
254
  dsh-vault get my-entry --mask # 只确认存在,不打印值
253
255
  dsh-vault show my-entry # 非敏感字段的 JSON
256
+ dsh-vault totp my-entry # 当前的 TOTP 动态码(只有 6 位数字)
257
+ dsh-vault totp my-entry --json # 附带 digits / period / secondsRemaining
254
258
  dsh-vault env # 带 env 标签的条目 → KEY=VALUE
255
259
  dsh-vault env --mask # 人类视角,两段都是 KEY=VALUE:
256
260
  # ## exported items
@@ -263,6 +267,8 @@ dsh-vault verify # 校验主密码,stdout 无输出
263
267
 
264
268
  密钥只走 stdout,其余(进度、错误)走 stderr,所以 `$(dsh-vault get …)` 与管道都正常;退出码 `0` 成功、`1` 运行错误、`2` 用法错误。
265
269
 
270
+ `totp` 是为了让脚本能自动完成两步验证、又完全不碰长期密钥:`code=$(dsh-vault totp my-entry)` 只输出六位数字。如果你用 `get my-entry --field otpSecret`,打印的是**密钥本身** —— 只有确实要把密钥导入别处时才那么做。`--json` 会多给一个 `secondsRemaining`,脚本据此等一个新窗口,而不是硬闯即将过期的码。
271
+
266
272
  ### 3. shell 引号:不是 `export $(…)`
267
273
 
268
274
  正确的是 `eval "$(dsh-vault env)"`;`export $(dsh-vault env)` 是错的。命令替换会**按空白拆分**,而且**不会**去掉 CLI 输出的引号 —— 值里只要有一个空格就会被拆成多个词,所有值还会带上字面引号:
@@ -320,7 +326,7 @@ npm i -g dsh-vault && dsh-vault list # 全局
320
326
  npx dsh-vault list # 或者干脆不装
321
327
  ```
322
328
 
323
- 读命令(`list`、`get`、`env`、`show`、`verify`、`export-env`)只依赖 Node,所以全局与 `npx` 都能直接用;**只有写保险库**才需要 harness 运行时,而插件环境一定有。
329
+ 读命令(`list`、`get`、`env`、`show`、`totp`、`verify`、`export-env`)只依赖 Node,所以全局与 `npx` 都能直接用;**只有写保险库**才需要 harness 运行时,而插件环境一定有。
324
330
 
325
331
  profile 这条路还有两个注意点:垫片可能被之后在该目录执行的 `pnpm install` 清掉;`pnpm dsh plugin --profile web add dsh-vault` 会依据包的 `dsh.bundle` 声明重整 `dsh.profile.bundles` —— 如果你自己的 patch 层**也**插入了这个插件,就会**挂两份**,要先删掉 patch 层那一行。
326
332
 
@@ -401,10 +407,33 @@ k-anonymity **没有**隐藏的东西,直说:
401
407
  | **完整** | 全部,含批量导入导出、浏览器会话、备份与保险库文件操作 |
402
408
  | **自定义…** | 基础 + 任选: 管理 / 导入导出 / 浏览器会话 / 备份与文件 |
403
409
 
404
- 也可在插件配置里写 `tools: basic|standard|full|custom`;界面选择优先,并保存在 `<vault 目录>/access.json`。
410
+ 也可在插件配置里写 `tools: basic|standard|full|custom`;界面选择优先,并保存在 `<vault 目录>/tools.json`(工具集是进程级设置,不随保险库变化)。
405
411
 
406
412
  ## 配置
407
413
 
414
+ > [!WARNING] 安全:不要把主密码明文写进配置
415
+ > profile patch(`cordis.patch.yml` / `cordis.yml`)是很容易被提交进 git、截图或进日志的配置文件 ——
416
+ > 一行明文的 `masterPassword:` 就等于泄露了整个加密保险库的钥匙。请一律使用
417
+ > `masterPasswordEnv` 并把真实密码放进进程/ shell 环境(例如 `export DSH_VAULT_PASSWORD='…'`)。
418
+ > 凭据类插件的证据化检查清单见
419
+ > [community-plugin-audit handbook](https://github.com/sandbaseai/deepseek-harness-handbook/blob/main/docs/en/security/community-plugin-audit.md)。
420
+ >
421
+ > ```yaml
422
+ > # ✗ 不要 —— 配置文件里写明文主密码
423
+ > - id: vault
424
+ > name: dsh-vault
425
+ > config:
426
+ > masterPassword: 'my-secret'
427
+ > ```
428
+ >
429
+ > ```yaml
430
+ > # ✓ 应该 —— 引用环境变量,并让文件本身保持 0600
431
+ > - id: vault
432
+ > name: dsh-vault
433
+ > config:
434
+ > masterPasswordEnv: DSH_VAULT_PASSWORD
435
+ > ```
436
+
408
437
  | 配置项 | 说明 |
409
438
  |---|---|
410
439
  | `masterPassword` | 直接配置主密码(会出现在 cordis.yml 中,不推荐) |
@@ -441,7 +470,7 @@ git clone git@github.com:Ox0400/dsh-vault.git
441
470
  cd dsh-vault
442
471
  pnpm install # 安装 devDependencies(typescript/tsdown/vitest 等)
443
472
  pnpm build # 构建 host 侧 lib/*.js 与浏览器 bundle lib/client.js
444
- pnpm test # 运行 420 项 vitest 测试
473
+ pnpm test # 运行 489 项 vitest 测试
445
474
  ```
446
475
 
447
476
  > 测试需要 harness 的 `dsh-llm`/`dsh-system-prompt` 等 peer 包,在 harness monorepo 内开发时由 workspace 链接提供。
@@ -449,7 +478,7 @@ pnpm test # 运行 420 项 vitest 测试
449
478
  常用命令:
450
479
 
451
480
  ```sh
452
- # 单元 + 集成测试(vitest,420 项)
481
+ # 单元 + 集成测试(vitest,489 项)
453
482
  pnpm test # 或 npx vitest run
454
483
 
455
484
  # 类型检查
@@ -462,7 +491,7 @@ pnpm build # = build:host (tsc) + build:client (tsdown)
462
491
  npm pack
463
492
  ```
464
493
 
465
- 仓库内所有测试通过:420/420(crypto/TOTP/密码生成/store CRUD/网关/集成)。
494
+ 仓库内所有测试通过:489/489(crypto/TOTP/密码生成/store CRUD/网关/集成)。
466
495
 
467
496
  浏览器侧检查跑在真实 `dsh web` 上(不属于 vitest),且**一律拒绝在 default 库上运行**:
468
497
 
@@ -504,7 +533,8 @@ UI 不写死颜色,而是读取宿主的设计变量:本地语义层映射
504
533
  - `dsh.bundle.patch` → `cordis.patch.yml`(安装到 profile 后自动应用的 layer)
505
534
  - `dsh.client` → 浏览器端声明(`exports["./client"]` 指向 `lib/client.js`)
506
535
  - `prepare` 脚本 → git 安装时自包含构建(`tsc` host + `tsdown` client)
507
- - 运行时依赖全部走 `peerDependencies`(由宿主 harness 提供,避免重复实例)
536
+ - 运行时依赖只有 `playwright-core`(懒加载,仅浏览器登录会话使用);其余全部由宿主通过
537
+ `peerDependencies` 提供,不会产生重复实例
508
538
 
509
539
  可选发布途径:
510
540
 
package/README.md CHANGED
@@ -80,7 +80,7 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
80
80
  | `vault_duplicates` | Find duplicate groups: `mode` = `both` (default) / `title` / `content` (no secrets) |
81
81
  | `vault_report` | Printable inventory with expiry/rotation columns and a stats footer (no secrets) |
82
82
  | `vault_export` / `vault_import` | Portable encrypted backup/migration of the whole vault (separate export password) |
83
- | `vault_backup` / `vault_backup_now` | Timestamped encrypted backup named `<vault>-backups-YYYY-MM-DD_HH-MM-SS-<hex>.json` (owning vault + date visible); retention pruning keeps the newest N |
83
+ | `vault_backup` / `vault_backup_now` | Timestamped encrypted backup named `<vault>-backups-YYYY-MM-DD_HH-MM-SS-<hex>.json` (owning vault + date visible); retention pruning keeps the newest N (`maxBackups`, default 10) |
84
84
  | `vault_restore_backup` | Restore from a backup: `mode: "merge"` (default) copies the backup entries INTO the current vault so they appear in the entries list; `mode: "replace"` overwrites the whole vault with a safety snapshot first |
85
85
  | `vault_vault_rename` / `vault_vault_delete` | Rename a named vault (file moves, active session follows) or permanently delete one (default vault protected) |
86
86
  | `vault_match_url` | Find login entries matching a URL (Bitwarden/1Password-style: exact host, subdomain, parent domain, path-prefix; www./port normalization) with a 0–100 score — never returns the password |
@@ -237,7 +237,7 @@ echo "$DEMO_API_KEY"
237
237
  Two things worth knowing:
238
238
 
239
239
  - **Only `env`-tagged entries are exported.** With none tagged you get
240
- "no entries are tagged for environment export"; tag one in the UI (the
240
+ `dsh-vault: nothing to export — no entry carries the "env" tag.`; tag one in the UI (the
241
241
  entry's tags field) or with `vault_update { id, tags: ["env"] }`.
242
242
  - The name is derived from the title and field (`DASHSCOPE` + `apiKey` →
243
243
  `DASHSCOPE_API_KEY`) unless the entry sets `envKeys` — see
@@ -253,7 +253,8 @@ Two things worth knowing:
253
253
  | `envKeys` | an entry **field** (optional) | pin exact names, e.g. `["DASHSCOPE_API_KEY"]`; the old singular `envKey` is accepted as a one-element shorthand |
254
254
 
255
255
  Nothing else is called `env*`: `list --json` / `show` report `envKeys` (the names
256
- an entry exports — what `get` accepts) and `envTagged` (whether `env` includes it).
256
+ an entry exports — what `get` accepts), `envTagged` (whether `env` includes it)
257
+ and `hasTotp` (whether `totp` can produce a code for it).
257
258
 
258
259
  ### 2. The commands
259
260
 
@@ -267,6 +268,8 @@ dsh-vault get my-entry # the entry's primary secret, stdout
267
268
  dsh-vault get my-entry --field apiKey # one named field
268
269
  dsh-vault get my-entry --mask # confirm it exists without printing it
269
270
  dsh-vault show my-entry # non-secret metadata as JSON
271
+ dsh-vault totp my-entry # the current TOTP code (digits only)
272
+ dsh-vault totp my-entry --json # + digits, period, secondsRemaining
270
273
  dsh-vault env # env-tagged entries as KEY=VALUE
271
274
  dsh-vault env --mask # human view, both sections as KEY=VALUE:
272
275
  # ## exported items
@@ -281,6 +284,13 @@ Secrets go to stdout and everything else (progress, errors) to stderr, so
281
284
  `$(dsh-vault get …)` and pipelines behave. Exit codes: `0` ok, `1` runtime
282
285
  error, `2` usage error.
283
286
 
287
+ `totp` exists so a script can automate two-factor login without ever handling the
288
+ long-lived secret: `code=$(dsh-vault totp my-entry)` prints six digits and nothing
289
+ else. `get my-entry --field otpSecret` would print the **secret** instead — use it
290
+ only when a tool genuinely needs to enrol the secret elsewhere. `--json` adds
291
+ `secondsRemaining`, which is what lets a caller wait for a fresh window rather
292
+ than race an expiring code.
293
+
284
294
  ### 3. Shell quoting: not `export $(…)`
285
295
 
286
296
  `eval "$(dsh-vault env)"` is right; `export $(dsh-vault env)` is wrong. Command
@@ -349,7 +359,7 @@ npm i -g dsh-vault && dsh-vault list # globally
349
359
  npx dsh-vault list # or not at all
350
360
  ```
351
361
 
352
- The read commands (`list`, `get`, `env`, `show`, `verify`, `export-env`) depend
362
+ The read commands (`list`, `get`, `env`, `show`, `totp`, `verify`, `export-env`) depend
353
363
  on Node alone, which is why the global and `npx` forms work. Only *writing* to a
354
364
  vault needs the harness runtime, and the plugin always has it.
355
365
 
@@ -464,7 +474,7 @@ Switch profiles in **Settings → Credentials → Permissions → Model tools**
464
474
  | **Full** | everything, incl. bulk import/export, browser sessions, backups and vault-file operations |
465
475
  | **Custom…** | core + any of: Management / Import-export / Browser sessions / Backups & files |
466
476
 
467
- `tools: basic|standard|full|custom` can also be set in the plugin config; the UI choice wins and is stored in `<vault dir>/access.json`.
477
+ `tools: basic|standard|full|custom` can also be set in the plugin config; the UI choice wins and is stored in `<vault dir>/tools.json` (the process-wide tool catalog is not per vault).
468
478
 
469
479
  ## Configuration
470
480
 
@@ -529,7 +539,7 @@ git clone git@github.com:Ox0400/dsh-vault.git
529
539
  cd dsh-vault
530
540
  pnpm install # installs devDependencies (typescript/tsdown/vitest, …)
531
541
  pnpm build # builds host lib/*.js and the browser bundle lib/client.js
532
- pnpm test # runs the 420 vitest tests
542
+ pnpm test # runs the 489 vitest tests
533
543
  ```
534
544
 
535
545
  > Tests need harness peer packages such as `dsh-llm`/`dsh-system-prompt`; inside the harness monorepo these resolve via workspace links.
@@ -537,13 +547,13 @@ pnpm test # runs the 420 vitest tests
537
547
  Common commands:
538
548
 
539
549
  ```sh
540
- pnpm test # unit + integration tests (vitest, 420)
550
+ pnpm test # unit + integration tests (vitest, 489)
541
551
  pnpm typecheck # tsc -p tsconfig.json --noEmit
542
552
  pnpm build # = build:host (tsc) + build:client (tsdown)
543
553
  npm pack # optional: tarball for `dsh plugin add ./dsh-vault-0.1.1.tgz`
544
554
  ```
545
555
 
546
- All 420 tests pass (crypto / TOTP / password generation / store CRUD / gateway / integration).
556
+ All 489 tests pass (crypto / TOTP / password generation / store CRUD / gateway / integration).
547
557
 
548
558
  Browser checks run against a real `dsh web` instead of vitest, and every one of
549
559
  them refuses to operate on the default vault:
@@ -601,7 +611,8 @@ This package is a standard npm bundle:
601
611
  - `dsh.bundle.patch` → `cordis.patch.yml` (the layer applied automatically when a profile lists this bundle)
602
612
  - `dsh.client` → browser-side declaration (`exports["./client"]` points at `lib/client.js`)
603
613
  - `prepare` script → self-contained build on git install (`tsc` host + `tsdown` client)
604
- - Runtime dependencies are all `peerDependencies` (provided by the host harness — no duplicate instances)
614
+ - Runtime dependencies: only `playwright-core` (lazily `require`d, used by browser login sessions).
615
+ Everything else the host provides via `peerDependencies`, so there are no duplicate instances
605
616
 
606
617
  Distribution options:
607
618
 
package/lib/cli.js CHANGED
@@ -25,6 +25,7 @@ import { createInterface } from 'node:readline';
25
25
  import { ReadStream } from 'node:tty';
26
26
  import { openVault, defaultVaultPath } from "./store.js";
27
27
  import { envLinesFor, envPairsForEntry, maskSecret, primarySecret, shellQuote } from "./env-export.js";
28
+ import { parseTotpSecret, totp } from "./totp.js";
28
29
  function packageVersion() {
29
30
  try {
30
31
  const require = createRequire(import.meta.url);
@@ -44,8 +45,9 @@ Commands:
44
45
  list List entries with their env name (never secrets)
45
46
  get <id|title|ENVKEY> Print one field of an entry (default: its main secret)
46
47
  show <id|title|ENVKEY> Print an entry's non-secret fields as JSON
48
+ totp <id|title|ENVKEY> Print the entry's current TOTP code (needs otpSecret)
47
49
  env Print env-TAGGED entries as KEY=VALUE lines (an entry
48
- must carry the tag "env"; "list" marks those [env]).
50
+ must carry the tag "env"; "list" groups those first).
49
51
  One value only? "get <entry>" needs no tag.
50
52
  export-env <path> Write those lines to a file (mode 0600)
51
53
  verify Check the master password (nothing on stdout)
@@ -61,13 +63,13 @@ Options:
61
63
  list options:
62
64
  --kind <k> Only entries of this kind
63
65
  --tag <t> Only entries carrying this tag
64
- (each row also shows the env name that "get" accepts,
65
- the count of further keys in parentheses, and [env]
66
- when the entry is included in "dsh-vault env")
66
+ (each row also shows the env name that "get" accepts
67
+ and the count of further keys in parentheses)
67
68
 
68
69
  JSON output ("list --json", "show") reports per entry:
69
70
  envKeys the env names it exports (what "get" accepts)
70
71
  envTagged whether "dsh-vault env" includes it
72
+ hasTotp whether "totp <entry>" can produce a code
71
73
 
72
74
  get options:
73
75
  --field <name> apiKey | secret | accessToken | refreshToken | privateKey
@@ -76,6 +78,14 @@ get options:
76
78
  --mask Print a masked value (first 4 chars + ***)
77
79
  --all Print the whole entry as JSON (every secret — opt-in)
78
80
 
81
+ totp options:
82
+ (none beyond the common options; --json adds digits, period and secondsRemaining)
83
+
84
+ The code is the only thing on stdout, so 2FA can be automated:
85
+ code=$(dsh-vault totp TwoFactor)
86
+ "get <entry> --field otpSecret" prints the long-lived SECRET instead — use
87
+ "totp" when a script needs a code, so the secret stays out of logs and history.
88
+
79
89
  env options:
80
90
  --prefix <P> Prefix for derived key names (an explicit envKey is kept verbatim)
81
91
  --kind <k> Only entries of this kind
@@ -219,7 +229,13 @@ async function promptHidden(prompt) {
219
229
  }
220
230
  async function readStdinLine() {
221
231
  const rl = createInterface({ input: process.stdin });
222
- const line = await new Promise(res => rl.once('line', res));
232
+ // Zero-byte input (</dev/null, a `pass` lookup that failed, a CI step with no
233
+ // secret set) emits `close` but never `line`, so waiting only for `line` hung
234
+ // forever and the empty-password check below was unreachable.
235
+ const line = await new Promise(res => {
236
+ rl.once('line', res);
237
+ rl.once('close', () => res(''));
238
+ });
223
239
  rl.close();
224
240
  return line;
225
241
  }
@@ -255,6 +271,11 @@ async function resolvePassword(parsed, io) {
255
271
  * prints exactly the value `env` would emit for that key.
256
272
  */
257
273
  export function resolveEntry(store, needle) {
274
+ // `''` is a substring of every title, so an empty argument used to match a
275
+ // single entry and print its secret with exit 0.
276
+ if (needle.trim().length === 0) {
277
+ throw new Error('an empty name matches every entry — pass an id, a title, an envKey or an exported key');
278
+ }
258
279
  const entries = store.list();
259
280
  const byId = entries.find(e => e.id === needle);
260
281
  if (byId !== undefined)
@@ -324,6 +345,9 @@ function publicFields(entry) {
324
345
  out[key] = value;
325
346
  }
326
347
  out.hasSecret = primarySecret(entry) !== undefined;
348
+ // Presence only — the same level of detail as hasSecret, so a script can find
349
+ // the entries `totp` accepts without pulling any secret into a transcript.
350
+ out.hasTotp = typeof entry.otpSecret === 'string' && entry.otpSecret.length > 0;
327
351
  // ONE name for "the env names this entry exports": `envKeys`. It used to be a
328
352
  // configured `envKeys` plus a computed `exportedKeys`, which read as two
329
353
  // similar things — the computed list is the useful one (it already starts with
@@ -333,7 +357,7 @@ function publicFields(entry) {
333
357
  out.envTagged = (entry.tags ?? []).includes('env');
334
358
  return out;
335
359
  }
336
- const COMMANDS = new Set(['list', 'get', 'show', 'env', 'export-env', 'verify']);
360
+ const COMMANDS = new Set(['list', 'get', 'show', 'totp', 'env', 'export-env', 'verify']);
337
361
  /** Run one CLI invocation; returns the process exit code. */
338
362
  export async function runCli(argv, io) {
339
363
  const parsed = parseArgs(argv);
@@ -359,6 +383,7 @@ export async function runCli(argv, io) {
359
383
  list: ['kind', 'tag'],
360
384
  get: ['field', 'fields', 'mask', 'all'],
361
385
  show: [],
386
+ totp: [],
362
387
  env: ['prefix', 'kind', 'keys-only', 'mask', 'explain', 'file'],
363
388
  'export-env': ['prefix', 'kind', 'keys-only'],
364
389
  verify: [],
@@ -371,6 +396,15 @@ export async function runCli(argv, io) {
371
396
  io.err(`accepted: ${[...allowed].map(f => `--${f}`).join(' ')}\n`);
372
397
  return 2;
373
398
  }
399
+ // `get my-entry --field` (no value) set the flag to `true`, which stringFlag
400
+ // reads as "not given" — so the command quietly printed the primary secret
401
+ // instead of the field the user asked for.
402
+ const needsValue = ['field', 'fields', 'kind', 'tag', 'prefix', 'file', 'vault', 'path'];
403
+ const valueless = needsValue.filter(name => parsed.flags.get(name) === true);
404
+ if (valueless.length > 0) {
405
+ io.err(`dsh-vault: ${valueless.map(f => `--${f}`).join(', ')} needs a value\n`);
406
+ return 2;
407
+ }
374
408
  const json = parsed.flags.get('json') === true;
375
409
  const mask = parsed.flags.get('mask') === true;
376
410
  const keysOnly = parsed.flags.get('keys-only') === true;
@@ -469,15 +503,64 @@ export async function runCli(argv, io) {
469
503
  else {
470
504
  value = fieldValue(entry, name);
471
505
  }
472
- if (typeof value !== 'string' || value.length === 0) {
506
+ // Absent, empty and non-string are three different answers. Reporting all
507
+ // of them as "has no field" made `--field expiresAt` (a number) and
508
+ // `--field tags` (an array) look like fields that do not exist.
509
+ if (value === undefined) {
473
510
  io.err(`dsh-vault: "${entry.title}" has no field "${name}"\n`);
474
511
  return 1;
475
512
  }
513
+ if (value === null || (typeof value === 'string' && value.length === 0)) {
514
+ io.err(`dsh-vault: "${entry.title}" field "${name}" is empty\n`);
515
+ return 1;
516
+ }
476
517
  const textual = name === 'username' || name === 'url' || name === 'notes' || name.startsWith('fields.');
477
- const shown = mask && !textual ? maskSecret(value) : value;
518
+ // Strings keep the old masking rule; anything else is printed as JSON so
519
+ // booleans, timestamps and tags stay usable from a script.
520
+ const shown = typeof value === 'string'
521
+ ? (mask && !textual ? maskSecret(value) : value)
522
+ : JSON.stringify(value);
478
523
  io.out(json ? `${JSON.stringify({ id: entry.id, title: entry.title, field: name, value: shown })}\n` : `${shown}\n`);
479
524
  return 0;
480
525
  }
526
+ if (parsed.command === 'totp') {
527
+ const needle = parsed.positional[0];
528
+ if (needle === undefined) {
529
+ io.err('dsh-vault: totp needs an entry id, title or env key\n');
530
+ return 2;
531
+ }
532
+ const entry = resolveEntry(store, needle).entry;
533
+ const secret = typeof entry.otpSecret === 'string' && entry.otpSecret.length > 0 ? entry.otpSecret : undefined;
534
+ if (secret === undefined) {
535
+ io.err(`dsh-vault: "${entry.title}" has no TOTP secret\n`);
536
+ io.err('hint: "show <entry>" lists non-secret fields; the secret goes in the entry\'s otpSecret field.\n');
537
+ return 1;
538
+ }
539
+ const nowMs = Date.now();
540
+ let parsedSecret;
541
+ let code;
542
+ try {
543
+ parsedSecret = parseTotpSecret(secret);
544
+ code = totp(secret, nowMs);
545
+ }
546
+ catch (err) {
547
+ io.err(`dsh-vault: "${entry.title}" has an unusable otpSecret: ${err instanceof Error ? err.message : String(err)}\n`);
548
+ return 1;
549
+ }
550
+ if (!json) {
551
+ // Digits only: the whole point is `code=$(dsh-vault totp …)` in a script.
552
+ io.out(`${code}\n`);
553
+ return 0;
554
+ }
555
+ // `secondsRemaining` is what lets a caller wait for a fresh window instead
556
+ // of racing an expiring code, which is the classic 2FA automation bug.
557
+ const secondsRemaining = parsedSecret.periodSeconds - Math.floor(nowMs / 1000) % parsedSecret.periodSeconds;
558
+ io.out(`${JSON.stringify({
559
+ id: entry.id, title: entry.title, code,
560
+ digits: parsedSecret.digits, period: parsedSecret.periodSeconds, secondsRemaining,
561
+ })}\n`);
562
+ return 0;
563
+ }
481
564
  // env / export-env
482
565
  const prefix = stringFlag(parsed, 'prefix') ?? '';
483
566
  const kind = stringFlag(parsed, 'kind');
@@ -560,7 +643,7 @@ export async function runCli(argv, io) {
560
643
  io.err(' Tag one entry and run it again:\n');
561
644
  io.err(' UI: Settings → Credentials → the entry → tags → env\n');
562
645
  io.err(' assistant: vault_update { id, tags: ["env"] }\n');
563
- io.err(' `dsh-vault list` marks tagged entries with [env] and shows the name each exports.\n');
646
+ io.err(' `dsh-vault list` groups tagged entries under "Exported by `env`" and shows each name.\n');
564
647
  io.err(' Only need one value? `dsh-vault get <entry>` needs no tag at all.\n');
565
648
  }
566
649
  return 0;
package/lib/client.js CHANGED
@@ -510,197 +510,197 @@ window.__ModuleLoader__.load({
510
510
  document.head.appendChild(tag);
511
511
  }
512
512
  var VaultSection_module_css_default = {
513
- "detailBox": "t_j5va_detailBox",
513
+ "auditChipActive": "t_j5va_auditChipActive",
514
+ "badgeBtn": "t_j5va_badgeBtn",
515
+ "trashButton": "t_j5va_trashButton",
516
+ "pwHistPwd": "t_j5va_pwHistPwd",
517
+ "detailTotp": "t_j5va_detailTotp",
514
518
  "secretField": "t_j5va_secretField",
515
- "pwBar": "t_j5va_pwBar",
516
- "customKeyInput": "t_j5va_customKeyInput",
517
- "riskDanger": "t_j5va_riskDanger",
518
- "strengthStars": "t_j5va_strengthStars",
519
- "pwWeak": "t_j5va_pwWeak",
519
+ "saveButton": "t_j5va_saveButton",
520
+ "error": "t_j5va_error",
521
+ "modeAuto": "t_j5va_modeAuto",
522
+ "checkField": "t_j5va_checkField",
523
+ "editorActions": "t_j5va_editorActions",
524
+ "dangerButton": "t_j5va_dangerButton",
525
+ "pwFair": "t_j5va_pwFair",
526
+ "tabActive": "t_j5va_tabActive",
527
+ "toolbar": "t_j5va_toolbar",
528
+ "histAdd": "t_j5va_histAdd",
529
+ "lenChipActive": "t_j5va_lenChipActive",
530
+ "emptyBox": "t_j5va_emptyBox",
531
+ "reportBox": "t_j5va_reportBox",
532
+ "pwMeter": "t_j5va_pwMeter",
533
+ "tabs": "t_j5va_tabs",
534
+ "detailRemain": "t_j5va_detailRemain",
535
+ "recentSearch": "t_j5va_recentSearch",
536
+ "recentLabel": "t_j5va_recentLabel",
537
+ "tagRow": "t_j5va_tagRow",
538
+ "tagCount": "t_j5va_tagCount",
539
+ "recentChip": "t_j5va_recentChip",
540
+ "tagManagerHead": "t_j5va_tagManagerHead",
541
+ "pwHistRow": "t_j5va_pwHistRow",
542
+ "status": "t_j5va_status",
543
+ "kindIcon": "t_j5va_kindIcon",
520
544
  "importActions": "t_j5va_importActions",
521
- "genOpts": "t_j5va_genOpts",
522
- "vaultTarget": "t_j5va_vaultTarget",
523
- "vaultManageRow": "t_j5va_vaultManageRow",
524
- "importName": "t_j5va_importName",
525
- "iconFieldWrap": "t_j5va_iconFieldWrap",
526
- "copied": "t_j5va_copied",
527
- "field": "t_j5va_field",
545
+ "reportLine": "t_j5va_reportLine",
546
+ "strengthTrack": "t_j5va_strengthTrack",
528
547
  "rowExpired": "t_j5va_rowExpired",
529
- "dangerButton": "t_j5va_dangerButton",
530
- "auditChips": "t_j5va_auditChips",
531
- "reportTitle": "t_j5va_reportTitle",
532
- "healthBar": "t_j5va_healthBar",
533
- "pwStrong": "t_j5va_pwStrong",
534
- "tagName": "t_j5va_tagName",
535
- "healthSummary": "t_j5va_healthSummary",
536
- "lenChip": "t_j5va_lenChip",
537
- "genOptRow": "t_j5va_genOptRow",
538
- "exportChips": "t_j5va_exportChips",
539
- "riskBanner": "t_j5va_riskBanner",
540
- "searchBox": "t_j5va_searchBox",
541
548
  "list": "t_j5va_list",
542
- "moreButton": "t_j5va_moreButton",
543
- "genHist": "t_j5va_genHist",
544
- "reportLine": "t_j5va_reportLine",
545
- "kindChip": "t_j5va_kindChip",
549
+ "bulkItem": "t_j5va_bulkItem",
550
+ "auditIssues": "t_j5va_auditIssues",
551
+ "pwBar": "t_j5va_pwBar",
552
+ "exportChips": "t_j5va_exportChips",
546
553
  "customEq": "t_j5va_customEq",
547
- "detailActions": "t_j5va_detailActions",
548
- "identity": "t_j5va_identity",
549
- "auditRow": "t_j5va_auditRow",
554
+ "pwHistBox": "t_j5va_pwHistBox",
555
+ "auditChip": "t_j5va_auditChip",
556
+ "kindChip": "t_j5va_kindChip",
557
+ "rowMain": "t_j5va_rowMain",
558
+ "favToggle": "t_j5va_favToggle",
559
+ "policyField": "t_j5va_policyField",
560
+ "lenChip": "t_j5va_lenChip",
561
+ "copyAllBtn": "t_j5va_copyAllBtn",
550
562
  "toolbarDivider": "t_j5va_toolbarDivider",
551
- "bulkCount": "t_j5va_bulkCount",
552
- "histAdd": "t_j5va_histAdd",
553
- "pinOff": "t_j5va_pinOff",
554
- "brandGlyph": "t_j5va_brandGlyph",
555
- "detailItem": "t_j5va_detailItem",
556
- "empty": "t_j5va_empty",
557
- "kindIcon": "t_j5va_kindIcon",
558
- "auditWhen": "t_j5va_auditWhen",
559
- "auditChipActive": "t_j5va_auditChipActive",
560
- "badge": "t_j5va_badge",
563
+ "importRow": "t_j5va_importRow",
564
+ "field": "t_j5va_field",
565
+ "emojiPicker": "t_j5va_emojiPicker",
566
+ "actionGroup": "t_j5va_actionGroup",
567
+ "strengthStrong": "t_j5va_strengthStrong",
568
+ "vaultSwitch": "t_j5va_vaultSwitch",
569
+ "customFieldRows": "t_j5va_customFieldRows",
570
+ "inlineField": "t_j5va_inlineField",
561
571
  "rowCheck": "t_j5va_rowCheck",
562
- "riskWarn": "t_j5va_riskWarn",
563
- "attachAdd": "t_j5va_attachAdd",
564
- "tagRow": "t_j5va_tagRow",
572
+ "srOnly": "t_j5va_srOnly",
573
+ "modeReadonly": "t_j5va_modeReadonly",
574
+ "histUpdate": "t_j5va_histUpdate",
575
+ "clearButton": "t_j5va_clearButton",
576
+ "dueBadge": "t_j5va_dueBadge",
577
+ "riskBanner": "t_j5va_riskBanner",
578
+ "attachBox": "t_j5va_attachBox",
579
+ "kindChipActive": "t_j5va_kindChipActive",
565
580
  "importList": "t_j5va_importList",
566
- "emptyHint": "t_j5va_emptyHint",
567
- "detailDeleteBtn": "t_j5va_detailDeleteBtn",
568
- "gridRow": "t_j5va_gridRow",
569
- "policyBar": "t_j5va_policyBar",
570
- "addButton": "t_j5va_addButton",
571
- "editor": "t_j5va_editor",
572
- "genHistItem": "t_j5va_genHistItem",
573
- "totp": "t_j5va_totp",
574
- "badgeOk": "t_j5va_badgeOk",
581
+ "moreWrap": "t_j5va_moreWrap",
582
+ "highBadge": "t_j5va_highBadge",
583
+ "dupGroup": "t_j5va_dupGroup",
584
+ "genOpts": "t_j5va_genOpts",
585
+ "selectActive": "t_j5va_selectActive",
586
+ "badge": "t_j5va_badge",
587
+ "badgeWarn": "t_j5va_badgeWarn",
588
+ "riskWarn": "t_j5va_riskWarn",
589
+ "strengthFair": "t_j5va_strengthFair",
575
590
  "tabPane": "t_j5va_tabPane",
576
- "recentClear": "t_j5va_recentClear",
577
- "kindChipActive": "t_j5va_kindChipActive",
578
- "row": "t_j5va_row",
579
- "kindFilter": "t_j5va_kindFilter",
580
- "dupMerge": "t_j5va_dupMerge",
581
- "brandGlyphSm": "t_j5va_brandGlyphSm",
582
- "rowDue": "t_j5va_rowDue",
583
- "tagActions": "t_j5va_tagActions",
584
- "vaultSwitch": "t_j5va_vaultSwitch",
585
- "recentLabel": "t_j5va_recentLabel",
586
- "badgeActive": "t_j5va_badgeActive",
587
- "gridDetail": "t_j5va_gridDetail",
588
- "detailCode": "t_j5va_detailCode",
591
+ "colorFieldWrap": "t_j5va_colorFieldWrap",
592
+ "customFieldRow": "t_j5va_customFieldRow",
593
+ "actionPrimary": "t_j5va_actionPrimary",
594
+ "bulkBar": "t_j5va_bulkBar",
595
+ "tagName": "t_j5va_tagName",
596
+ "reportTitle": "t_j5va_reportTitle",
589
597
  "actionBtn": "t_j5va_actionBtn",
590
- "lockedTag": "t_j5va_lockedTag",
598
+ "rowSelected": "t_j5va_rowSelected",
599
+ "histDanger": "t_j5va_histDanger",
591
600
  "pinStar": "t_j5va_pinStar",
592
- "toolbarFilters": "t_j5va_toolbarFilters",
593
- "modeAuto": "t_j5va_modeAuto",
594
- "policyField": "t_j5va_policyField",
595
- "revealButton": "t_j5va_revealButton",
596
- "modeAsk": "t_j5va_modeAsk",
597
- "actionGroupLabel": "t_j5va_actionGroupLabel",
598
- "status": "t_j5va_status",
599
- "rowMain": "t_j5va_rowMain",
601
+ "actionGroups": "t_j5va_actionGroups",
602
+ "importDesc": "t_j5va_importDesc",
603
+ "vaultHeader": "t_j5va_vaultHeader",
600
604
  "pinOn": "t_j5va_pinOn",
605
+ "detailActions": "t_j5va_detailActions",
606
+ "vaultManageRow": "t_j5va_vaultManageRow",
607
+ "tagActions": "t_j5va_tagActions",
608
+ "row": "t_j5va_row",
609
+ "editor": "t_j5va_editor",
610
+ "totp": "t_j5va_totp",
611
+ "auditWhen": "t_j5va_auditWhen",
612
+ "reportSub": "t_j5va_reportSub",
613
+ "pwStrong": "t_j5va_pwStrong",
614
+ "colorSwatch": "t_j5va_colorSwatch",
601
615
  "gridInput": "t_j5va_gridInput",
602
- "histLink": "t_j5va_histLink",
603
- "title": "t_j5va_title",
604
- "lockedBanner": "t_j5va_lockedBanner",
605
- "selectActive": "t_j5va_selectActive",
606
- "backupButton": "t_j5va_backupButton",
607
- "toolbar": "t_j5va_toolbar",
608
- "importDesc": "t_j5va_importDesc",
609
- "auditIssues": "t_j5va_auditIssues",
610
- "editorActions": "t_j5va_editorActions",
611
- "gridName": "t_j5va_gridName",
612
- "uriCode": "t_j5va_uriCode",
613
- "section": "t_j5va_section",
614
- "badgeLink": "t_j5va_badgeLink",
616
+ "pwWeak": "t_j5va_pwWeak",
615
617
  "favActive": "t_j5va_favActive",
616
- "colorFieldWrap": "t_j5va_colorFieldWrap",
617
- "detailRemain": "t_j5va_detailRemain",
618
- "strengthTrack": "t_j5va_strengthTrack",
619
- "searchKbd": "t_j5va_searchKbd",
620
- "histUpdate": "t_j5va_histUpdate",
621
- "actionPrimary": "t_j5va_actionPrimary",
622
- "dupNames": "t_j5va_dupNames",
623
- "totpCopy": "t_j5va_totpCopy",
624
- "pwHistRow": "t_j5va_pwHistRow",
618
+ "listCompact": "t_j5va_listCompact",
619
+ "brandGlyph": "t_j5va_brandGlyph",
620
+ "modeAsk": "t_j5va_modeAsk",
621
+ "customValueInput": "t_j5va_customValueInput",
622
+ "detailCode": "t_j5va_detailCode",
623
+ "gridDetail": "t_j5va_gridDetail",
624
+ "sortButton": "t_j5va_sortButton",
625
+ "urlGlyph": "t_j5va_urlGlyph",
626
+ "badgeLink": "t_j5va_badgeLink",
627
+ "strengthStars": "t_j5va_strengthStars",
628
+ "gridRow": "t_j5va_gridRow",
625
629
  "badgeDanger": "t_j5va_badgeDanger",
626
- "toolbarMain": "t_j5va_toolbarMain",
627
- "strengthStrong": "t_j5va_strengthStrong",
628
- "auditChip": "t_j5va_auditChip",
629
- "pwFair": "t_j5va_pwFair",
630
- "totpRing": "t_j5va_totpRing",
631
- "tab": "t_j5va_tab",
632
- "customFieldRows": "t_j5va_customFieldRows",
633
- "favToggle": "t_j5va_favToggle",
634
- "emojiBtn": "t_j5va_emojiBtn",
635
- "tabActive": "t_j5va_tabActive",
636
- "intro": "t_j5va_intro",
637
- "pwLabel": "t_j5va_pwLabel",
638
- "trashButton": "t_j5va_trashButton",
639
- "hit": "t_j5va_hit",
640
- "histDanger": "t_j5va_histDanger",
630
+ "healthBar": "t_j5va_healthBar",
641
631
  "histNeutral": "t_j5va_histNeutral",
632
+ "section": "t_j5va_section",
633
+ "emptyHint": "t_j5va_emptyHint",
634
+ "totpCopy": "t_j5va_totpCopy",
635
+ "kindFilter": "t_j5va_kindFilter",
636
+ "recentClear": "t_j5va_recentClear",
637
+ "gridName": "t_j5va_gridName",
638
+ "pwLabel": "t_j5va_pwLabel",
639
+ "auditRow": "t_j5va_auditRow",
642
640
  "editorBody": "t_j5va_editorBody",
643
- "highBadge": "t_j5va_highBadge",
644
- "detailTotp": "t_j5va_detailTotp",
645
- "reportJump": "t_j5va_reportJump",
646
- "strengthFair": "t_j5va_strengthFair",
647
- "colorSwatch": "t_j5va_colorSwatch",
648
- "importRow": "t_j5va_importRow",
649
- "exportChip": "t_j5va_exportChip",
650
- "recentSearch": "t_j5va_recentSearch",
651
- "recentChip": "t_j5va_recentChip",
641
+ "searchBox": "t_j5va_searchBox",
642
+ "healthSummary": "t_j5va_healthSummary",
643
+ "uriCode": "t_j5va_uriCode",
644
+ "toolbarFilters": "t_j5va_toolbarFilters",
645
+ "policyBar": "t_j5va_policyBar",
646
+ "histLink": "t_j5va_histLink",
647
+ "tab": "t_j5va_tab",
648
+ "genOptRow": "t_j5va_genOptRow",
649
+ "lockedBanner": "t_j5va_lockedBanner",
652
650
  "strengthWeak": "t_j5va_strengthWeak",
653
- "emptyBox": "t_j5va_emptyBox",
654
- "pwHistPwd": "t_j5va_pwHistPwd",
651
+ "empty": "t_j5va_empty",
652
+ "addButton": "t_j5va_addButton",
653
+ "brandGlyphSm": "t_j5va_brandGlyphSm",
654
+ "riskDanger": "t_j5va_riskDanger",
655
+ "lockedTag": "t_j5va_lockedTag",
656
+ "reportLineLink": "t_j5va_reportLineLink",
657
+ "actionGroupLabel": "t_j5va_actionGroupLabel",
658
+ "identity": "t_j5va_identity",
659
+ "hit": "t_j5va_hit",
660
+ "colorPalette": "t_j5va_colorPalette",
661
+ "rowDue": "t_j5va_rowDue",
662
+ "dupMerge": "t_j5va_dupMerge",
655
663
  "scoreRing": "t_j5va_scoreRing",
656
- "modeReadonly": "t_j5va_modeReadonly",
657
- "badgeWarn": "t_j5va_badgeWarn",
658
- "saveButton": "t_j5va_saveButton",
659
- "bulkBar": "t_j5va_bulkBar",
664
+ "totpRing": "t_j5va_totpRing",
665
+ "badgeOk": "t_j5va_badgeOk",
666
+ "dupNames": "t_j5va_dupNames",
667
+ "vaultTarget": "t_j5va_vaultTarget",
660
668
  "moreMenu": "t_j5va_moreMenu",
661
- "dueBadge": "t_j5va_dueBadge",
662
- "urlGlyph": "t_j5va_urlGlyph",
663
- "retryButton": "t_j5va_retryButton",
669
+ "auditChips": "t_j5va_auditChips",
670
+ "customKeyInput": "t_j5va_customKeyInput",
671
+ "revealButton": "t_j5va_revealButton",
672
+ "iconFieldWrap": "t_j5va_iconFieldWrap",
673
+ "intro": "t_j5va_intro",
674
+ "pwHistTime": "t_j5va_pwHistTime",
675
+ "attachAdd": "t_j5va_attachAdd",
676
+ "searchKbd": "t_j5va_searchKbd",
664
677
  "lenChips": "t_j5va_lenChips",
665
- "lenChipActive": "t_j5va_lenChipActive",
678
+ "headerMessage": "t_j5va_headerMessage",
679
+ "detailDeleteBtn": "t_j5va_detailDeleteBtn",
666
680
  "strengthFill": "t_j5va_strengthFill",
667
- "tagManager": "t_j5va_tagManager",
668
- "srOnly": "t_j5va_srOnly",
669
- "resultCount": "t_j5va_resultCount",
670
- "attachBox": "t_j5va_attachBox",
671
- "tabs": "t_j5va_tabs",
672
- "bulkItem": "t_j5va_bulkItem",
673
- "reportBox": "t_j5va_reportBox",
681
+ "pinOff": "t_j5va_pinOff",
682
+ "deleteButton": "t_j5va_deleteButton",
674
683
  "rowActions": "t_j5va_rowActions",
675
- "pwHistBox": "t_j5va_pwHistBox",
676
- "reportLineLink": "t_j5va_reportLineLink",
677
- "tagCount": "t_j5va_tagCount",
678
- "actionGroups": "t_j5va_actionGroups",
679
- "pwMeter": "t_j5va_pwMeter",
680
- "colorPalette": "t_j5va_colorPalette",
681
- "copyAllBtn": "t_j5va_copyAllBtn",
682
- "customValueInput": "t_j5va_customValueInput",
684
+ "detailItem": "t_j5va_detailItem",
685
+ "badgeActive": "t_j5va_badgeActive",
686
+ "moreButton": "t_j5va_moreButton",
687
+ "toolbarMain": "t_j5va_toolbarMain",
688
+ "reportJump": "t_j5va_reportJump",
689
+ "tagManager": "t_j5va_tagManager",
690
+ "genHistItem": "t_j5va_genHistItem",
691
+ "title": "t_j5va_title",
683
692
  "inlineInput": "t_j5va_inlineInput",
684
- "emojiPicker": "t_j5va_emojiPicker",
685
- "moreWrap": "t_j5va_moreWrap",
693
+ "bulkCount": "t_j5va_bulkCount",
694
+ "emojiBtn": "t_j5va_emojiBtn",
695
+ "copied": "t_j5va_copied",
696
+ "resultCount": "t_j5va_resultCount",
697
+ "exportChip": "t_j5va_exportChip",
698
+ "importName": "t_j5va_importName",
699
+ "retryButton": "t_j5va_retryButton",
700
+ "backupButton": "t_j5va_backupButton",
686
701
  "gridActions": "t_j5va_gridActions",
687
- "badgeBtn": "t_j5va_badgeBtn",
688
- "dupGroup": "t_j5va_dupGroup",
689
- "deleteButton": "t_j5va_deleteButton",
690
- "pwHistTime": "t_j5va_pwHistTime",
691
- "sortButton": "t_j5va_sortButton",
692
- "tagManagerHead": "t_j5va_tagManagerHead",
693
- "actionGroup": "t_j5va_actionGroup",
694
- "error": "t_j5va_error",
695
- "vaultHeader": "t_j5va_vaultHeader",
696
- "checkField": "t_j5va_checkField",
697
- "clearButton": "t_j5va_clearButton",
698
- "headerMessage": "t_j5va_headerMessage",
699
- "reportSub": "t_j5va_reportSub",
700
- "rowSelected": "t_j5va_rowSelected",
701
- "listCompact": "t_j5va_listCompact",
702
- "customFieldRow": "t_j5va_customFieldRow",
703
- "inlineField": "t_j5va_inlineField"
702
+ "genHist": "t_j5va_genHist",
703
+ "detailBox": "t_j5va_detailBox"
704
704
  };
705
705
  //#endregion
706
706
  //#region src/client/VaultSection.tsx
package/lib/store.js CHANGED
@@ -977,15 +977,27 @@ export class VaultStore {
977
977
  }
978
978
  throw error;
979
979
  }
980
- const parsed = JSON.parse(raw);
980
+ // Name the file in every failure below: a vault can live at an explicit
981
+ // --path, and "Unexpected token 'o'" or "version undefined" told the user
982
+ // neither which file was damaged nor that it was damaged at all.
983
+ let parsed;
984
+ try {
985
+ parsed = JSON.parse(raw);
986
+ }
987
+ catch {
988
+ throw new Error(`${this.path} is not valid JSON — it is corrupt or not a vault file`);
989
+ }
990
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
991
+ throw new Error(`${this.path} is not a vault file (expected a JSON object)`);
992
+ }
981
993
  if (parsed.version !== VAULT_FORMAT_VERSION) {
982
- throw new Error(`unsupported vault format version ${parsed.version} (expected ${VAULT_FORMAT_VERSION})`);
994
+ throw new Error(`${this.path} has unsupported vault format version ${JSON.stringify(parsed.version)} (expected ${VAULT_FORMAT_VERSION})`);
983
995
  }
984
996
  if (!parsed.kdf || parsed.kdf.algo !== 'scrypt' || !parsed.kdf.saltHex) {
985
- throw new Error('vault document is missing valid KDF parameters');
997
+ throw new Error(`${this.path} is missing valid KDF parameters`);
986
998
  }
987
999
  if (!parsed.verify) {
988
- throw new Error('vault document is missing its password-verification envelope');
1000
+ throw new Error(`${this.path} is missing its password-verification envelope`);
989
1001
  }
990
1002
  return { file: parsed, created: false };
991
1003
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-vault",
3
- "version": "1.10.73",
3
+ "version": "1.10.74-rc.2",
4
4
  "bin": {
5
- "dsh-vault": "./lib/cli.js"
5
+ "dsh-vault": "lib/cli.js"
6
6
  },
7
7
  "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.",
8
8
  "type": "module",
@@ -88,6 +88,8 @@
88
88
  "e2e:theme": "node tests/e2e/theme-check.mjs",
89
89
  "e2e:polish": "node tests/e2e/polish-check.mjs",
90
90
  "e2e:contrast": "node tests/e2e/contrast-audit.mjs",
91
- "e2e:prompt": "node tests/e2e/cli-prompt.mjs"
91
+ "e2e:prompt": "node tests/e2e/cli-prompt.mjs",
92
+ "release:check": "node scripts/release-check.mjs",
93
+ "postpublish": "node scripts/release-check.mjs --version"
92
94
  }
93
95
  }