@dbx-app/node-core 0.4.4 → 0.4.6

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,7 @@ export interface ConnectionConfig {
10
10
  database?: string;
11
11
  url_params?: string;
12
12
  ssh_enabled: boolean;
13
+ ssh_tunnels?: SshTunnelConfig[];
13
14
  proxy_enabled?: boolean;
14
15
  proxy_type?: "socks5" | "http";
15
16
  proxy_host?: string;
@@ -26,6 +27,19 @@ export interface ConnectionConfig {
26
27
  redis_sentinel_password?: string;
27
28
  redis_sentinel_tls?: boolean;
28
29
  }
30
+ export interface SshTunnelConfig {
31
+ id: string;
32
+ name?: string;
33
+ enabled?: boolean;
34
+ host: string;
35
+ port: number;
36
+ user: string;
37
+ password?: string;
38
+ key_path?: string;
39
+ key_passphrase?: string;
40
+ connect_timeout_secs?: number;
41
+ expose_lan?: boolean;
42
+ }
29
43
  export interface ConnectionStoreOptions {
30
44
  path?: string;
31
45
  }
@@ -143,6 +143,7 @@ export async function addConnection(config) {
143
143
  ssh_key_path: "",
144
144
  ssh_key_passphrase: "",
145
145
  ssh_expose_lan: false,
146
+ ssh_tunnels: normalized.ssh_tunnels ?? [],
146
147
  proxy_enabled: normalized.proxy_enabled ?? false,
147
148
  proxy_type: normalized.proxy_type ?? "socks5",
148
149
  proxy_host: normalized.proxy_host ?? "",
package/dist/database.js CHANGED
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { homedir, platform } from "node:os";
5
5
  import Database from "better-sqlite3";
6
6
  import { sqlSafetyFromEnv } from "./sql-safety.js";
7
+ import { isDirectQueryType } from "./diagnostics.js";
7
8
  const MAX_ROWS = 100;
8
9
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
9
10
  const QUERY_TIMEOUT_MS = 30_000;
@@ -194,19 +195,6 @@ function portBytes(port) {
194
195
  function isMysqlType(dbType) {
195
196
  return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
196
197
  }
197
- function isDirectType(dbType) {
198
- switch (dbType) {
199
- case "postgres":
200
- case "redshift":
201
- case "mysql":
202
- case "doris":
203
- case "starrocks":
204
- case "sqlite":
205
- return true;
206
- default:
207
- return false;
208
- }
209
- }
210
198
  function bridgeAppDataDir() {
211
199
  const home = homedir();
212
200
  switch (platform()) {
@@ -366,7 +354,7 @@ export async function executeQuery(config, sql, options) {
366
354
  }
367
355
  throw new Error("Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.projects.countDocuments({}), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})");
368
356
  }
369
- if (isDirectType(config.db_type)) {
357
+ if (isDirectQueryType(config.db_type)) {
370
358
  return query(config, sql, undefined, options);
371
359
  }
372
360
  const result = await withTimeout(bridgeDataRequest("/data/execute-query", {
@@ -389,7 +377,7 @@ export async function listTables(config, schema) {
389
377
  const result = await query(config, `SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name`);
390
378
  return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
391
379
  }
392
- if (!isDirectType(config.db_type)) {
380
+ if (!isDirectQueryType(config.db_type)) {
393
381
  const tables = await bridgeDataRequest("/data/list-tables", {
394
382
  connection_name: config.name,
395
383
  database: config.database || "",
@@ -422,7 +410,7 @@ export async function describeTable(config, table, schema) {
422
410
  comment: null,
423
411
  }));
424
412
  }
425
- if (!isDirectType(config.db_type)) {
413
+ if (!isDirectQueryType(config.db_type)) {
426
414
  const columns = await bridgeDataRequest("/data/describe-table", {
427
415
  connection_name: config.name,
428
416
  database: config.database || "",
@@ -743,7 +731,7 @@ function readChainedIntegerArgument(chain, method, fallback) {
743
731
  return Number(arg.trim());
744
732
  }
745
733
  function normalizeJsonArgument(arg) {
746
- const value = (arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}');
734
+ const value = quoteUnquotedObjectKeys(convertSingleQuotedStrings((arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}')));
747
735
  try {
748
736
  JSON.parse(value);
749
737
  return value;
@@ -752,6 +740,100 @@ function normalizeJsonArgument(arg) {
752
740
  return null;
753
741
  }
754
742
  }
743
+ function convertSingleQuotedStrings(source) {
744
+ let result = "";
745
+ let copiedUntil = 0;
746
+ let quote = null;
747
+ let start = 0;
748
+ let value = "";
749
+ let escaped = false;
750
+ for (let i = 0; i < source.length; i += 1) {
751
+ const char = source[i];
752
+ if (!quote) {
753
+ if (char === "'") {
754
+ quote = char;
755
+ start = i;
756
+ value = "";
757
+ escaped = false;
758
+ }
759
+ else if (char === '"') {
760
+ quote = char;
761
+ }
762
+ continue;
763
+ }
764
+ if (quote === '"') {
765
+ if (escaped)
766
+ escaped = false;
767
+ else if (char === "\\")
768
+ escaped = true;
769
+ else if (char === '"')
770
+ quote = null;
771
+ continue;
772
+ }
773
+ if (escaped) {
774
+ value += char;
775
+ escaped = false;
776
+ }
777
+ else if (char === "\\") {
778
+ escaped = true;
779
+ }
780
+ else if (char === "'") {
781
+ result += source.slice(copiedUntil, start) + JSON.stringify(value);
782
+ copiedUntil = i + 1;
783
+ quote = null;
784
+ }
785
+ else {
786
+ value += char;
787
+ }
788
+ }
789
+ return quote === "'" ? source : result + source.slice(copiedUntil);
790
+ }
791
+ function quoteUnquotedObjectKeys(source) {
792
+ let result = "";
793
+ let quote = null;
794
+ let escaped = false;
795
+ for (let i = 0; i < source.length; i += 1) {
796
+ const char = source[i];
797
+ if (quote) {
798
+ result += char;
799
+ if (escaped)
800
+ escaped = false;
801
+ else if (char === "\\")
802
+ escaped = true;
803
+ else if (char === quote)
804
+ quote = null;
805
+ continue;
806
+ }
807
+ if (char === '"' || char === "'") {
808
+ quote = char;
809
+ result += char;
810
+ continue;
811
+ }
812
+ if (/[A-Za-z_$]/.test(char) && shouldQuoteObjectKey(source, i)) {
813
+ let end = i + 1;
814
+ while (/[\w$]/.test(source[end] || ""))
815
+ end += 1;
816
+ result += `"${source.slice(i, end)}"`;
817
+ i = end - 1;
818
+ continue;
819
+ }
820
+ result += char;
821
+ }
822
+ return result;
823
+ }
824
+ function shouldQuoteObjectKey(source, index) {
825
+ let before = index - 1;
826
+ while (/\s/.test(source[before] || ""))
827
+ before -= 1;
828
+ if (source[before] !== "{" && source[before] !== ",")
829
+ return false;
830
+ let after = index + 1;
831
+ while (/[\w$]/.test(source[after] || ""))
832
+ after += 1;
833
+ while (/\s/.test(source[after] || ""))
834
+ after += 1;
835
+ return source[after] === ":";
836
+ }
755
837
  function isEmptyJsonObject(json) {
756
838
  try {
757
839
  const parsed = JSON.parse(json);
@@ -1,5 +1,7 @@
1
1
  export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
2
- export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "yashandb", "databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "oceanbase-oracle", "gbase", "tdengine", "h2", "snowflake", "trino", "hive", "db2", "informix", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
2
+ export type DirectQueryType = (typeof DIRECT_QUERY_TYPES)[number];
3
+ export declare function isDirectQueryType(dbType: string): dbType is DirectQueryType;
4
+ export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "yashandb", "databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "oceanbase-oracle", "gbase", "tdengine", "h2", "snowflake", "trino", "hive", "db2", "informix", "iris", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
3
5
  export interface DbxDiagnostics {
4
6
  appDataDir: string;
5
7
  dbPath: string;
@@ -1,7 +1,20 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { bridgePortFilePath, dbPath, appDataDir } from "./paths.js";
3
3
  import { inspectConnectionStore } from "./connections.js";
4
- export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
4
+ export const DIRECT_QUERY_TYPES = [
5
+ "postgres",
6
+ "redshift",
7
+ "mysql",
8
+ "doris",
9
+ "starrocks",
10
+ "sqlite",
11
+ "gaussdb",
12
+ "opengauss",
13
+ ];
14
+ const DIRECT_QUERY_TYPE_SET = new Set(DIRECT_QUERY_TYPES);
15
+ export function isDirectQueryType(dbType) {
16
+ return DIRECT_QUERY_TYPE_SET.has(dbType);
17
+ }
5
18
  export const BRIDGE_REQUIRED_TYPES = [
6
19
  "redis",
7
20
  "mongodb",
@@ -31,6 +44,7 @@ export const BRIDGE_REQUIRED_TYPES = [
31
44
  "hive",
32
45
  "db2",
33
46
  "informix",
47
+ "iris",
34
48
  "neo4j",
35
49
  "cassandra",
36
50
  "bigquery",
@@ -0,0 +1 @@
1
+ export declare function isMainModule(moduleUrl: string, argvPath: string | undefined): boolean;
@@ -0,0 +1,20 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { normalize, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function isMainModule(moduleUrl, argvPath) {
5
+ if (!argvPath)
6
+ return false;
7
+ return normalizeEntryPath(fileURLToPath(moduleUrl)) === normalizeEntryPath(argvPath);
8
+ }
9
+ function normalizeEntryPath(path) {
10
+ const normalized = normalize(realpathIfPossible(resolve(path)));
11
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
12
+ }
13
+ function realpathIfPossible(path) {
14
+ try {
15
+ return realpathSync.native(path);
16
+ }
17
+ catch {
18
+ return path;
19
+ }
20
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./bridge.js";
3
3
  export * from "./connections.js";
4
4
  export * from "./database.js";
5
5
  export * from "./diagnostics.js";
6
+ export * from "./entrypoint.js";
6
7
  export * from "./format.js";
7
8
  export * from "./paths.js";
8
9
  export * from "./schema-context.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./bridge.js";
3
3
  export * from "./connections.js";
4
4
  export * from "./database.js";
5
5
  export * from "./diagnostics.js";
6
+ export * from "./entrypoint.js";
6
7
  export * from "./format.js";
7
8
  export * from "./paths.js";
8
9
  export * from "./schema-context.js";
@@ -1,6 +1,7 @@
1
1
  export interface SqlSafetyOptions {
2
2
  allowWrites?: boolean;
3
3
  allowDangerous?: boolean;
4
+ allowMultipleStatements?: boolean;
4
5
  }
5
6
  export interface SqlSafetyDecision {
6
7
  allowed: boolean;
@@ -8,3 +9,4 @@ export interface SqlSafetyDecision {
8
9
  }
9
10
  export declare function evaluateSqlSafety(sql: string, options?: SqlSafetyOptions): SqlSafetyDecision;
10
11
  export declare function sqlSafetyFromEnv(env?: NodeJS.ProcessEnv): SqlSafetyOptions;
12
+ export declare function splitSqlStatements(sql: string): string[];
@@ -1,12 +1,37 @@
1
1
  const READ_KEYWORDS = new Set(["select", "with", "show", "describe", "desc", "explain"]);
2
2
  const DANGEROUS_KEYWORDS = new Set(["drop", "truncate", "alter"]);
3
+ function parseBooleanEnv(value) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ const normalized = value.trim().toLowerCase();
7
+ if (normalized === "1" || normalized === "true")
8
+ return true;
9
+ if (normalized === "0" || normalized === "false")
10
+ return false;
11
+ return undefined;
12
+ }
3
13
  export function evaluateSqlSafety(sql, options = {}) {
4
14
  const statements = splitSqlStatements(sql);
5
15
  if (statements.length === 0)
6
16
  return { allowed: false, reason: "SQL is empty." };
7
- if (statements.length > 1)
8
- return { allowed: false, reason: "Only one SQL statement is allowed per MCP query." };
9
- const normalized = stripSqlCommentsAndStrings(statements[0]).trim();
17
+ if (statements.length > 1 && !options.allowMultipleStatements) {
18
+ return { allowed: false, reason: "Only one SQL statement is allowed per query." };
19
+ }
20
+ for (let i = 0; i < statements.length; i++) {
21
+ const decision = evaluateSingleSqlStatementSafety(statements[i], options);
22
+ if (!decision.allowed && statements.length > 1) {
23
+ return {
24
+ allowed: false,
25
+ reason: `Statement ${i + 1}: ${decision.reason ?? "SQL blocked."}`,
26
+ };
27
+ }
28
+ if (!decision.allowed)
29
+ return decision;
30
+ }
31
+ return { allowed: true };
32
+ }
33
+ function evaluateSingleSqlStatementSafety(sql, options = {}) {
34
+ const normalized = stripSqlCommentsAndStrings(sql).trim();
10
35
  const firstKeyword = normalized.match(/^[a-zA-Z_]+/)?.[0]?.toLowerCase();
11
36
  if (!firstKeyword)
12
37
  return { allowed: false, reason: "SQL statement is not recognized." };
@@ -18,7 +43,7 @@ export function evaluateSqlSafety(sql, options = {}) {
18
43
  if (!options.allowWrites && !READ_KEYWORDS.has(firstKeyword)) {
19
44
  return {
20
45
  allowed: false,
21
- reason: "MCP SQL execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
46
+ reason: "MCP SQL execution is read-only for this session. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
22
47
  };
23
48
  }
24
49
  if (options.allowWrites && !options.allowDangerous) {
@@ -32,12 +57,14 @@ export function evaluateSqlSafety(sql, options = {}) {
32
57
  return { allowed: true };
33
58
  }
34
59
  export function sqlSafetyFromEnv(env = process.env) {
60
+ const allowWrites = parseBooleanEnv(env.DBX_MCP_ALLOW_WRITES);
61
+ const allowDangerous = parseBooleanEnv(env.DBX_MCP_ALLOW_DANGEROUS_SQL);
35
62
  return {
36
- allowWrites: env.DBX_MCP_ALLOW_WRITES === "1" || env.DBX_MCP_ALLOW_WRITES === "true",
37
- allowDangerous: env.DBX_MCP_ALLOW_DANGEROUS_SQL === "1" || env.DBX_MCP_ALLOW_DANGEROUS_SQL === "true",
63
+ allowWrites: allowWrites ?? true,
64
+ allowDangerous: allowDangerous ?? false,
38
65
  };
39
66
  }
40
- function splitSqlStatements(sql) {
67
+ export function splitSqlStatements(sql) {
41
68
  const statements = [];
42
69
  let current = "";
43
70
  let quote = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbx-app/node-core",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Shared Node.js database and DBX connection utilities for DBX CLI and MCP server",
5
5
  "type": "module",
6
6
  "engines": {
@@ -16,6 +16,7 @@
16
16
  "./connections": "./dist/connections.js",
17
17
  "./database": "./dist/database.js",
18
18
  "./diagnostics": "./dist/diagnostics.js",
19
+ "./entrypoint": "./dist/entrypoint.js",
19
20
  "./format": "./dist/format.js",
20
21
  "./paths": "./dist/paths.js",
21
22
  "./schema-context": "./dist/schema-context.js",