dsh-vault 1.10.68 → 1.10.70

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
@@ -252,6 +252,11 @@ dsh-vault get my-entry --field apiKey # 取指定字段
252
252
  dsh-vault get my-entry --mask # 只确认存在,不打印值
253
253
  dsh-vault show my-entry # 非敏感字段的 JSON
254
254
  dsh-vault env # 带 env 标签的条目 → KEY=VALUE
255
+ dsh-vault env --mask # 人类视角,两段都是 KEY=VALUE:
256
+ # ## exported items
257
+ # NPM_REGISTRY_API_KEY=npm_***
258
+ # ## unexported items
259
+ # EXAMPLE_BILLING_API_KEY=***
255
260
  dsh-vault export-env .env # ……或写成 0600 权限的 .env 文件
256
261
  dsh-vault verify # 校验主密码,stdout 无输出
257
262
  ```
@@ -319,6 +324,22 @@ npx dsh-vault list # 或者干脆不装
319
324
 
320
325
  profile 这条路还有两个注意点:垫片可能被之后在该目录执行的 `pnpm install` 清掉;`pnpm dsh plugin --profile web add dsh-vault` 会依据包的 `dsh.bundle` 声明重整 `dsh.profile.bundles` —— 如果你自己的 patch 层**也**插入了这个插件,就会**挂两份**,要先删掉 patch 层那一行。
321
326
 
327
+ ## 强度指示
328
+
329
+ 列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ = 满分**,中间按半星分档。半星不是 Unicode 字符(大多数字体没有),而是把同样三个 `★` 复制一层、按百分比裁剪彩色层实现的:
330
+
331
+ | 分数 | 显示 | 档位 |
332
+ |---|---|---|
333
+ | 0–8 | ☆☆☆ | 弱 |
334
+ | 9–24 | ½☆☆ | 弱 |
335
+ | 25–41 | ★☆☆ | 弱 |
336
+ | 42–58 | ★½☆ | 中 |
337
+ | 59–74 | ★★☆ | 中 |
338
+ | 75–91 | ★★½ | 强 |
339
+ | 92–100 | ★★★ | 强 |
340
+
341
+ 悬停可看到精确分数与评价。分数在 **host 侧**计算(列表本身永远拿不到密钥),针对条目的**密码或银行卡 PIN**;机器生成的 API Key、私钥**故意不评分**,免得所有密钥都是满分。
342
+
322
343
  ## 环境变量导出
323
344
 
324
345
  打上 `env` 标签的条目可以用 `vault_env` / `vault_export_env` 导出为 `KEY=VALUE` 行,名字按各家工具链预期的字段名生成:
package/README.md CHANGED
@@ -268,6 +268,11 @@ dsh-vault get my-entry --field apiKey # one named field
268
268
  dsh-vault get my-entry --mask # confirm it exists without printing it
269
269
  dsh-vault show my-entry # non-secret metadata as JSON
270
270
  dsh-vault env # env-tagged entries as KEY=VALUE
271
+ dsh-vault env --mask # human view, both sections as KEY=VALUE:
272
+ # ## exported items
273
+ # NPM_REGISTRY_API_KEY=npm_***
274
+ # ## unexported items
275
+ # EXAMPLE_BILLING_API_KEY=***
271
276
  dsh-vault export-env .env # …or write them to a 0600 file
272
277
  dsh-vault verify # check the master password, nothing on stdout
273
278
  ```
@@ -354,6 +359,28 @@ Two more caveats on the profile route: its `.bin` shim can be pruned by a later
354
359
  patch layer *also* inserts the plugin, that mounts it twice, so remove the patch
355
360
  row first.
356
361
 
362
+ ## Strength indicator
363
+
364
+ Each entry row shows a three-star indicator after its title — **☆☆☆ = 0 score,
365
+ ★★★ = full marks** — in six half-star steps. A half star is the coloured copy of
366
+ the same three glyphs clipped to a percentage (Unicode has no half-star glyph
367
+ most fonts can render):
368
+
369
+ | score | shows | band |
370
+ |---|---|---|
371
+ | 0–8 | ☆☆☆ | weak |
372
+ | 9–24 | ½☆☆ | weak |
373
+ | 25–41 | ★☆☆ | weak |
374
+ | 42–58 | ★½☆ | fair |
375
+ | 59–74 | ★★☆ | fair |
376
+ | 75–91 | ★★½ | strong |
377
+ | 92–100 | ★★★ | strong |
378
+
379
+ Hovering it shows the exact score and verdict. The score is computed **host-side**
380
+ (the list never receives the secret) and covers the entry's **password or card
381
+ PIN**; machine-generated API keys and private keys are deliberately not scored,
382
+ so every key does not sit at full marks.
383
+
357
384
  ## Environment export
358
385
 
