@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.
- package/dist/connections.d.ts +14 -0
- package/dist/connections.js +1 -0
- package/dist/database.js +99 -17
- package/dist/diagnostics.d.ts +3 -1
- package/dist/diagnostics.js +15 -1
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sql-safety.d.ts +2 -0
- package/dist/sql-safety.js +34 -7
- package/package.json +2 -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
|
@@ -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 (
|
|
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 (!
|
|
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 (!
|
|
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);
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
|
|
2
|
-
export
|
|
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;
|
package/dist/diagnostics.js
CHANGED
|
@@ -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 = [
|
|
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
package/dist/index.js
CHANGED
package/dist/sql-safety.d.ts
CHANGED
|
@@ -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[];
|
package/dist/sql-safety.js
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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
|
|
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:
|
|
37
|
-
allowDangerous:
|
|
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.
|
|
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",
|