@dbx-app/node-core 0.4.5 → 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.
- package/dist/connections.d.ts +14 -0
- package/dist/connections.js +1 -0
- package/dist/database.js +95 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +11 -1
- package/dist/sql-safety.js +15 -3
- package/package.json +1 -1
package/dist/connections.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/connections.js
CHANGED
|
@@ -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
|
@@ -731,7 +731,7 @@ function readChainedIntegerArgument(chain, method, fallback) {
|
|
|
731
731
|
return Number(arg.trim());
|
|
732
732
|
}
|
|
733
733
|
function normalizeJsonArgument(arg) {
|
|
734
|
-
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"}')));
|
|
735
735
|
try {
|
|
736
736
|
JSON.parse(value);
|
|
737
737
|
return value;
|
|
@@ -740,6 +740,100 @@ function normalizeJsonArgument(arg) {
|
|
|
740
740
|
return null;
|
|
741
741
|
}
|
|
742
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
|
+
}
|
|
743
837
|
function isEmptyJsonObject(json) {
|
|
744
838
|
try {
|
|
745
839
|
const parsed = JSON.parse(json);
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
|
|
2
2
|
export type DirectQueryType = (typeof DIRECT_QUERY_TYPES)[number];
|
|
3
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", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
|
|
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"];
|
|
5
5
|
export interface DbxDiagnostics {
|
|
6
6
|
appDataDir: string;
|
|
7
7
|
dbPath: string;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
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 = [
|
|
4
|
+
export const DIRECT_QUERY_TYPES = [
|
|
5
|
+
"postgres",
|
|
6
|
+
"redshift",
|
|
7
|
+
"mysql",
|
|
8
|
+
"doris",
|
|
9
|
+
"starrocks",
|
|
10
|
+
"sqlite",
|
|
11
|
+
"gaussdb",
|
|
12
|
+
"opengauss",
|
|
13
|
+
];
|
|
5
14
|
const DIRECT_QUERY_TYPE_SET = new Set(DIRECT_QUERY_TYPES);
|
|
6
15
|
export function isDirectQueryType(dbType) {
|
|
7
16
|
return DIRECT_QUERY_TYPE_SET.has(dbType);
|
|
@@ -35,6 +44,7 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
35
44
|
"hive",
|
|
36
45
|
"db2",
|
|
37
46
|
"informix",
|
|
47
|
+
"iris",
|
|
38
48
|
"neo4j",
|
|
39
49
|
"cassandra",
|
|
40
50
|
"bigquery",
|
package/dist/sql-safety.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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)
|
|
@@ -33,7 +43,7 @@ function evaluateSingleSqlStatementSafety(sql, options = {}) {
|
|
|
33
43
|
if (!options.allowWrites && !READ_KEYWORDS.has(firstKeyword)) {
|
|
34
44
|
return {
|
|
35
45
|
allowed: false,
|
|
36
|
-
reason: "MCP SQL execution is read-only
|
|
46
|
+
reason: "MCP SQL execution is read-only for this session. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
if (options.allowWrites && !options.allowDangerous) {
|
|
@@ -47,9 +57,11 @@ function evaluateSingleSqlStatementSafety(sql, options = {}) {
|
|
|
47
57
|
return { allowed: true };
|
|
48
58
|
}
|
|
49
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);
|
|
50
62
|
return {
|
|
51
|
-
allowWrites:
|
|
52
|
-
allowDangerous:
|
|
63
|
+
allowWrites: allowWrites ?? true,
|
|
64
|
+
allowDangerous: allowDangerous ?? false,
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
export function splitSqlStatements(sql) {
|