@bytebase/dbhub 0.22.3 → 0.22.4
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.
|
@@ -1094,6 +1094,92 @@ function validateToolsConfig(tools, sources, configPath) {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
|
+
function getRawDSNQueryParam(dsn, key) {
|
|
1098
|
+
const queryStart = dsn.indexOf("?");
|
|
1099
|
+
if (queryStart === -1) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
for (const pair of dsn.substring(queryStart + 1).split("&")) {
|
|
1103
|
+
if (pair === "") {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const eq = pair.indexOf("=");
|
|
1107
|
+
const rawKey = eq === -1 ? pair : pair.substring(0, eq);
|
|
1108
|
+
if (rawKey === key) {
|
|
1109
|
+
const rawValue = eq === -1 ? "" : pair.substring(eq + 1);
|
|
1110
|
+
try {
|
|
1111
|
+
return decodeURIComponent(rawValue);
|
|
1112
|
+
} catch {
|
|
1113
|
+
return rawValue;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
function validateDSNFieldConflicts(source, configPath) {
|
|
1120
|
+
const conflict = (field, fieldValue, dsnValue) => {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`Configuration file ${configPath}: source '${source.id}' has conflicting ${field}: the DSN specifies '${dsnValue}' but the ${field} field is '${fieldValue}'. Set ${field} in only one place, or make the two values match.`
|
|
1123
|
+
);
|
|
1124
|
+
};
|
|
1125
|
+
const info = parseConnectionInfoFromDSN(source.dsn);
|
|
1126
|
+
if (source.type && info?.type && source.type !== info.type) {
|
|
1127
|
+
conflict("type", source.type, info.type);
|
|
1128
|
+
}
|
|
1129
|
+
if (info?.type === "sqlite") {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
let url;
|
|
1133
|
+
try {
|
|
1134
|
+
url = new SafeURL(source.dsn);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (info) {
|
|
1139
|
+
if (source.host && info.host && source.host.toLowerCase() !== info.host.toLowerCase()) {
|
|
1140
|
+
conflict("host", source.host, info.host);
|
|
1141
|
+
}
|
|
1142
|
+
if (source.port !== void 0 && info.port !== void 0 && source.port !== info.port) {
|
|
1143
|
+
conflict("port", String(source.port), String(info.port));
|
|
1144
|
+
}
|
|
1145
|
+
if (source.database && info.database && source.database !== info.database) {
|
|
1146
|
+
conflict("database", source.database, info.database);
|
|
1147
|
+
}
|
|
1148
|
+
if (source.user && info.user && source.user !== info.user) {
|
|
1149
|
+
conflict("user", source.user, info.user);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (source.password && source.password !== url.password) {
|
|
1153
|
+
if (!url.password) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`Configuration file ${configPath}: source '${source.id}' has a 'password' field but the DSN has no password. The field is ignored at connection time \u2014 add the password to the DSN, or use individual connection parameters instead of a DSN.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
`Configuration file ${configPath}: source '${source.id}' has a 'password' field that conflicts with the password in the DSN. Set the password in only one place.`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
const dsnSslmode = getRawDSNQueryParam(source.dsn, "sslmode");
|
|
1163
|
+
if (source.sslmode && dsnSslmode !== null && dsnSslmode !== source.sslmode) {
|
|
1164
|
+
conflict("sslmode", source.sslmode, dsnSslmode);
|
|
1165
|
+
}
|
|
1166
|
+
const dsnSslrootcert = getRawDSNQueryParam(source.dsn, "sslrootcert");
|
|
1167
|
+
if (source.sslrootcert && dsnSslrootcert !== null && expandHomeDir(source.sslrootcert) !== expandHomeDir(dsnSslrootcert)) {
|
|
1168
|
+
conflict("sslrootcert", expandHomeDir(source.sslrootcert), expandHomeDir(dsnSslrootcert));
|
|
1169
|
+
}
|
|
1170
|
+
const dsnInstanceName = getRawDSNQueryParam(source.dsn, "instanceName");
|
|
1171
|
+
if (source.instanceName && dsnInstanceName !== null && dsnInstanceName !== source.instanceName) {
|
|
1172
|
+
conflict("instanceName", source.instanceName, dsnInstanceName);
|
|
1173
|
+
}
|
|
1174
|
+
const dsnAuthentication = getRawDSNQueryParam(source.dsn, "authentication");
|
|
1175
|
+
if (source.authentication && dsnAuthentication !== null && dsnAuthentication !== source.authentication) {
|
|
1176
|
+
conflict("authentication", source.authentication, dsnAuthentication);
|
|
1177
|
+
}
|
|
1178
|
+
const dsnDomain = getRawDSNQueryParam(source.dsn, "domain");
|
|
1179
|
+
if (source.domain && dsnDomain !== null && dsnDomain !== source.domain) {
|
|
1180
|
+
conflict("domain", source.domain, dsnDomain);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1097
1183
|
function validateSourceConfig(source, configPath) {
|
|
1098
1184
|
const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
|
|
1099
1185
|
if (!source.dsn && !hasConnectionParams) {
|
|
@@ -1176,6 +1262,9 @@ function validateSourceConfig(source, configPath) {
|
|
|
1176
1262
|
);
|
|
1177
1263
|
}
|
|
1178
1264
|
}
|
|
1265
|
+
if (source.dsn) {
|
|
1266
|
+
validateDSNFieldConflicts(source, configPath);
|
|
1267
|
+
}
|
|
1179
1268
|
if (source.sslrootcert !== void 0) {
|
|
1180
1269
|
if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
|
|
1181
1270
|
throw new Error(
|
|
@@ -1350,9 +1439,51 @@ function expandHomeDir(filePath) {
|
|
|
1350
1439
|
}
|
|
1351
1440
|
return filePath;
|
|
1352
1441
|
}
|
|
1442
|
+
function mergeSourceFieldsIntoDSN(dsn, source) {
|
|
1443
|
+
if (source.type === "sqlite") {
|
|
1444
|
+
return dsn;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
new SafeURL(dsn);
|
|
1448
|
+
} catch {
|
|
1449
|
+
return dsn;
|
|
1450
|
+
}
|
|
1451
|
+
const hasParam = (key) => getRawDSNQueryParam(dsn, key) !== null;
|
|
1452
|
+
const additions = [];
|
|
1453
|
+
if (source.type === "sqlserver") {
|
|
1454
|
+
if (source.instanceName && !hasParam("instanceName")) {
|
|
1455
|
+
additions.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
|
|
1456
|
+
}
|
|
1457
|
+
if (source.authentication && !hasParam("authentication")) {
|
|
1458
|
+
additions.push(`authentication=${encodeURIComponent(source.authentication)}`);
|
|
1459
|
+
}
|
|
1460
|
+
if (source.domain && !hasParam("domain")) {
|
|
1461
|
+
additions.push(`domain=${encodeURIComponent(source.domain)}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (source.sslmode && !hasParam("sslmode")) {
|
|
1465
|
+
additions.push(`sslmode=${source.sslmode}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !hasParam("sslrootcert")) {
|
|
1468
|
+
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1469
|
+
additions.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (additions.length === 0) {
|
|
1472
|
+
return dsn;
|
|
1473
|
+
}
|
|
1474
|
+
let separator;
|
|
1475
|
+
if (!dsn.includes("?")) {
|
|
1476
|
+
separator = "?";
|
|
1477
|
+
} else if (dsn.endsWith("?") || dsn.endsWith("&")) {
|
|
1478
|
+
separator = "";
|
|
1479
|
+
} else {
|
|
1480
|
+
separator = "&";
|
|
1481
|
+
}
|
|
1482
|
+
return `${dsn}${separator}${additions.join("&")}`;
|
|
1483
|
+
}
|
|
1353
1484
|
function buildDSNFromSource(source) {
|
|
1354
1485
|
if (source.dsn) {
|
|
1355
|
-
return source.dsn;
|
|
1486
|
+
return mergeSourceFieldsIntoDSN(source.dsn, source);
|
|
1356
1487
|
}
|
|
1357
1488
|
if (!source.type) {
|
|
1358
1489
|
throw new Error(
|
|
@@ -1434,6 +1565,53 @@ async function generateRdsAuthToken(params) {
|
|
|
1434
1565
|
return signer.getAuthToken();
|
|
1435
1566
|
}
|
|
1436
1567
|
|
|
1568
|
+
// src/utils/error-classifier.ts
|
|
1569
|
+
var TUNNEL_ERROR_MARKER = "__dbhubSSHTunnelError";
|
|
1570
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
1571
|
+
"ECONNREFUSED",
|
|
1572
|
+
"ETIMEDOUT",
|
|
1573
|
+
"ENOTFOUND",
|
|
1574
|
+
"EHOSTUNREACH",
|
|
1575
|
+
"ENETUNREACH",
|
|
1576
|
+
"ECONNRESET"
|
|
1577
|
+
]);
|
|
1578
|
+
var AUTH_CODES = {
|
|
1579
|
+
postgres: ["28P01", "28000"],
|
|
1580
|
+
mysql: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1581
|
+
mariadb: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1582
|
+
sqlserver: ["ELOGIN"],
|
|
1583
|
+
sqlite: []
|
|
1584
|
+
// no network/auth layer
|
|
1585
|
+
};
|
|
1586
|
+
function unreachableMessage(sourceId) {
|
|
1587
|
+
return `Source '${sourceId}' is unreachable. Verify the database is running and reachable (host, port, network), then retry.`;
|
|
1588
|
+
}
|
|
1589
|
+
function authMessage(sourceId) {
|
|
1590
|
+
return `Authentication failed for source '${sourceId}'. Verify the credentials/access for this source are valid, then retry.`;
|
|
1591
|
+
}
|
|
1592
|
+
function tunnelMessage(sourceId) {
|
|
1593
|
+
return `SSH tunnel for source '${sourceId}' failed to establish. Verify SSH host/credentials and bastion reachability, then retry.`;
|
|
1594
|
+
}
|
|
1595
|
+
function classifyConnectionError(error, connectorType, sourceId) {
|
|
1596
|
+
if (!error || typeof error !== "object") {
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
const err = error;
|
|
1600
|
+
if (err[TUNNEL_ERROR_MARKER] === true) {
|
|
1601
|
+
return { code: "TUNNEL_FAILED", message: tunnelMessage(sourceId) };
|
|
1602
|
+
}
|
|
1603
|
+
const code = err.code;
|
|
1604
|
+
if (typeof code === "string" && NETWORK_CODES.has(code)) {
|
|
1605
|
+
return { code: "SOURCE_UNREACHABLE", message: unreachableMessage(sourceId) };
|
|
1606
|
+
}
|
|
1607
|
+
const authCodes = AUTH_CODES[connectorType];
|
|
1608
|
+
const errno = err.errno;
|
|
1609
|
+
if (typeof code === "string" && authCodes.includes(code) || typeof errno === "number" && authCodes.includes(errno)) {
|
|
1610
|
+
return { code: "AUTH_FAILED", message: authMessage(sourceId) };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1437
1615
|
// src/connectors/manager.ts
|
|
1438
1616
|
var managerInstance = null;
|
|
1439
1617
|
var AWS_IAM_TOKEN_REFRESH_MS = 14 * 60 * 1e3;
|
|
@@ -1574,10 +1752,18 @@ var ConnectorManager = class {
|
|
|
1574
1752
|
const targetHost = url.hostname;
|
|
1575
1753
|
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
1576
1754
|
const tunnel = new SSHTunnel();
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1755
|
+
let tunnelInfo;
|
|
1756
|
+
try {
|
|
1757
|
+
tunnelInfo = await tunnel.establish(sshConfig, {
|
|
1758
|
+
targetHost,
|
|
1759
|
+
targetPort
|
|
1760
|
+
});
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
if (error && typeof error === "object") {
|
|
1763
|
+
error[TUNNEL_ERROR_MARKER] = true;
|
|
1764
|
+
}
|
|
1765
|
+
throw error;
|
|
1766
|
+
}
|
|
1581
1767
|
url.hostname = "127.0.0.1";
|
|
1582
1768
|
url.port = tunnelInfo.localPort.toString();
|
|
1583
1769
|
actualDSN = url.toString();
|
|
@@ -2190,6 +2376,7 @@ export {
|
|
|
2190
2376
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
2191
2377
|
loadTomlConfig,
|
|
2192
2378
|
resolveTomlConfigPath,
|
|
2379
|
+
classifyConnectionError,
|
|
2193
2380
|
ConnectorManager,
|
|
2194
2381
|
mapArgumentsToArray,
|
|
2195
2382
|
ToolRegistry,
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
4
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
5
|
ConnectorManager,
|
|
6
|
+
classifyConnectionError,
|
|
6
7
|
getToolRegistry,
|
|
7
8
|
initializeToolRegistry,
|
|
8
9
|
isDemoMode,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
resolveSourceConfigs,
|
|
14
15
|
resolveTomlConfigPath,
|
|
15
16
|
resolveTransport
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-DN45LGY4.js";
|
|
17
18
|
import {
|
|
18
19
|
loadConnectors
|
|
19
20
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -248,6 +249,20 @@ function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
|
248
249
|
error
|
|
249
250
|
});
|
|
250
251
|
}
|
|
252
|
+
function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
|
|
253
|
+
let connectorType;
|
|
254
|
+
try {
|
|
255
|
+
connectorType = ConnectorManager.getSourceConfig(rawSourceId)?.type;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (!connectorType) return null;
|
|
260
|
+
const classified = classifyConnectionError(error, connectorType, displaySourceId);
|
|
261
|
+
if (!classified) return null;
|
|
262
|
+
return createToolErrorResponse(classified.message, classified.code, {
|
|
263
|
+
source_id: displaySourceId
|
|
264
|
+
});
|
|
265
|
+
}
|
|
251
266
|
|
|
252
267
|
// src/tools/execute-sql.ts
|
|
253
268
|
var executeSqlSchema = {
|
|
@@ -292,6 +307,8 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
292
307
|
} catch (error) {
|
|
293
308
|
success = false;
|
|
294
309
|
errorMessage = error.message;
|
|
310
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
311
|
+
if (classified) return classified;
|
|
295
312
|
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
296
313
|
} finally {
|
|
297
314
|
trackToolRequest(
|
|
@@ -313,11 +330,11 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
313
330
|
import { z as z2 } from "zod";
|
|
314
331
|
var searchDatabaseObjectsSchema = {
|
|
315
332
|
object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
316
|
-
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)
|
|
333
|
+
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)"),
|
|
317
334
|
schema: z2.string().optional().describe("Filter to schema"),
|
|
318
335
|
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
319
336
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
320
|
-
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results
|
|
337
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
|
|
321
338
|
};
|
|
322
339
|
function likePatternToRegex(pattern) {
|
|
323
340
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
@@ -771,6 +788,8 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
771
788
|
} catch (error) {
|
|
772
789
|
success = false;
|
|
773
790
|
errorMessage = error.message;
|
|
791
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
792
|
+
if (classified) return classified;
|
|
774
793
|
return createToolErrorResponse(
|
|
775
794
|
`Error searching database objects: ${errorMessage}`,
|
|
776
795
|
"SEARCH_ERROR"
|
|
@@ -875,7 +894,7 @@ function getSearchObjectsMetadata(sourceId) {
|
|
|
875
894
|
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
876
895
|
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
877
896
|
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
878
|
-
const description = isSingleSource ? `${userDescPrefix}Search and list database objects
|
|
897
|
+
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
879
898
|
return {
|
|
880
899
|
name: toolName,
|
|
881
900
|
description,
|
|
@@ -916,7 +935,7 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
916
935
|
name: "object_type",
|
|
917
936
|
type: "string",
|
|
918
937
|
required: true,
|
|
919
|
-
description: "Object type to search"
|
|
938
|
+
description: "Object type to search: schema, table, view, column, procedure, function, index"
|
|
920
939
|
},
|
|
921
940
|
{
|
|
922
941
|
name: "pattern",
|
|
@@ -1064,6 +1083,8 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1064
1083
|
} catch (error) {
|
|
1065
1084
|
success = false;
|
|
1066
1085
|
errorMessage = error.message;
|
|
1086
|
+
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1087
|
+
if (classified) return classified;
|
|
1067
1088
|
if (error instanceof z4.ZodError) {
|
|
1068
1089
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1069
1090
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
@@ -1557,7 +1578,7 @@ See documentation for more details on configuring database connections.
|
|
|
1557
1578
|
const sources = sourceConfigsData.sources;
|
|
1558
1579
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1559
1580
|
await connectorManager.connectWithSources(sources);
|
|
1560
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1581
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-OF5GPCBK.js");
|
|
1561
1582
|
initializeToolRegistry2({
|
|
1562
1583
|
sources: sourceConfigsData.sources,
|
|
1563
1584
|
tools: sourceConfigsData.tools
|
package/package.json
CHANGED