dsh-db-tool 1.2.0 → 1.3.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.md CHANGED
@@ -40,7 +40,7 @@ GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb
40
40
 
41
41
  ## 安全模型
42
42
 
43
- - HTTP API 仅同源(`ctx.webServer` prefix `/dsh-db-tool/api`)+ loopback/Origin trust 校验(等价 ssh-tunnel `http-trust`),body 限 2MB
43
+ - HTTP API 仅同源(`ctx.webServer` prefix `/dsh-db-tool/api`)+ 宿主 Connection 栅栏(`ctx.connection.requestRejection`:Host/Origin + 浏览器 token 鉴权。0.2.0-rc.1 起插件前缀路由不经宿主栅栏,故在 handler 顶层显式应用;宿主无 connection 服务时回退 loopback/Origin trust 校验,等价 ssh-tunnel `http-trust`),body 限 2MB
44
44
  - ro 双保险:服务层拦截 + 驱动会话级 `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`(pg/gaussdb)、readonly 打开(SQLite)
45
45
  - SQL 参数绑定 + 标识符白名单(`[A-Za-z0-9_$]+` + 引用包裹);MySQL `multipleStatements:false`;Redis 元数据走 `SCAN`(禁 `KEYS`);Mongo 递归拒 `$where`
46
46
  - `run_script`:node:vm 独立 context、60s 超时、无 require/process/网络/文件系统,仅注入受限 `db.{query,execute}` 句柄
@@ -52,7 +52,7 @@ GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb
52
52
 
53
53
  症状:DSH 启动后插件加载报 `failed to import`,伴随 `TypeError: Cannot read properties of null (reading 'Symbol(Symbol.iterator)')`,栈指向 `dsh-app-boot` 的 `routeScoped`。
54
54
 
55
- 根因:上游 `@deepseek-ai/dsh-app-boot` 对 `createRequire(parent).resolve.paths(name)` 直接做 `for..of`,而 Node 对 core-module 同名包(`punycode` 等)返回 `null`,hoisted profile 下凡依赖树含此类 npm 包的插件都会炸。`0.1.7-rc.2` 中共两处同型缺陷:`routeScoped()` 与 `nativePackageDir()`,本补丁一并修复(两处分别独立判定,已打过旧版单处补丁的机器只会补上缺的那处)。
55
+ 根因:上游 `@deepseek-ai/dsh-app-boot` 对 `createRequire(parent).resolve.paths(name)` 直接做 `for..of`,而 Node 对 core-module 同名包(`punycode` 等)返回 `null`,hoisted profile 下凡依赖树含此类 npm 包的插件都会炸。`0.1.7-rc.2` 与 `0.2.0-rc.1` 中同型缺陷两处(锚点行逐字节一致,仅整体行号偏移 1 行):`routeScoped()` 与 `nativePackageDir()`,本补丁一并修复(两处分别独立判定,已打过旧版单处补丁的机器只会补上缺的那处)。
56
56
 
57
57
  一键修复(幂等,应用前自动备份为 `index.js.bak-hotfix`;`--revert` 可还原):
58
58
 
@@ -61,7 +61,7 @@ npm run patch:dsh # Windows(PowerShell)
61
61
  npm run patch:dsh:sh # macOS / Linux
