dsh-vault 1.2.0-rc.8 → 1.2.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
@@ -61,6 +61,7 @@ dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件:把你在使
61
61
  | `vault_health` | 保险库健康扫描:弱密码/复用凭据、未启用 2FA、不安全的 http:// 站点,以及整体安全评分(0–100) |
62
62
  | `vault_breach_check` | Watchtower 风格泄露扫描:对接 Have I Been Pwned(k-anonymity,仅发送 SHA-1 前缀),附离线常见密码兜底 |
63
63
  | `vault_integrity` | 校验磁盘上的保险库文件可正确解密并与内存中的 store 一致 |
64
+ | `vault_integrity` | 校验磁盘上的保险库文件可正确解密并与内存中的 store 一致 |
64
65
  | `vault_merge` | 合并一个条目到另一个;`keepSource: true` 保留源条目 |
65
66
  | `vault_quick_add` | 快速录入(标题 + 一个密钥),支持 tags/notes |
66
67
  | `vault_expiry` | 设置/清除过期(`expiresAt: 0` 移除) |
@@ -71,7 +72,9 @@ dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件:把你在使
71
72
  | `vault_export` / `vault_import` | 整库加密备份/迁移(独立导出密码) |
72
73
  | `vault_fill` | 按 host/URL/用户名/标题匹配条目并返回其凭据 |
73
74
  | `vault_env` | 把标记 env 的条目(tags 含 `env`)渲染为 `KEY=VALUE` 行 |
74
- | `vault_templates` | 返回某类凭据(ssh/api-key/oauth 等)的推荐字段 |
75
+ | `vault_export_bitwarden` / `vault_import_bitwarden` | Bitwarden/Vaultwarden JSON 互通(完整字段映射,支持覆盖) |
76
+ | `vault_copy` | 复制条目(含密钥)到另一个命名保险库 |
77
+ | `vault_templates` | 内建 + 用户自定义模板(save/list/remove,KeePassXC 风格) |
75
78
 
76
79
  **典型开发场景**:存一条 SSH 凭据(`kind: ssh` + host/port/username/password 或 privateKey),开发时让模型 `vault_search` 找主机、`vault_get` 取连接信息;存 API 网关的 `api-key`/`oauth` 条目管理 access/refresh token 轮换。
77
80
 
package/README.md CHANGED
@@ -54,7 +54,7 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
54
54
  | `vault_generate_password` | Generate a strong random password (length/classes/grouping) **or a memorable passphrase** (`passphrase: true`, EFF-style word list, `words`/`separator`/`wordDigits`) |
55
55
  | `vault_strength` | Zero-dependency password strength estimate (score 0–100, weak/fair/strong/very strong) |
56
56
  | `vault_rekey` | Upgrade the vault to fresh scrypt KDF parameters in place |
57
- | `vault_backup` | Timestamped encrypted backup with retention: prunes old copies beyond `maxBackups` (default 10) |
57
+ | `vault_backup` | Timestamped encrypted backup with retention; optional `note` |
58
58
  | `vault_import_csv` | Bulk-import credentials from a CSV file (custom columns become fields; `overwrite: true` merges fields into existing entries instead of duplicating) |
59
59
  | `vault_apply_tags` | Bulk add/remove/replace tags on every entry matching a query (dry-run supported, no secrets) |
60
60
  | `vault_totp_uri` | Build an otpauth:// provisioning URI for a stored or bare TOTP secret |
@@ -63,6 +63,7 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
63
63
  | `vault_health` | Vault health scan: weak/reused passwords, missing 2FA, insecure http:// sites, and an overall security score (0–100) |
64
64
  | `vault_breach_check` | Watchtower-style breach scan against Have I Been Pwned (k-anonymity: only the SHA-1 prefix leaves the machine), with an offline common-password fallback |
65
65
  | `vault_integrity` | Verify the on-disk vault file decrypts correctly and matches the in-memory store |
66
+ | `vault_integrity` | Verify the on-disk vault file decrypts correctly and matches the in-memory store |
66
67
  | `vault_merge` | Merge one entry into another; `keepSource: true` keeps the source after merging |
