@dbx-app/node-core 0.4.3

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.
@@ -0,0 +1,103 @@
1
+ const READ_KEYWORDS = new Set(["select", "with", "show", "describe", "desc", "explain"]);
2
+ const DANGEROUS_KEYWORDS = new Set(["drop", "truncate", "alter"]);
3
+ export function evaluateSqlSafety(sql, options = {}) {
4
+ const statements = splitSqlStatements(sql);
5
+ if (statements.length === 0)
6
+ 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();
10
+ const firstKeyword = normalized.match(/^[a-zA-Z_]+/)?.[0]?.toLowerCase();
11
+ if (!firstKeyword)
12
+ return { allowed: false, reason: "SQL statement is not recognized." };
13
+ const tokens = normalized.toLowerCase().match(/[a-z_]+/g) ?? [];
14
+ const dangerous = tokens.find((token) => DANGEROUS_KEYWORDS.has(token));
15
+ if (dangerous && !options.allowDangerous) {
16
+ return { allowed: false, reason: `Dangerous SQL keyword "${dangerous.toUpperCase()}" is blocked.` };
17
+ }
18
+ if (!options.allowWrites && !READ_KEYWORDS.has(firstKeyword)) {
19
+ return {
20
+ allowed: false,
21
+ reason: "MCP SQL execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
22
+ };
23
+ }
24
+ if (options.allowWrites && !options.allowDangerous) {
25
+ if (firstKeyword === "update" && !tokens.includes("where")) {
26
+ return { allowed: false, reason: "UPDATE statements must include a WHERE clause." };
27
+ }
28
+ if (firstKeyword === "delete" && !tokens.includes("where")) {
29
+ return { allowed: false, reason: "DELETE statements must include a WHERE clause." };
30
+ }
31
+ }
32
+ return { allowed: true };
33
+ }
34
+ export function sqlSafetyFromEnv(env = process.env) {
35
+ 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",
38
+ };
39
+ }
40
+ function splitSqlStatements(sql) {
41
+ const statements = [];
42
+ let current = "";
43
+ let quote = null;
44
+ let inLineComment = false;
45
+ let inBlockComment = false;
46
+ for (let i = 0; i < sql.length; i++) {
47
+ const char = sql[i];
48
+ const next = sql[i + 1];
49
+ if (inLineComment) {
50
+ current += char;
51
+ if (char === "\n")
52
+ inLineComment = false;
53
+ continue;
54
+ }
55
+ if (inBlockComment) {
56
+ current += char;
57
+ if (char === "*" && next === "/") {
58
+ current += next;
59
+ i++;
60
+ inBlockComment = false;
61
+ }
62
+ continue;
63
+ }
64
+ if (quote) {
65
+ current += char;
66
+ if (char === quote) {
67
+ if (next === quote) {
68
+ current += next;
69
+ i++;
70
+ }
71
+ else {
72
+ quote = null;
73
+ }
74
+ }
75
+ continue;
76
+ }
77
+ if (char === "-" && next === "-")
78
+ inLineComment = true;
79
+ if (char === "/" && next === "*")
80
+ inBlockComment = true;
81
+ if (char === "'" || char === '"' || char === "`")
82
+ quote = char;
83
+ if (char === ";") {
84
+ if (current.trim())
85
+ statements.push(current.trim());
86
+ current = "";
87
+ }
88
+ else {
89
+ current += char;
90
+ }
91
+ }
92
+ if (current.trim())
93
+ statements.push(current.trim());
94
+ return statements;
95
+ }
96
+ function stripSqlCommentsAndStrings(sql) {
97
+ return sql
98
+ .replace(/--.*$/gm, " ")
99
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
100
+ .replace(/'([^']|'')*'/g, "''")
101
+ .replace(/"([^"]|"")*"/g, '""')
102
+ .replace(/`([^`]|``)*`/g, "``");
103
+ }
@@ -0,0 +1,9 @@
1
+ import type { ConnectionConfig } from "./connections.js";
2
+ import type { TableInfo, ColumnInfo, QueryOptions, QueryResult } from "./database.js";
3
+ export declare function loadConnections(): Promise<ConnectionConfig[]>;
4
+ export declare function findConnection(name: string): Promise<ConnectionConfig | undefined>;
5
+ export declare function addConnection(config: Omit<ConnectionConfig, "id">): Promise<ConnectionConfig>;
6
+ export declare function removeConnection(name: string): Promise<boolean>;
7
+ export declare function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
8
+ export declare function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
9
+ export declare function executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
@@ -0,0 +1,115 @@
1
+ const baseUrl = process.env.DBX_WEB_URL.replace(/\/+$/, "");
2
+ const password = process.env.DBX_WEB_PASSWORD || "";
3
+ let sessionCookie = null;
4
+ async function ensureAuth() {
5
+ if (sessionCookie)
6
+ return;
7
+ if (!password)
8
+ return; // no password set, assume no auth required
9
+ const res = await fetch(`${baseUrl}/api/auth/login`, {
10
+ method: "POST",
11
+ headers: { "Content-Type": "application/json" },
12
+ body: JSON.stringify({ password }),
13
+ redirect: "manual",
14
+ });
15
+ if (!res.ok) {
16
+ throw new Error(`Authentication failed: ${res.status} ${res.statusText}`);
17
+ }
18
+ const setCookie = res.headers.get("set-cookie");
19
+ if (setCookie) {
20
+ const match = setCookie.match(/dbx_session=([^;]+)/);
21
+ if (match) {
22
+ sessionCookie = match[1];
23
+ }
24
+ }
25
+ }
26
+ function headers(extra) {
27
+ const h = { "Content-Type": "application/json", ...extra };
28
+ if (sessionCookie) {
29
+ h["Cookie"] = `dbx_session=${sessionCookie}`;
30
+ }
31
+ return h;
32
+ }
33
+ async function apiFetch(path, init) {
34
+ await ensureAuth();
35
+ const res = await fetch(`${baseUrl}${path}`, {
36
+ ...init,
37
+ headers: headers(init?.headers),
38
+ });
39
+ if (!res.ok) {
40
+ const body = await res.text().catch(() => "");
41
+ throw new Error(`API request ${path} failed: ${res.status} ${res.statusText} ${body}`);
42
+ }
43
+ return res;
44
+ }
45
+ export async function loadConnections() {
46
+ const res = await apiFetch("/api/connection/list");
47
+ return res.json();
48
+ }
49
+ export async function findConnection(name) {
50
+ const connections = await loadConnections();
51
+ return connections.find((c) => c.name.toLowerCase() === name.toLowerCase());
52
+ }
53
+ export async function addConnection(config) {
54
+ const res = await apiFetch("/api/connection/save", {
55
+ method: "POST",
56
+ body: JSON.stringify({ configs: [config] }),
57
+ });
58
+ const saved = (await res.json());
59
+ return saved;
60
+ }
61
+ export async function removeConnection(name) {
62
+ const connection = await findConnection(name);
63
+ if (!connection)
64
+ return false;
65
+ await apiFetch(`/api/connection/delete?id=${encodeURIComponent(connection.id)}`, { method: "DELETE" });
66
+ return true;
67
+ }
68
+ async function ensureConnected(config) {
69
+ await apiFetch("/api/connection/connect", {
70
+ method: "POST",
71
+ body: JSON.stringify({ config }),
72
+ });
73
+ }
74
+ export async function listTables(config, schema) {
75
+ await ensureConnected(config);
76
+ const params = new URLSearchParams({
77
+ connection_id: config.id,
78
+ database: config.database || "",
79
+ schema: schema || "",
80
+ });
81
+ const res = await apiFetch(`/api/schema/tables?${params}`);
82
+ return res.json();
83
+ }
84
+ export async function describeTable(config, table, schema) {
85
+ await ensureConnected(config);
86
+ const params = new URLSearchParams({
87
+ connection_id: config.id,
88
+ database: config.database || "",
89
+ schema: schema || "",
90
+ table,
91
+ });
92
+ const res = await apiFetch(`/api/schema/columns?${params}`);
93
+ return res.json();
94
+ }
95
+ export async function executeQuery(config, sql, options) {
96
+ await ensureConnected(config);
97
+ const res = await apiFetch("/api/query/execute", {
98
+ method: "POST",
99
+ body: JSON.stringify({
100
+ connectionId: config.id,
101
+ database: config.database || "",
102
+ sql,
103
+ }),
104
+ });
105
+ const data = (await res.json());
106
+ const rows = data.rows.map((row) => {
107
+ const obj = {};
108
+ data.columns.forEach((col, i) => {
109
+ obj[col] = row[i];
110
+ });
111
+ return obj;
112
+ });
113
+ const limitedRows = rows.slice(0, options?.maxRows ?? rows.length);
114
+ return { columns: data.columns, rows: limitedRows, row_count: limitedRows.length };
115
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@dbx-app/node-core",
3
+ "version": "0.4.3",
4
+ "description": "Shared Node.js database and DBX connection utilities for DBX CLI and MCP server",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.13.0"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./backend": "./dist/backend.js",
15
+ "./bridge": "./dist/bridge.js",
16
+ "./connections": "./dist/connections.js",
17
+ "./database": "./dist/database.js",
18
+ "./diagnostics": "./dist/diagnostics.js",
19
+ "./paths": "./dist/paths.js",
20
+ "./schema-context": "./dist/schema-context.js",
21
+ "./sql-safety": "./dist/sql-safety.js"
22
+ },
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "better-sqlite3": "^12.9.0",
26
+ "keytar": "^7.9.0",
27
+ "mysql2": "^3.14.1",
28
+ "pg": "^8.16.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/better-sqlite3": "^7.6.13",
32
+ "@types/node": "^22.15.21",
33
+ "@types/pg": "^8.15.4",
34
+ "tsx": "^4.19.4",
35
+ "typescript": "^5.8.3"
36
+ },
37
+ "scripts": {
38
+ "test": "tsx --test tests/*.test.ts",
39
+ "build": "tsc"
40
+ }
41
+ }