62
62
  ```
63
63
 
64
- 脚本自动探测 DSH 安装根(`--dsh-root` 可显式指定);补丁文件见 `patches/dsh-app-boot-route-scoped-hotfix.patch`,仅对 `0.1.7-rc.2` 声明兼容,其他版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
64
+ 脚本自动探测 DSH 安装根(`--dsh-root` 可显式指定);补丁文件按安装版本自动选择:`patches/dsh-app-boot-route-scoped-hotfix.patch`(`0.1.7-rc.2`)、`patches/dsh-app-boot-route-scoped-hotfix-0.2.0-rc.1.patch`(`0.2.0-rc.1`,与内建替换路径逐字节等价已验证);不支持的版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
65
65
 
66
66
  **本插件的免补丁路径(0.1.9+)**:该 bug 的触发条件是依赖树中出现 core-module 同名包,mongodb 链(`mongodb-connection-string-url → whatwg-url → tr46 → punycode`)正是元凶。0.1.9 起 mongodb 驱动经 esbuild bundle 为 `vendor/mongodb-driver.cjs` 随包发布,生产依赖树不再含 mongodb 与 punycode——在**未修复宿主**(含桌面版 app.asar,无法打补丁)上也能正常安装加载。守卫测试 `tests/guard/deps-core-collision.spec.ts` 断言依赖树永不回退(dependencies 白名单 + lockfile 生产树 core 同名包零交集)。`patch:dsh` 仍保留,用于修复**其他**含同类依赖的插件或宿主自身报错。
67
67
 
package/dist/index.js CHANGED
@@ -4,15 +4,17 @@
4
4
  * - 注册 DatabaseManager 模型工具(单工具多 action),projectPath 取自
5
5
  * session header.cwd(不信任模型自报),业务操作经 GrantStore 授权。
6
6
  * - HTTP API 挂载在 DSH webServer(prefix /dsh-db-tool/api):前端同源
7
- * 相对路径 fetch('/dsh-db-tool/api/...') 即可达;trust 校验移植
8
- * http-trust 逻辑(Host loopback / trustedHosts + Origin 同源校验)。
7
+ * 相对路径 fetch('/dsh-db-tool/api/...') 即可达;0.2.0-rc.1 起插件前缀路由
8
+ * 不经宿主 Connection 栅栏,故在 handler 顶层先应用 ctx.connection.requestRejection
9
+ * (Host/Origin + 浏览器 token 鉴权,401/403);宿主无 connection 服务时退回
10
+ * http-trust 本地校验(Host loopback / trustedHosts + Origin 同源校验)。
9
11
  * - ctx.effect 收尾:注销 HTTP 路由、关适配器、停 challenge 清理。
10
12
  */
11
13
  import { DbToolStore, normalizeProjectKey } from './store/index.js';
12
14
  import { DbToolService, handleToolAction } from './manager.js';
13
15
  import { handleDbToolRequest } from './http/index.js';
14
- /** cordis 注入清单(服务名以 dsh-ssh-tunnel 为准) */
15
- export const inject = ['webServer', 'sessions', 'tools', 'webRuntime'];
16
+ /** cordis 注入清单(服务名以 dsh-ssh-tunnel 为准;connection 用于宿主鉴权栅栏) */
17
+ export const inject = ['webServer', 'sessions', 'tools', 'webRuntime', 'connection'];
16
18
  const TOOL_DESCRIPTION = `数据库管理工具(dsh-db-tool):在已配置的数据库连接上执行查询/写操作/脚本。
17
19
 
18
20
  权限模型(务必遵守):