67
68
  | `vault_quick_add` | Fast capture (title + one secret) with optional tags/notes |
68
69
  | `vault_expiry` | Set/clear expiry (`expiresAt: 0` removes it) |
@@ -73,7 +74,9 @@ Each record has a `title`, an optional `kind`, and any combination of fields:
73
74
  | `vault_export` / `vault_import` | Portable encrypted backup/migration of the whole vault (separate export password) |
74
75
  | `vault_fill` | Find the entry matching a host/URL/username/title and return its credentials |
75
76
  | `vault_env` | Render env-flagged entries (tags contain `env`) as `KEY=VALUE` lines |
76
- | `vault_templates` | List recommended fields for a credential kind (ssh / api-key / oauth / …) |
77
+ | `vault_export_bitwarden` / `vault_import_bitwarden` | Bitwarden/Vaultwarden JSON interop (full field mapping, overwrite support) |
78
+ | `vault_copy` | Copy an entry (secrets included) into another named vault |
79
+ | `vault_templates` | Built-in + user-defined templates (save/list/remove), KeePassXC-style |
77
80
 
78
81
  **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.
79
82
 
package/lib/client.js CHANGED
@@ -17,83 +17,83 @@ window.__ModuleLoader__.load({
17
17
  document.head.appendChild(tag);
18
18
  }
19
19
  var VaultSection_module_css_default = {
20
- "totpRing": "t_j5va_totpRing",
21
- "rowActions": "t_j5va_rowActions",
22
- "reportTitle": "t_j5va_reportTitle",
23
- "dupMerge": "t_j5va_dupMerge",
24
- "kindFilter": "t_j5va_kindFilter",
25
- "clearButton": "t_j5va_clearButton",
26
- "checkField": "t_j5va_checkField",
27
- "retryButton": "t_j5va_retryButton",
28
- "modeReadonly": "t_j5va_modeReadonly",
20
+ "lockedBanner": "t_j5va_lockedBanner",
21
+ "intro": "t_j5va_intro",
22
+ "toolbar": "t_j5va_toolbar",
23
+ "policyBar": "t_j5va_policyBar",
24
+ "editorBody": "t_j5va_editorBody",
29
25
  "field": "t_j5va_field",
30
- "highBadge": "t_j5va_highBadge",
31
- "badgeDanger": "t_j5va_badgeDanger",
32
- "trashButton": "t_j5va_trashButton",
33
- "editorActions": "t_j5va_editorActions",
34
- "dupNames": "t_j5va_dupNames",
35
- "searchBox": "t_j5va_searchBox",
36
- "pwMeter": "t_j5va_pwMeter",
37
- "modeAsk": "t_j5va_modeAsk",
26
+ "reportLine": "t_j5va_reportLine",
27
+ "sortButton": "t_j5va_sortButton",
28
+ "deleteButton": "t_j5va_deleteButton",
29
+ "editor": "t_j5va_editor",
38
30
  "healthBar": "t_j5va_healthBar",
39
- "footer": "t_j5va_footer",
40
- "editorBody": "t_j5va_editorBody",
41
- "kindIcon": "t_j5va_kindIcon",
42
- "detailItem": "t_j5va_detailItem",
43
- "genOpts": "t_j5va_genOpts",
44
- "addButton": "t_j5va_addButton",
45
- "modeAuto": "t_j5va_modeAuto",
31
+ "checkField": "t_j5va_checkField",
32
+ "rowDue": "t_j5va_rowDue",
46
33
  "badgeOk": "t_j5va_badgeOk",
47
- "saveButton": "t_j5va_saveButton",
34
+ "emptyHint": "t_j5va_emptyHint",
35
+ "list": "t_j5va_list",
48
36
  "section": "t_j5va_section",
49
- "uriCode": "t_j5va_uriCode",
50
- "srOnly": "t_j5va_srOnly",
51
- "toolbar": "t_j5va_toolbar",
52
- "error": "t_j5va_error",
53
- "dueBadge": "t_j5va_dueBadge",
54
- "rowExpired": "t_j5va_rowExpired",
55
- "intro": "t_j5va_intro",
56
- "rowDue": "t_j5va_rowDue",
57
- "dangerButton": "t_j5va_dangerButton",
58
- "pwBar": "t_j5va_pwBar",
59
- "pwWeak": "t_j5va_pwWeak",
37
+ "rowActions": "t_j5va_rowActions",
60
38
  "kindChip": "t_j5va_kindChip",
61
- "pwLabel": "t_j5va_pwLabel",
62
- "badgeWarn": "t_j5va_badgeWarn",
63
- "title": "t_j5va_title",
64
- "totp": "t_j5va_totp",
65
- "badge": "t_j5va_badge",
66
- "revealButton": "t_j5va_revealButton",
67
- "scoreRing": "t_j5va_scoreRing",
68
- "reportBox": "t_j5va_reportBox",
69
39
  "emptyBox": "t_j5va_emptyBox",
70
- "pinStar": "t_j5va_pinStar",
71
- "reportSub": "t_j5va_reportSub",
72
- "pwStrong": "t_j5va_pwStrong",
40
+ "badge": "t_j5va_badge",
73
41
  "row": "t_j5va_row",
42
+ "genOpts": "t_j5va_genOpts",
43
+ "revealButton": "t_j5va_revealButton",
44
+ "auditRow": "t_j5va_auditRow",
45
+ "saveButton": "t_j5va_saveButton",
46
+ "trashButton": "t_j5va_trashButton",
47
+ "detailItem": "t_j5va_detailItem",
74
48
  "identity": "t_j5va_identity",
75
- "rowMain": "t_j5va_rowMain",
76
- "editor": "t_j5va_editor",
77
- "secretField": "t_j5va_secretField",
49
+ "badgeDanger": "t_j5va_badgeDanger",
50
+ "auditIssues": "t_j5va_auditIssues",
51
+ "pwMeter": "t_j5va_pwMeter",
78
52
  "pwFair": "t_j5va_pwFair",
79
- "emptyHint": "t_j5va_emptyHint",
53
+ "reportTitle": "t_j5va_reportTitle",
54
+ "reportSub": "t_j5va_reportSub",
55
+ "copied": "t_j5va_copied",
56
+ "title": "t_j5va_title",
57
+ "dupGroup": "t_j5va_dupGroup",
58
+ "addButton": "t_j5va_addButton",
59
+ "badgeWarn": "t_j5va_badgeWarn",
80
60
  "resultCount": "t_j5va_resultCount",
81
- "genOptRow": "t_j5va_genOptRow",
61
+ "secretField": "t_j5va_secretField",
62
+ "footer": "t_j5va_footer",
63
+ "pinStar": "t_j5va_pinStar",
64
+ "uriCode": "t_j5va_uriCode",
82
65
  "empty": "t_j5va_empty",
83
- "auditRow": "t_j5va_auditRow",
84
- "dupGroup": "t_j5va_dupGroup",
85
- "lockedBanner": "t_j5va_lockedBanner",
86
- "deleteButton": "t_j5va_deleteButton",
87
- "policyField": "t_j5va_policyField",
88
- "auditIssues": "t_j5va_auditIssues",
89
- "status": "t_j5va_status",
90
- "copied": "t_j5va_copied",
91
- "reportLine": "t_j5va_reportLine",
92
- "detailBox": "t_j5va_detailBox",
93
- "sortButton": "t_j5va_sortButton",
66
+ "reportBox": "t_j5va_reportBox",
67
+ "scoreRing": "t_j5va_scoreRing",
68
+ "rowExpired": "t_j5va_rowExpired",
69
+ "searchBox": "t_j5va_searchBox",
70
+ "dangerButton": "t_j5va_dangerButton",
71
+ "clearButton": "t_j5va_clearButton",
72
+ "pwWeak": "t_j5va_pwWeak",
73
+ "rowMain": "t_j5va_rowMain",
74
+ "dupNames": "t_j5va_dupNames",
94
75
  "backupButton": "t_j5va_backupButton",
95
- "policyBar": "t_j5va_policyBar",
96
- "list": "t_j5va_list"
76
+ "modeAsk": "t_j5va_modeAsk",
77
+ "totpRing": "t_j5va_totpRing",
78
+ "highBadge": "t_j5va_highBadge",
79
+ "pwStrong": "t_j5va_pwStrong",
80
+ "totp": "t_j5va_totp",
81
+ "editorActions": "t_j5va_editorActions",
82
+ "detailBox": "t_j5va_detailBox",
83
+ "srOnly": "t_j5va_srOnly",
84
+ "status": "t_j5va_status",
85
+ "error": "t_j5va_error",
86
+ "retryButton": "t_j5va_retryButton",
87
+ "policyField": "t_j5va_policyField",
88
+ "genOptRow": "t_j5va_genOptRow",
89
+ "modeReadonly": "t_j5va_modeReadonly",
90
+ "kindIcon": "t_j5va_kindIcon",
91
+ "dueBadge": "t_j5va_dueBadge",
92
+ "modeAuto": "t_j5va_modeAuto",
93
+ "kindFilter": "t_j5va_kindFilter",
94
+ "pwBar": "t_j5va_pwBar",
95
+ "pwLabel": "t_j5va_pwLabel",
96
+ "dupMerge": "t_j5va_dupMerge"
97
97
  };
98
98
  //#endregion
99
99
  //#region src/client/VaultSection.tsx
package/lib/index.js CHANGED
@@ -1748,7 +1748,7 @@ export async function apply(ctx, config) {
1748
1748
  return { added: 0, skipped: 0, updated: 0 };
1749
1749
  // Header may be name,url,username,password or url,name,username,password.
1750
1750
  const header = rows[0].map(h => h.trim().toLowerCase());
1751
- const idx = {
1751
+ let idx = {
1752
1752
  name: header.indexOf('name'),
1753
1753
  url: header.indexOf('url'),
1754
1754
  username: header.indexOf('username'),
@@ -1756,6 +1756,10 @@ export async function apply(ctx, config) {
1756
1756
  otpauth: header.indexOf('otpauth'),
1757
1757
  notes: header.indexOf('notes'),
1758
1758
  };
1759
+ // LastPass export order: url,username,password,extra,name,grouping,fav
1760
+ if (idx.name < 0 && header.length >= 5 && header[4] === 'name') {
1761
+ idx = { name: 4, url: 0, username: 1, password: 2, otpauth: -1, notes: header.indexOf('extra') };
1762
+ }
1759
1763
  if (idx.name < 0 || idx.password < 0)
1760
1764
  return { added: 0, skipped: rows.length - 1, updated: 0 };
1761
1765
  let added = 0;
@@ -2269,10 +2273,9 @@ export async function apply(ctx, config) {
2269
2273
  overwrite: { type: 'boolean', description: 'Replace existing entries with the same id instead of merging (default false).' },
2270
2274
  dryRun: { type: 'boolean', description: 'Preview how many entries would be imported without writing anything (default false).' },
2271
2275
  },
2272
- output: { schema: { type: 'object', additionalProperties: false, properties: { imported: { type: 'integer', required: true } } }, render: (_a, v) => [{ type: 'text', text: `imported ${v.imported} entries` }] },
2276
+ output: { schema: { type: 'object', additionalProperties: false, properties: { imported: { type: 'integer', required: true }, note: { type: 'string' } } }, render: (_a, v) => [{ type: 'text', text: v.note ?? `imported ${v.imported} entries` }] },
2273
2277
  async execute(args) {
2274
2278
  assertWritable('vault_import');
2275
- const exportPassword = resolveExportPassword(config);
2276
2279
  const s = await guardStore();
2277
2280
  if (args.path === undefined && args.blob === undefined) {
2278
2281
  throw new Error('vault_import: provide either path or blob');
@@ -2281,8 +2284,68 @@ export async function apply(ctx, config) {
2281
2284
  throw new Error('vault_import: blob exceeds 64 MiB — write it to a file and pass path instead');
2282
2285
  }
2283
2286
  const blob = args.path !== undefined ? await readFile(args.path, 'utf8') : args.blob;
2287
+ // Auto-sniff: an unencrypted Bitwarden JSON export (encrypted:false +
2288
+ // items[]) routes to the Bitwarden importer; anything else is treated as
2289
+ // the portable encrypted dsh-vault document.
2290
+ try {
2291
+ const probe = JSON.parse(blob);
2292
+ if (probe && probe.encrypted === false && Array.isArray(probe.items)) {
2293
+ let added = 0;
2294
+ let skipped = 0;
2295
+ let updated = 0;
2296
+ for (const item of probe.items) {
2297
+ const title = (item.name ?? '').trim();
2298
+ if (!title) {
2299
+ skipped++;
2300
+ continue;
2301
+ }
2302
+ const patch = {};
2303
+ const login = item.login;
2304
+ if (login?.username !== undefined)
2305
+ patch.username = login.username;
2306
+ if (login?.password !== undefined)
2307
+ patch.password = login.password;
2308
+ if (login?.totp !== undefined)
2309
+ patch.otpSecret = login.totp;
2310
+ const uri = login?.uris?.find((u) => u.uri)?.uri;
2311
+ if (uri !== undefined)
2312
+ patch.url = uri;
2313
+ if (item.notes)
2314
+ patch.notes = item.notes;
2315
+ const fields = {};
2316
+ for (const f of item.fields ?? []) {
2317
+ if (f.name !== undefined && f.value !== undefined)
2318
+ fields[f.name] = f.value;
2319
+ }
2320
+ for (const key of ['host', 'apiKey', 'secret', 'accessToken', 'refreshToken', 'privateKey']) {
2321
+ if (fields[key] !== undefined) {
2322
+ patch[key] = fields[key];
2323
+ delete fields[key];
2324
+ }
2325
+ }
2326
+ if (Object.keys(fields).length > 0)
2327
+ patch.fields = fields;
2328
+ const existing = s.list().find(e => e.title === title);
2329
+ if (existing && args.overwrite !== true) {
2330
+ skipped++;
2331
+ continue;
2332
+ }
2333
+ if (existing) {
2334
+ await s.update(existing.id, patch);
2335
+ updated++;
2336
+ }
2337
+ else {
2338
+ await s.add({ title, ...patch });
2339
+ added++;
2340
+ }
2341
+ }
2342
+ return { imported: added + updated, note: `sniffed Bitwarden JSON: added ${added}, updated ${updated}, skipped ${skipped}` };
2343
+ }
2344
+ }
2345
+ catch { /* not JSON or not Bitwarden — fall through to encrypted import */ }
2346
+ const exportPassword = resolveExportPassword(config);
2284
2347
  const count = await s.importEncrypted(blob, exportPassword, args.overwrite === true, args.dryRun === true);
2285
- return { imported: count };
2348
+ return { imported: count, note: 'imported encrypted dsh-vault document' };
2286
2349
  },
2287
2350
  }));
2288
2351
  // ── vault_fill: find the credential that fits a target ─────────────────────
@@ -2463,6 +2526,7 @@ export async function apply(ctx, config) {
2463
2526
  + 'optionally filtered by kind. Writes to <vault dir>/vault-export-<ts>.csv and returns the path.',
2464
2527
  parameters: {
2465
2528
  path: { type: 'string', description: 'Optional absolute output path; defaults to <vault dir>/vault-export-<ts>.csv.' },
2529
+ delimiter: { type: 'string', description: 'CSV delimiter (default ",").' },
2466
2530
  kind: { type: 'string', description: 'Only export entries of this kind (login/ssh/api-key/secret/oauth/custom).', enum: ['login', 'ssh', 'api-key', 'secret', 'oauth', 'custom'] },
2467
2531
  includeSecrets: { type: 'boolean', description: 'Include secret columns (password/apiKey/secret/tokens). Default false — the CSV is secret-free for safe handling.' },
2468
2532
  favoriteOnly: { type: 'boolean', description: 'Only export pinned (favorite) entries.' },
@@ -2486,7 +2550,8 @@ export async function apply(ctx, config) {
2486
2550
  const str = v === undefined || v === null ? '' : Array.isArray(v) ? v.join(';') : String(v);
2487
2551
  return `"${str.replace(/"/g, '""')}"`;
2488
2552
  };
2489
- const lines = [fields.join(',')];
2553
+ const delim = args.delimiter ?? ',';
2554
+ const lines = [fields.join(delim)];
2490
2555
  const MIN_LEN = 12;
2491
2556
  const now = Date.now();
2492
2557
  for (const e of entries) {
@@ -2497,7 +2562,7 @@ export async function apply(ctx, config) {
2497
2562
  rec.no2fa = pw.length > 0 && (kind === 'login' || kind === 'ssh') && rec.otpSecret === undefined ? 'true' : 'false';
2498
2563
  rec.httpSite = typeof rec.url === 'string' && /^http:\/\//i.test(rec.url) ? 'true' : 'false';
2499
2564
  rec.expired = typeof rec.expiresAt === 'number' && rec.expiresAt < now ? 'true' : 'false';
2500
- lines.push(fields.map(f => esc(rec[f])).join(','));
2565
+ lines.push(fields.map(f => esc(rec[f])).join(delim));
2501
2566
  }
2502
2567
  const file = args.path ?? join(dirname(resolveVaultPath(config)), `vault-export-${Date.now()}.csv`);
2503
2568
  await mkdir(dirname(file), { recursive: true, mode: 0o700 });
@@ -2651,8 +2716,11 @@ export async function apply(ctx, config) {
2651
2716
  description: 'Create a timestamped backup of the vault file (a copy of the on-disk encrypted '
2652
2717
  + 'document, not a plaintext export). Old backups beyond maxBackups (default 10) are pruned '
2653
2718
  + 'automatically (newest kept). Returns the backup path and retention stats.',
2654
- parameters: { maxBackups: { type: 'integer', description: 'Keep at most this many backups (default 10, min 1).' } },
2655
- output: { schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string', required: true }, kept: { type: 'integer', required: true }, pruned: { type: 'integer', required: true } } }, render: (_a, v) => [{ type: 'text', text: `backup written to ${v.path} (kept ${v.kept}, pruned ${v.pruned})` }] },
2719
+ parameters: {
2720
+ maxBackups: { type: 'integer', description: 'Keep at most this many backups (default 10, min 1).' },
2721
+ note: { type: 'string', description: 'Optional note returned with the backup (e.g. why it was taken).' },
2722
+ },
2723
+ output: { schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string', required: true }, kept: { type: 'integer', required: true }, pruned: { type: 'integer', required: true }, note: { type: 'string' } } }, render: (_a, v) => [{ type: 'text', text: `backup written to ${v.path} (kept ${v.kept}, pruned ${v.pruned})${v.note !== undefined ? ' — ' + v.note : ''}` }] },
2656
2724
  async execute(args) {
2657
2725
  const s = await guardStore();
2658
2726
  const source = resolveVaultPath(config);
@@ -2686,7 +2754,7 @@ export async function apply(ctx, config) {
2686
2754
  catch { /* best-effort */ }
2687
2755
  }
2688
2756
  void s;
2689
- return { path: backup, kept: Math.min(backups.length + 1, maxBackups), pruned };
2757
+ return { path: backup, kept: Math.min(backups.length + 1, maxBackups), pruned, ...(typeof args.note === 'string' && args.note.length > 0 ? { note: args.note } : {}) };
2690
2758
  },
2691
2759
  }));
2692
2760
  // ── vault_import_csv: bulk import from a CSV file ───────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vault",
3
- "version": "1.2.0-rc.8",
3
+ "version": "1.2.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",