359
386
  Entries tagged `env` can be materialised as `KEY=VALUE` lines (`vault_env`,
package/lib/cli.js CHANGED
@@ -24,7 +24,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
  import { createInterface } from 'node:readline';
25
25
  import { ReadStream } from 'node:tty';
26
26
  import { openVault, defaultVaultPath } from "./store.js";
27
- import { envLinesFor, envPairsForEntry, maskSecret, primarySecret } from "./env-export.js";
27
+ import { envLinesFor, envPairsForEntry, maskSecret, primarySecret, shellQuote } from "./env-export.js";
28
28
  function packageVersion() {
29
29
  try {
30
30
  const require = createRequire(import.meta.url);
@@ -80,7 +80,9 @@ env options:
80
80
  --prefix <P> Prefix for derived key names (an explicit envKey is kept verbatim)
81
81
  --kind <k> Only entries of this kind
82
82
  --keys-only Print key names without values
83
- --mask Print masked values
83
+ --mask Human view: masked values PLUS an "unexported items"
84
+ section (what the tag currently leaves out)
85
+ --explain Same two-section view with the real values
84
86
  --file <path> Write to a file instead of stdout
85
87
 
86
88
  Exported lines are shell-quoted (KEY='value'), so read them with
@@ -357,7 +359,7 @@ export async function runCli(argv, io) {
357
359
  list: ['kind', 'tag'],
358
360
  get: ['field', 'fields', 'mask', 'all'],
359
361
  show: [],
360
- env: ['prefix', 'kind', 'keys-only', 'mask', 'file'],
362
+ env: ['prefix', 'kind', 'keys-only', 'mask', 'explain', 'file'],
361
363
  'export-env': ['prefix', 'kind', 'keys-only'],
362
364
  verify: [],
363
365
  };
@@ -479,7 +481,8 @@ export async function runCli(argv, io) {
479
481
  // env / export-env
480
482
  const prefix = stringFlag(parsed, 'prefix') ?? '';
481
483
  const kind = stringFlag(parsed, 'kind');
482
- const lines = envLinesFor(store.list(), { prefix, ...(kind !== undefined ? { kind } : {}) });
484
+ const matching = store.list().filter(entry => kind === undefined || (entry.kind ?? 'login') === kind);
485
+ const lines = envLinesFor(matching, { prefix, ...(kind !== undefined ? { kind } : {}) });
483
486
  const rendered = lines.map(line => {
484
487
  if (keysOnly)
485
488
  return line.split('=')[0] ?? line;
@@ -488,6 +491,27 @@ export async function runCli(argv, io) {
488
491
  const eq = line.indexOf('=');
489
492
  return `${line.slice(0, eq)}=${maskSecret(line.slice(eq + 1).replace(/^'|'$/g, ''))}`;
490
493
  });
494
+ // `--mask` is the human view (masked values are useless to a script), so it
495
+ // also lists what is NOT exported. Without it the output stays pure
496
+ // `KEY=VALUE`, which is what `eval "$(…)"` and pipelines need.
497
+ const explain = mask || parsed.flags.get('explain') === true;
498
+ const unexported = matching.filter(entry => !(entry.tags ?? []).includes('env'));
499
+ /** `KEY=***` for entries that are not exported (names visible, values not). */
500
+ const envPairsForEntryPairs = (entries) => pairsOf(entries, true).map(line => `${line.slice(0, line.indexOf('='))}=***`);
501
+ /** KEY=VALUE pairs of the given entries, de-duplicated, first entry wins. */
502
+ const pairsOf = (entries, masked) => {
503
+ const seen = new Set();
504
+ const out = [];
505
+ for (const entry of entries) {
506
+ for (const pair of envPairsForEntry(entry, prefix)) {
507
+ if (seen.has(pair.key))
508
+ continue;
509
+ seen.add(pair.key);
510
+ out.push(`${pair.key}=${masked ? maskSecret(pair.value) : shellQuote(pair.value)}`);
511
+ }
512
+ }
513
+ return out;
514
+ };
491
515
  const target = parsed.command === 'export-env' ? parsed.positional[0] : stringFlag(parsed, 'file');
492
516
  if (parsed.command === 'export-env' && target === undefined) {
493
517
  io.err('dsh-vault: export-env needs a file path\n');
@@ -504,6 +528,28 @@ export async function runCli(argv, io) {
504
528
  : `dsh-vault: wrote ${rendered.length} lines to ${file} (mode 0600)\n`);
505
529
  return 0;
506
530
  }
531
+ if (explain) {
532
+ // Both sections use the same KEY=VALUE shape, so a name can be copied from
533
+ // either one. The unexported values stay masked even under --explain: the
534
+ // tag is what says "this one is meant for a shell".
535
+ io.out('## exported items\n');
536
+ if (rendered.length > 0)
537
+ io.out(`${rendered.join('\n')}\n`);
538
+ else
539
+ io.out(' (none — no entry carries the "env" tag)\n');
540
+ io.out('\n## unexported items\n');
541
+ // A fixed placeholder, not `maskSecret`: these did not opt into export, so
542
+ // not even a four-character prefix of a password should be shown.
543
+ const hidden = envPairsForEntryPairs(unexported);
544
+ if (hidden.length > 0) {
545
+ io.out(`${hidden.join('\n')}\n`);
546
+ io.out(' not exported (no "env" tag) — values hidden. `get <name>` still reads them.\n');
547
+ }
548
+ else {
549
+ io.out(' (none — every entry is exported)\n');
550
+ }
551
+ return 0;
552
+ }
507
553
  io.out(rendered.join('\n') + (rendered.length > 0 ? '\n' : ''));
508
554
  if (rendered.length === 0) {
509
555
  // The usual first-run wall: `env` only exports entries tagged `env`, and