@@ -50,6 +52,21 @@ export function apply(ctx) {
50
52
  kind: 'prefix',
51
53
  path: '/dsh-db-tool/api',
52
54
  handler: async (req, res) => {
55
+ // 宿主 Connection 栅栏优先:0.2.0-rc.1 起插件前缀路由不经宿主鉴权,
56
+ // 必须显式应用 requestRejection(Host/Origin + 浏览器 token);
57
+ // 401/403 时由本处兜底写出 JSON 响应(宿主只返回状态码)。
58
+ const rejection = ctx.connection?.requestRejection?.(req);
59
+ if (rejection !== undefined) {
60
+ const httpRes = res;
61
+ httpRes.statusCode = rejection;
62
+ httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
63
+ httpRes.end(JSON.stringify({
64
+ ok: false,
65
+ error: rejection === 401 ? 'unauthorized: browser authentication required' : 'forbidden: untrusted request',
66
+ code: rejection === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
67
+ }));
68
+ return;
69
+ }
53
70
  await handleDbToolRequest(req, res, service, {
54
71
  trustedHosts: trustedHosts(),
55
72
  // 项目上下文权威解析:会话 header.cwd 优先,回退前端 cwd(对齐 ssh-tunnel getProjectContext)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",
@@ -14,7 +14,7 @@
14
14
  "homepage": "https://github.com/mengqi1436/dsh-db-tool#readme",
15
15
  "engines": {
16
16
  "node": ">=20",
17
- "dsh": ">=0.1.0-rc.6"
17
+ "dsh": ">=0.1.0-rc.6 <0.2.0-0 || 0.1.7-rc.2 || >=0.2.0-rc.1 <0.3.0-0"
18
18
  },
19
19
  "main": "dist/index.js",
20
20
  "exports": {
@@ -55,10 +55,12 @@
55
55
  "redis": "^6.2.1"
56
56
  },
57
57
  "devDependencies": {
58
+ "@deepseek-ai/cordis": "^4.0.4",
58
59
  "@stryker-mutator/core": "^10.0.0",
59
60
  "@stryker-mutator/vitest-runner": "^10.0.0",
60
61
  "@types/better-sqlite3": "latest",
61
62
  "@types/node": "latest",
63
+ "dsh-better-sidebar": "^0.24.1",
62
64
  "esbuild": "^0.28.0",
63
65
  "mongodb": "^7.6.0",
64
66
  "stryker-cli": "^1.1.0",
@@ -0,0 +1,40 @@
1
+ # DSH dsh-app-boot resolve.paths hotfix (upstream bug: createRequire(parent).resolve.paths(name)
2
+ # returns null for core-module names like "punycode", breaking hoisted profile installs).
3
+ # Covers BOTH unguarded for..of sites in 0.2.0-rc.1: routeScoped() (@@ -1423,14) and
4
+ # nativePackageDir() (@@ -1245,10). Anchor lines are byte-identical to 0.1.7-rc.2 (overall +1 shift).
5
+ # Target-Version: 0.2.0-rc.1
6
+ # Apply manually: git apply -p1 -d <dsh-app-boot package root> <this file>
7
+ # Or one command: npm run patch:dsh (scripts/apply-dsh-hotfix.ps1)
8
+ # Generated from @deepseek-ai/dsh-app-boot@0.2.0-rc.1 npm tarball vs patched local copy.
9
+ diff --git a/lib/index.js b/lib/index.js
10
+ index e60aac4..fcd1a38 100644
11
+ --- a/lib/index.js
12
+ +++ b/lib/index.js
13
+ @@ -1245,7 +1245,10 @@ function profileChild(path, treePrefix) {
14
+ return treePrefix + (end < 0 ? rest : rest.slice(0, end));
15
+ }
16
+ function nativePackageDir(parent, name) {
17
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
18
+ + /* local hotfix: nativePackageDir: resolve.paths returns null for core-module names
19
+ + (fs, node:fs, ...); an empty list means no local candidate, matching the
20
+ + guard in profile.ts:596. */
21
+ + for (const searchPath of createRequire(parent).resolve.paths(name) ?? []) {
22
+ const candidate = join(searchPath, name);
23
+ if (existsSync(join(candidate, "package.json"))) return candidate;
24
+ }
25
+ @@ -1420,7 +1423,14 @@ var ResolutionRouter = class {
26
+ const target = resolution.entries.get(name);
27
+ const candidates = [];
28
+ const localSearchPaths = [];
29
+ - for (const searchPath of createRequire(parent).resolve.paths(name)) {
30
+ + /* local hotfix: resolve.paths returns null for core-module names (e.g. "punycode"),
31
+ + which made the for..of throw TypeError and broke every hoisted profile install
32
+ + whose dependency tree requires such an npm package. Fall back to the standard
33
+ + node_modules search chain so the local candidate can still be found. */
34
+ + const _hotReq = createRequire(parent);
35
+ + const _hotPaths = _hotReq.resolve.paths(name)
36
+ + ?? _hotReq("node:module")._nodeModulePaths(_hotReq("node:path").dirname(parent));
37
+ + for (const searchPath of _hotPaths) {
38
+ if (!searchPath.startsWith(layer.localPrefix)) break;
39
+ localSearchPaths.push(searchPath);
40
+ const candidate = localPackageCandidate(searchPath, name, flavor);
@@ -2,6 +2,8 @@
2
2
  * 类型声明:scripts/hotfix-core.mjs(无 allowJs,为 tests/scripts/hotfix.spec.ts 提供 import 类型)。
3
3
  */
4
4
  export declare const TARGET_VERSION: string;
5
+ export declare const TARGET_VERSIONS: string[];
6
+ export declare const PATCH_BY_VERSION: Record<string, string>;
5
7
  export declare const HOTFIX_MARKER: string;
6
8
  export declare const BOOT_INDEX_REL: string;
7
9
  export declare const BACKUP_SUFFIX: string;
@@ -2,12 +2,15 @@
2
2
  /**
3
3
  * DSH dsh-app-boot resolve.paths hotfix —— 核心逻辑(纯 Node、零依赖、可被测试 import)。
4
4
  *
5
- * 修复的 bug:@deepseek-ai/dsh-app-boot@0.1.7-rc.2 对
5
+ * 修复的 bug:@deepseek-ai/dsh-app-boot 对
6
6
  * `createRequire(parent).resolve.paths(name)` 的 for..of 无保护,而 Node 对
7
7
  * core-module 同名包(punycode 等)返回 null,for..of null 直接抛 TypeError,
8
- * 导致 hoisted profile 下 npm 安装插件导入失败。rc.2 中共两处同型缺陷:
9
- * 1. routeScoped()(L1419 区域)—— 症状直接暴露;
10
- * 2. nativePackageDir()(L1246 区域)—— 第二处,症状相同,一并修复。
8
+ * 导致 hoisted profile 下 npm 安装插件导入失败。rc.2 起共两处同型缺陷:
9
+ * 1. routeScoped()(rc.2 L1419 区域 / 0.2.0-rc.1 L1423 区域)—— 症状直接暴露;
10
+ * 2. nativePackageDir()(rc.2 L1244 区域 / 0.2.0-rc.1 L1248 区域)—— 症状相同,一并修复。
11
+ *
12
+ * 版本支持:锚点行在 TARGET_VERSIONS 列出的版本中逐字节一致(仅整体行号偏移),
13
+ * 内建替换路径与版本无关;git-apply 路径按安装版本选择 patches/ 下行号匹配的补丁。
11
14
  *
12
15
  * 用法:node scripts/hotfix-core.mjs [--dsh-root <dir>] [--patch-file <file>]
13
16
  * [--force] [--revert] [--dry-run]
@@ -20,6 +23,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
20
23
 
21
24
  /** 补丁文件里的目标版本声明行(patches/*.patch 注释头)。 */
22
25
  export const TARGET_VERSION = '0.1.7-rc.2';
26
+ /** 锚点行逐字节一致的受支持版本列表(仅整体行号偏移;0.2.0-rc.1 = rc.2 行号 +1)。 */
27
+ export const TARGET_VERSIONS = ['0.1.7-rc.2', '0.2.0-rc.1'];
28
+ /** 各受支持版本对应的默认补丁文件名(patches/ 下;git-apply 路径按版本选择)。 */
29
+ export const PATCH_BY_VERSION = {
30
+ '0.1.7-rc.2': 'dsh-app-boot-route-scoped-hotfix.patch',
31
+ '0.2.0-rc.1': 'dsh-app-boot-route-scoped-hotfix-0.2.0-rc.1.patch',
32
+ };
23
33
  /** 判断目标文件是否已打补丁的标识串(hotfix 注释首行片段)。 */
24
34
  export const HOTFIX_MARKER = 'local hotfix: resolve.paths returns null';
25
35
  /** dsh-app-boot 内 lib/index.js 相对包根的路径。 */
@@ -287,15 +297,18 @@ function isGitApplyable(indexFile, patchFile) {
287
297
  /** 命令行主入口。@returns {number} 退出码 */
288
298
  export function run(argv = process.argv.slice(2)) {
289
299
  let dshRoot;
290
- let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', 'dsh-app-boot-route-scoped-hotfix.patch');
300
+ let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[TARGET_VERSION]);
301
+ let patchFileExplicit = false;
291
302
  let force = false;
292
303
  let revert = false;
293
304
  let dryRun = false;
294
305
  for (let i = 0; i < argv.length; i++) {
295
306
  const arg = argv[i];
296
307
  if (arg === '--dsh-root') dshRoot = argv[++i];
297
- else if (arg === '--patch-file') patchFile = argv[++i];
298
- else if (arg === '--force') force = true;
308
+ else if (arg === '--patch-file') {
309
+ patchFile = argv[++i];
310
+ patchFileExplicit = true;
311
+ } else if (arg === '--force') force = true;
299
312
  else if (arg === '--revert') revert = true;
300
313
  else if (arg === '--dry-run') dryRun = true;
301
314
  else if (arg === '--help' || arg === '-h') {
@@ -324,11 +337,25 @@ export function run(argv = process.argv.slice(2)) {
324
337
  } catch {
325
338
  /* 读不到版本号时跳过比对 */
326
339
  }
340
+ // 按安装版本选择行号匹配的补丁(未显式指定 --patch-file 时);
341
+ // 锚点内容各版本一致,选错补丁只会让 git-apply 路径失配回退内建替换,但优先选对。
342
+ if (!patchFileExplicit && actualVersion && PATCH_BY_VERSION[actualVersion]) {
343
+ patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', PATCH_BY_VERSION[actualVersion]);
344
+ }
327
345
  const patchText = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, 'utf8') : '';
328
- const declared = parsePatchTargetVersion(patchText) ?? TARGET_VERSION;
329
- if (actualVersion && actualVersion !== declared && !force) {
330
- console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
331
- return 2;
346
+ const declared = parsePatchTargetVersion(patchText);
347
+ if (actualVersion && !force) {
348
+ if (declared) {
349
+ if (declared !== actualVersion) {
350
+ console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
351
+ return 2;
352
+ }
353
+ } else if (!TARGET_VERSIONS.includes(actualVersion)) {
354
+ console.warn(
355
+ `[dsh-hotfix] version mismatch: installed ${actualVersion} is not supported (supported: ${TARGET_VERSIONS.join(', ')}) and patch declares no Target-Version. Use --force to apply anyway.`,
356
+ );
357
+ return 2;
358
+ }
332
359
  }
333
360
 
334
361
  let content;