dsh-db-tool 1.5.9 → 1.6.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.
@@ -10,6 +10,16 @@ import * as path from 'node:path';
10
10
  import { readJson, writeJsonAtomic } from './io.js';
11
11
  /** fields 中被视为机密的键,落盘前拆出到 secrets.json */
12
12
  const PASSWORD_KEY = 'password';
13
+ /** id 冲突哨兵错误:importBundle 以此 instanceof 识别跳过项,
14
+ * 替代对 create() 中文错误文案的字符串匹配(文案改动不破坏幂等契约) */
15
+ export class DuplicateConnectionError extends Error {
16
+ connId;
17
+ constructor(connId) {
18
+ super(`连接已存在: ${connId}`);
19
+ this.connId = connId;
20
+ this.name = 'DuplicateConnectionError';
21
+ }
22
+ }
13
23
  /** authority 段定位:scheme:// 到首个 /?#(兼容可选 jdbc: 前缀,如 jdbc:gaussdb://),
14
24
  * 捕获组 1 为 scheme:// 部分、组 2 为 authority(多主机逗号串整体落在该段内)。
15
25
  * redactUrl 与 mergeUrlCredentials 的字符串回退注入共用。 */
@@ -178,11 +188,11 @@ export class ConnectionStore {
178
188
  const rec = this.findRec(this.load(), id);
179
189
  return rec ? toMeta(rec, this.hasPassword(id)) : undefined;
180
190
  }
181
- /** 新建连接;id 已存在时抛错 */
191
+ /** 新建连接;id 已存在时抛 DuplicateConnectionError */
182
192
  create(input) {
183
193
  const data = this.load();
184
194
  if (this.findRec(data, input.id)) {
185
- throw new Error(`连接已存在: ${input.id}`);
195
+ throw new DuplicateConnectionError(input.id);
186
196
  }
187
197
  const rec = { id: input.id, kind: input.kind };
188
198
  if (input.name !== undefined)
@@ -296,4 +306,77 @@ export class ConnectionStore {
296
306
  rc.ssl = rec.ssl;
297
307
  return rc;
298
308
  }
309
+ /** 导出连接迁移包:全部连接记录 + 对应机密(含明文密码)。
310
+ * 调用方必须将整个返回值经 export-crypto 加密后方可落盘/传输,
311
+ * 本方法本身不加密(加密口令属于 HTTP 层入参,不应侵入存储层)。 */
312
+ exportBundle() {
313
+ const connections = this.load().connections;
314
+ const secrets = {};
315
+ for (const rec of connections) {
316
+ const sec = this.secrets.get(rec.id);
317
+ if (sec !== undefined)
318
+ secrets[rec.id] = sec;
319
+ }
320
+ return { connections, secrets };
321
+ }
322
+ /** 导入连接迁移包:逐条走既有 create()(机密拆分/脱敏逻辑全复用)。
323
+ * - id 冲突(「连接已存在」)→ 记入 skipped,不覆盖已有数据;
324
+ * - 其他异常 → 记入 errors 并继续处理后续条目;
325
+ * - 幂等可重入:重复导入同一 bundle,已存在的全部落入 skipped。 */
326
+ importBundle(bundle) {
327
+ const result = { imported: 0, skipped: [], errors: [] };
328
+ for (const rec of bundle.connections) {
329
+ // per-record 校验:解密成功的包也可能内容畸形(schema 漂移/手工构造),
330
+ // 缺 id/kind 的记录不落库,计入 errors
331
+ if (rec === null ||
332
+ typeof rec !== 'object' ||
333
+ typeof rec.id !== 'string' ||
334
+ rec.id === '' ||
335
+ typeof rec.kind !== 'string' ||
336
+ rec.kind === '') {
337
+ result.errors.push({ id: String(rec?.id ?? ''), message: '记录结构非法(缺少 id/kind)' });
338
+ continue;
339
+ }
340
+ const sec = bundle.secrets?.[rec.id];
341
+ try {
342
+ // 重建 create 入参:脱敏 URL 回换完整 URL(机密在 sec.url 中);
343
+ // fields 模式密码经 fields.password 传入由 create 自动拆分到 secrets
344
+ this.create({
345
+ id: rec.id,
346
+ kind: rec.kind,
347
+ name: rec.name,
348
+ url: sec?.url,
349
+ fields: sec?.password !== undefined ? { ...(rec.fields ?? {}), password: sec.password } : rec.fields,
350
+ ssl: rec.ssl,
351
+ });
352
+ // 覆盖式写回:create 成功后该 id 必无冲突,先清掉既有/孤儿机密
353
+ // (create 崩溃窗口可能残留旧条目),再整体重写,确保导入后机密与包一致。
354
+ // 合并写会残留旧键(如旧 url),适配器优先用 rc.url,会被静默错连旧主机。
355
+ this.secrets.delete(rec.id);
356
+ try {
357
+ if (sec !== undefined)
358
+ this.secrets.set(rec.id, sec);
359
+ }
360
+ catch (secErr) {
361
+ // 写回机密失败:连接行已落盘,回滚避免「errors 里却已存在」的半导入门状态
362
+ try {
363
+ this.remove(rec.id);
364
+ }
365
+ catch { /* 尽力而为 */ }
366
+ throw secErr;
367
+ }
368
+ result.imported += 1;
369
+ }
370
+ catch (err) {
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ if (err instanceof DuplicateConnectionError) {
373
+ result.skipped.push(rec.id);
374
+ }
375
+ else {
376
+ result.errors.push({ id: rec.id, message });
377
+ }
378
+ }
379
+ }
380
+ return result;
381
+ }
299
382
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 连接导出/导入的口令加解密(纯函数,仅用 Node stdlib crypto)。
3
+ *
4
+ * 方案:
5
+ * - KDF:scryptSync(passphrase, salt, 32) 派生 AES-256 密钥;
6
+ * - 算法:AES-256-GCM(机密性 + 完整性一体,tag 防篡改);
7
+ * - iv:12 字节随机(GCM 推荐长度);salt:16 字节随机;
8
+ * - 输出为 JSON 字符串:{ v: 1, kdf: 'scrypt', salt, iv, tag, data },二进制段全部 base64。
9
+ *
10
+ * 失败统一抛 Error('口令错误或文件已损坏'),不向外泄漏是口令错还是文件坏,
11
+ * 避免给攻击者侧信道。口令本身为空/非字符串时单独抛 Error(参数错误,非解密失败)。
12
+ */
13
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
14
+ /** 解密失败/文件损坏的统一错误信息 */
15
+ export const DECRYPT_FAIL_MESSAGE = '口令错误或文件已损坏';
16
+ /** 口令断言:必须为非空字符串 */
17
+ function assertPassphrase(passphrase) {
18
+ if (typeof passphrase !== 'string' || passphrase.length === 0) {
19
+ throw new Error('口令必须为非空字符串');
20
+ }
21
+ }
22
+ /** 加密任意可 JSON 序列化的 payload,返回信封 JSON 字符串 */
23
+ export function encryptPayload(plain, passphrase) {
24
+ assertPassphrase(passphrase);
25
+ const salt = randomBytes(16);
26
+ // 显式固定 scrypt 成本参数(与 Node 当前默认一致),加解密两侧同步使用,
27
+ // 避免导出包跨运行时/跨 Node 版本时因默认值漂移而无法解密
28
+ const key = scryptSync(passphrase, salt, 32, { N: 16384, r: 8, p: 1 });
29
+ const iv = randomBytes(12);
30
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
31
+ const data = Buffer.concat([cipher.update(JSON.stringify(plain), 'utf8'), cipher.final()]);
32
+ const box = {
33
+ v: 1,
34
+ kdf: 'scrypt',
35
+ salt: salt.toString('base64'),
36
+ iv: iv.toString('base64'),
37
+ tag: cipher.getAuthTag().toString('base64'),
38
+ data: data.toString('base64'),
39
+ };
40
+ return JSON.stringify(box);
41
+ }
42
+ /** 结构校验:缺字段/类型不符即视为文件损坏 */
43
+ function isEncryptedBox(x) {
44
+ if (x === null || typeof x !== 'object')
45
+ return false;
46
+ const b = x;
47
+ return (b.v === 1 &&
48
+ b.kdf === 'scrypt' &&
49
+ typeof b.salt === 'string' &&
50
+ typeof b.iv === 'string' &&
51
+ typeof b.tag === 'string' &&
52
+ typeof b.data === 'string');
53
+ }
54
+ /** 解密信封 JSON,返回原始 payload;口令错误或任一环节损坏统一抛 DECRYPT_FAIL_MESSAGE */
55
+ export function decryptPayload(encryptedJson, passphrase) {
56
+ assertPassphrase(passphrase);
57
+ try {
58
+ const box = JSON.parse(encryptedJson);
59
+ if (!isEncryptedBox(box))
60
+ throw new Error('结构非法');
61
+ const salt = Buffer.from(box.salt, 'base64');
62
+ const iv = Buffer.from(box.iv, 'base64');
63
+ const tag = Buffer.from(box.tag, 'base64');
64
+ const data = Buffer.from(box.data, 'base64');
65
+ // base64 解码静默容错,需显式校验解码后长度(iv 12B / tag 16B / salt 非空)
66
+ if (iv.length !== 12 || tag.length !== 16 || salt.length === 0)
67
+ throw new Error('长度非法');
68
+ const key = scryptSync(passphrase, salt, 32, { N: 16384, r: 8, p: 1 });
69
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
70
+ decipher.setAuthTag(tag);
71
+ const plain = Buffer.concat([decipher.update(data), decipher.final()]);
72
+ return JSON.parse(plain.toString('utf8'));
73
+ }
74
+ catch (err) {
75
+ // 对外统一文案(防侧信道),原始错误挂 cause 供服务端日志区分失败模式
76
+ throw new Error(DECRYPT_FAIL_MESSAGE, { cause: err });
77
+ }
78
+ }
@@ -18,7 +18,8 @@ import { GrantStore } from './grants.js';
18
18
  import { chmodBestEffort } from './io.js';
19
19
  import { SecretsBox } from './secrets.js';
20
20
  export { normalizeProjectKey } from './normalize.js';
21
- export { mergeUrlCredentials, redactUrl } from './connections.js';
21
+ export { mergeUrlCredentials, redactUrl, DuplicateConnectionError } from './connections.js';
22
+ export { encryptPayload, decryptPayload, DECRYPT_FAIL_MESSAGE } from './export-crypto.js';
22
23
  export { AuditLog, ConnectionStore, GrantStore, SecretsBox };
23
24
  export class DbToolStore {
24
25
  /** 数据目录:$DSH_HOME/db-tool */
@@ -0,0 +1,172 @@
1
+ # 连接管理「导出 / 导入」方案
2
+
3
+ > 状态:已实现并验收(全仓 `tsc --noEmit` 0 报错;全量 `npm test` 564/564 通过;`npm run build` 成功)
4
+ > 版本:dsh-db-tool ≥ 1.5.91
5
+ > 范围:仅侧边栏「连接管理」面板;模型工具(LLM)不暴露。
6
+ > 技术依据:Node.js crypto 官方文档 + MDN Web Docs(经 context7 检索),逐条对照见 §3、§4.4。
7
+
8
+ ## 1. 背景与目标
9
+
10
+ dsh-db-tool 把连接配置拆两处持久化:
11
+
12
+ - `connections.json` — 连接元数据(绝不含密码;密码段在 URL 中被替换为 `***`)
13
+ - `secrets.json` — 连接级机密(完整 URL / 明文密码,权限 0600)
14
+
15
+ 用户需要跨机器迁移连接:一键导出为**单个加密文件**,在另一台机器(或另一个 DSH 实例)用口令解密导入。
16
+
17
+ ### 成功标准(验收已满足)
18
+
19
+ - 导出文件用任意文本编辑器打开**无明文密码**(整个 payload 加密)。
20
+ - 导出 → 删除全部连接 → 导入,round-trip 后 `testTarget` 可拼回真实 URL/密码,连接可 test 连通。
21
+ - 重复导入幂等:已存在 id 全部计入 `skipped`,不覆盖。
22
+ - 零新增 npm 依赖(全部走 Node stdlib + 各库官方驱动)。
23
+
24
+ ### 明确不做(YAGNI)
25
+
26
+ - 不含 `grants.json` / `audit.jsonl`:授权是「项目路径 ↔ 连接」的机器绑定关系,跨机迁移无意义;审计不可移植。
27
+ - 不做选择性导出(勾选部分连接):需要时后续给 export API 加 `ids?: string[]` 即可,不预留抽象。
28
+ - 不在 `handleToolAction`(模型工具)暴露导出:机密不应进入 LLM 上下文,仅 HTTP 面板可用。
29
+
30
+ ## 2. 关键设计决策
31
+
32
+ | 决策点 | 结论 | 依据 |
33
+ |---|---|---|
34
+ | 导出内容 | 含密码 | 目标是跨机器迁移,无密码无法做到导入即可用 |
35
+ | 加密算法 | 用户口令 + scrypt 派生密钥 + AES-256-GCM | Node stdlib `crypto` 足够,不引第三方加密库(ponytail 阶梯:stdlib 优先) |
36
+ | 冲突策略 | **skip**(不覆盖) | 用户明确选择;幂等可重入,重复导入安全 |
37
+ | 错误反馈 | 口令错 / 密文篡改 / 格式坏**统一**报「口令错误或文件已损坏」 | 不向外泄露失败原因,防侧信道探测 |
38
+ | AAD | 不使用 | 见 §3「AAD 裁决」 |
39
+
40
+ ## 3. 加密格式(按 Node 官方文档校准)
41
+
42
+ 信封结构(JSON,二进制段全 base64):
43
+
44
+ ```json
45
+ {
46
+ "v": 1,
47
+ "kdf": "scrypt",
48
+ "salt": "<base64, 16 字节>",
49
+ "iv": "<base64, 12 字节>",
50
+ "tag": "<base64, 16 字节>",
51
+ "data": "<base64 密文>"
52
+ }
53
+ ```
54
+
55
+ ### 官方文档逐条对照
56
+
57
+ | 参数 | 取值 | 官方依据(context7 → nodejs/node) |
58
+ |---|---|---|
59
+ | KDF | `scryptSync(passphrase, salt, 32)` | `crypto.scryptSync` 官方定位为 password-based KDF,"designed to be expensive computationally and memory-wise in order to make brute-force attacks unrewarding" |
60
+ | scrypt cost/blockSize/parallelization | **默认值**(N=16384, r=8, p=1),maxmem 默认 32MB | 官方默认值即推荐值,不显式覆盖 |
61
+ | salt | 随机 **16 字节**(`randomBytes(16)`) | 官方:"It is recommended that a salt is random and at least 16 bytes long"(NIST SP 800-132) |
62
+ | 算法 | `aes-256-gcm` | GCM 同时提供机密性与完整性(auth tag 防篡改) |
63
+ | IV | 随机 **12 字节**(`randomBytes(12)`) | OpenSSL 官方变更说明:"The IV length is by default 12 bytes (96 bits)",GCM 推荐 96-bit |
64
+ | auth tag | **默认 16 字节**,不显式传 `authTagLength` | Node DEP0182 已废止短 tag(<16B)以对齐 NIST SP 800-38D;`createCipheriv` 文档明确 GCM 默认 16 字节 |
65
+ | 解密顺序 | `setAuthTag(tag)` 必须在 `final()` 前 | 官方示例代码(`test-crypto-authenticated.js`):`decrypt.setAuthTag(...)` → `update` → `final` |
66
+
67
+ ### AAD 裁决(不使用,理由记录)
68
+
69
+ 官方示例用 `setAAD` 把密文绑定到外部上下文(用户 id、文件名等),防信封被整体搬到别的上下文冒充。本方案不加,理由:
70
+
71
+ 1. 信封自含 `v/kdf/salt/iv/tag`,且**口令本身就是解密前提**——信封整体替换后在任何机器上都需要同一口令才能解,AAD 不增加实际安全边界。
72
+ 2. 引入 AAD 需要持久化一个额外的上下文字段并保持两端同步,是真实复杂度(ponytail 阶梯:两个同级方案取边界更简单的)。
73
+ 3. 文件本身就是交付物,不存在「信封被混进别的数据流」的攻击面。
74
+
75
+ 解密任一环节失败(JSON 解析、结构校验、base64 长度校验、GCM tag 验证、明文 JSON 解析)统一抛 `Error('口令错误或文件已损坏')`(常量 `DECRYPT_FAIL_MESSAGE`)。
76
+
77
+ 明文 payload 结构:
78
+
79
+ ```json
80
+ {
81
+ "connections": [{ "id", "kind", "name?", "urlSafe?", "fields?", "ssl?" }],
82
+ "secrets": { "<connId>": { "url?", "password?", "...其他机密键" } }
83
+ }
84
+ ```
85
+
86
+ `secrets` 保留 `SecretEntry` 的扩展键(如 MongoDB 的额外凭证),不只挑 `url/password`。
87
+
88
+ ## 4. 实现分层
89
+
90
+ ### 4.1 存储层 — `lib/store/`
91
+
92
+ **`export-crypto.ts`(新建,纯函数)**
93
+
94
+ - `encryptPayload(plain: unknown, passphrase: string): string` — 非空口令断言 → scryptSync(pass, salt, 32) → AES-256-GCM → 信封 JSON。
95
+ - `decryptPayload(encryptedJson: string, passphrase: string): unknown` — 结构校验 + iv/tag/salt 长度校验 + tag 验证 → 原始 payload;任一失败统一抛 `DECRYPT_FAIL_MESSAGE`。
96
+ - `DECRYPT_FAIL_MESSAGE` 导出供 service 层复用。
97
+
98
+ **`connections.ts`(修改)**
99
+
100
+ - `exportBundle(): { connections: ConnRecord[]; secrets: Record<string, SecretEntry> }`
101
+ 从 `load()` 取全部连接记录,逐条 `this.secrets.get(id)` 合并;无机密的连接不出现在 `secrets` 中。
102
+ - `importBundle(bundle): { imported: number; skipped: string[]; errors: {id, message}[] }`
103
+ 逐条走**既有** `this.create()`(机密拆分 / URL 脱敏逻辑全复用,不新造写入路径):`sec.url` 作完整 URL 传入、`sec.password` 经 `fields.password` 自动拆分。冲突(`连接已存在`)→ `skipped`;其他异常 → `errors` 续跑。create 后 `this.secrets.set(rec.id, sec)` 幂等合并写回,保留扩展机密键。
104
+
105
+ **`index.ts`(修改)**:补导出 `encryptPayload / decryptPayload / DECRYPT_FAIL_MESSAGE` 及 `ConnRecord` 类型。
106
+
107
+ ### 4.2 服务层 — `lib/manager.ts`(DbToolService)
108
+
109
+ - `exportConnections(passphrase: string): string`
110
+ 非空校验 → `store.exportBundle()` → `encryptPayload` → 审计 `export_connections`(statement 仅记「导出 N 个连接」,不落口令/密文)。
111
+ - `importConnections(encryptedJson: string, passphrase: string)`
112
+ 解密失败 + 结构校验失败(缺 `connections` 数组)统一转 `DbToolError('INVALID_ARGUMENT', DECRYPT_FAIL_MESSAGE)` → `store.importBundle` → 审计 `import_connections`(记 imported/skipped 数)。
113
+
114
+ ### 4.3 HTTP 层 — `lib/http/index.ts`
115
+
116
+ 两条新路由,**插在 `connMatch` 之前**(`/api/connections/export` 与 `/api/connections/import` 都是单层路径,放后面会被正则 `^/api/connections/([^/]+)` 当作连接 id 静默吞掉):
117
+
118
+ | 路由 | Body | 成功 | 失败 |
119
+ |---|---|---|---|
120
+ | `POST /api/connections/export` | `{passphrase}` | `{ok:true, data:{json}}` | 缺/空 passphrase → 400 INVALID_ARGUMENT |
121
+ | `POST /api/connections/import` | `{json, passphrase}` | `{ok:true, data:{imported, skipped[], errors[]}}` | 缺参 400;口令错走 `sendError` → 200 + `{ok:false, code:'INVALID_ARGUMENT'}`(与既有契约一致) |
122
+
123
+ trust 校验(Host/Origin loopback)、2MB body 上限、`{ok,error,code}` 响应契约全部复用既有链路。
124
+
125
+ ### 4.4 客户端 — `client/client.js`(ManageView)
126
+
127
+ **i18n**:zh/en 各加 8 键(exportConns / importConns / passphrase / passphraseHint / exportDone / importResult / importDecryptFail / pickFile)。
128
+
129
+ **`PassphraseDialog`**(局部组件,紧邻 `EMPTY_FORM`):复用 `dbt-overlay/dbt-dialog` 样式;键盘契约对齐 `DangerDialog`(Escape=取消、Tab 循环锁定),差异——初始焦点在口令输入框、确认按钮主色、Enter 提交、前端预校验 ≥6 位。ponytail:仅两处使用,未抽通用模块。
130
+
131
+ **导出 / 导入文件 IO(按 MDN 官方文档校准)**:
132
+
133
+ | 操作 | MDN 最佳实践 | 实现 |
134
+ |---|---|---|
135
+ | 下载 | `Blob` → `URL.createObjectURL` → `a.download` → click | `dsh-db-connections-YYYYMMDD.enc.json` |
136
+ | 释放 object URL | 必须 `revokeObjectURL`,但**不能过早**(官方明示:立即 revoke 会让用户无法保存/打开) | `a.click()` 后 `setTimeout(revoke, 1000)`,与仓库内结果导出下载同一模式 |
137
+ | 读取本地文件 | `FileReader.readAsText` + `onload/onerror` 双通道,先校验文件存在 | 读失败走通用错误通道 |
138
+
139
+ **流程**:
140
+ - 导出:点导出 → 口令窗 → `POST connections/export` → Blob 下载 → `dbt-msg` 显示 `exportDone`。
141
+ - 导入:点导入 → 隐藏 `input[type=file accept=".json"]` 选文件 → FileReader 读文本 → 口令窗 → `POST connections/import` → `reload()` → `dbt-msg` 显示 `importResult`;`err.code === 'INVALID_ARGUMENT'` 时 `dbt-err` 显示 `importDecryptFail`。
142
+
143
+ ## 5. 边界与失败模式
144
+
145
+ | 场景 | 行为 |
146
+ |---|---|
147
+ | 口令为空 / 非字符串 | store 层抛 `Error('口令必须为非空字符串')`;HTTP 层 400 |
148
+ | 密文被篡改 | GCM tag 校验失败 → 统一「口令错误或文件已损坏」(不泄露是哪一环) |
149
+ | 导入文件非信封结构 / 明文缺 `connections` 数组 | 同上统一错误 |
150
+ | 单条导入失败(kind 非法、secrets 写入 I/O 故障等) | 记入 `errors` 续跑,不整批回滚(与 skip 语义一致,幂等可重入) |
151
+ | 连接无密码 | 不出现在 `secrets` 段;导入后无 `hasPassword` 指示 |
152
+
153
+ ## 6. 测试
154
+
155
+ - `tests/store/export-crypto.spec.ts`(5 例):round-trip(密文不含明文)、随机 salt/iv 同文异密、错口令统一错误、篡改 data/tag/结构统一错误、空/非字符串口令拒绝。
156
+ - `tests/store/connections.spec.ts`(追加 4 例):exportBundle 含密码、importBundle skip 冲突不覆盖 + 重复导入幂等、单条异常入 errors 续跑、round-trip 后 testTarget 密码/URL 一致且 connections.json 无明文。
157
+ - `tests/tool/manager.spec.ts`(追加 4 例):round-trip(导出→删除→导入恢复)、错口令 INVALID_ARGUMENT、空参数 INVALID_ARGUMENT、导出/导入审计断言(记条数、不落口令)。
158
+ - `tests/http/api.spec.ts`(追加 3 例):happy path(HTTP 建连接→导出→全删→导入恢复,断言密文不含明文连接串)、缺/空 passphrase 400、坏密文导入 INVALID_ARGUMENT。
159
+
160
+ 验收命令:`npm run typecheck && npm test && npm run build`,全部通过。
161
+
162
+ ## 7. 实施过程(Agent Teams 并行)
163
+
164
+ 任务拆为 3 个 teammate 并行,写范围互斥:
165
+
166
+ | Teammate | 写范围 | 产出 |
167
+ |---|---|---|
168
+ | store-crypto | `lib/store/connections.ts`、`lib/store/export-crypto.ts` + 配套测试 | 存储层 + 加解密,21 测试绿 |
169
+ | service-http | `lib/manager.ts`、`lib/http/index.ts` + 配套测试 | service + HTTP 路由,46 测试绿 |
170
+ | client-ui | `client/client.js` | UI + i18n,98 client 测试无回归 |
171
+
172
+ 契约先行冻结(store 方法签名、HTTP 端点、错误 code),三路按契约并行开发;store-crypto 完成后 service-http 的真实 store 联跑自然转绿。Lead 负责收尾 integrator(`lib/store/index.ts` 导出面)+ 全仓验收。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.5.9",
3
+ "version": "1.6.0",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,8 +26,9 @@
26
26
  "test": "vitest run",
27
27
  "test:watch": "vitest",
28
28
  "typecheck": "tsc --noEmit",
29
- "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\" && node scripts/build-mongodb.mjs",
29
+ "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\" && node scripts/build-mongodb.mjs && node scripts/build-codemirror.mjs",
30
30
  "build:mongodb": "node scripts/build-mongodb.mjs",
31
+ "build:codemirror": "node scripts/build-codemirror.mjs",
31
32
  "prepack": "npm run build",
32
33
  "patch:dsh": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/apply-dsh-hotfix.ps1",
33
34
  "patch:dsh:sh": "bash scripts/apply-dsh-hotfix.sh"
@@ -56,7 +57,14 @@
56
57
  "better-sqlite3": "^13.0.3"
57
58
  },
58
59
  "devDependencies": {
60
+ "@codemirror/autocomplete": "^6.20.3",
61
+ "@codemirror/commands": "^6.11.1",
62
+ "@codemirror/lang-sql": "^6.10.0",
63
+ "@codemirror/language": "^6.12.4",
64
+ "@codemirror/state": "^6.7.6",
65
+ "@codemirror/view": "^6.43.13",
59
66
  "@deepseek-ai/cordis": "^4.0.4",
67
+ "@lezer/highlight": "^1.2.4",
60
68
  "@stryker-mutator/core": "^10.0.0",
61
69
  "@stryker-mutator/vitest-runner": "^10.0.0",
62
70
  "@types/better-sqlite3": "latest",
@@ -64,6 +72,7 @@
64
72
  "dsh-better-sidebar": "^0.24.1",
65
73
  "esbuild": "^0.28.0",
66
74
  "mongodb": "^7.6.0",
75
+ "sql-formatter": "^15.9.0",
67
76
  "stryker-cli": "^1.1.0",
68
77
  "typescript": "^5.9.3",
69
78
  "vitest": "^4.1.11"
@@ -81,6 +90,7 @@
81
90
  "patches",
82
91
  "scripts",
83
92
  "vendor/mongodb-driver.cjs",
93
+ "vendor/codemirror-sql.cjs",
84
94
  "README.md",
85
95
  "README_EN.md"
86
96
  ]