@bytebase/dbhub 1.3.1 → 1.4.0

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.
@@ -1206,6 +1206,30 @@ function getRawDSNQueryParam(dsn, key) {
1206
1206
  }
1207
1207
  return null;
1208
1208
  }
1209
+ var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
1210
+ function validateReadableFile(source, field, filePath, configPath) {
1211
+ const expandedPath = expandHomeDir(filePath);
1212
+ let stats;
1213
+ try {
1214
+ stats = fs2.statSync(expandedPath);
1215
+ } catch {
1216
+ throw new Error(
1217
+ `Configuration file ${configPath}: source '${source.id}' ${field} file not found or not accessible: '${expandedPath}'`
1218
+ );
1219
+ }
1220
+ if (!stats.isFile()) {
1221
+ throw new Error(
1222
+ `Configuration file ${configPath}: source '${source.id}' ${field} path is not a regular file: '${expandedPath}'`
1223
+ );
1224
+ }
1225
+ try {
1226
+ fs2.accessSync(expandedPath, fs2.constants.R_OK);
1227
+ } catch {
1228
+ throw new Error(
1229
+ `Configuration file ${configPath}: source '${source.id}' ${field} file is not readable: '${expandedPath}'`
1230
+ );
1231
+ }
1232
+ }
1209
1233
  function validateDSNFieldConflicts(source, configPath) {
1210
1234
  const conflict = (field, fieldValue, dsnValue) => {
1211
1235
  throw new Error(
@@ -1262,6 +1286,13 @@ function validateDSNFieldConflicts(source, configPath) {
1262
1286
  if (source.sslrootcert && dsnSslrootcert !== null && expandHomeDir(source.sslrootcert) !== expandHomeDir(dsnSslrootcert)) {
1263
1287
  conflict("sslrootcert", expandHomeDir(source.sslrootcert), expandHomeDir(dsnSslrootcert));
1264
1288
  }
1289
+ for (const field of ["sslcert", "sslkey"]) {
1290
+ const fieldValue = source[field];
1291
+ const dsnValue = getRawDSNQueryParam(source.dsn, field);
1292
+ if (fieldValue && dsnValue !== null && expandHomeDir(fieldValue) !== expandHomeDir(dsnValue)) {
1293
+ conflict(field, expandHomeDir(fieldValue), expandHomeDir(dsnValue));
1294
+ }
1295
+ }
1265
1296
  const dsnInstanceName = getRawDSNQueryParam(source.dsn, "instanceName");
1266
1297
  if (source.instanceName && dsnInstanceName !== null && dsnInstanceName !== source.instanceName) {
1267
1298
  conflict("instanceName", source.instanceName, dsnInstanceName);
@@ -1400,27 +1431,26 @@ function validateSourceConfig(source, configPath) {
1400
1431
  `Configuration file ${configPath}: source '${source.id}' has sslrootcert but sslmode is '${source.sslmode ?? "not set"}'. sslrootcert requires sslmode 'verify-ca' or 'verify-full'`
1401
1432
  );
1402
1433
  }
1403
- const expandedPath = expandHomeDir(source.sslrootcert);
1404
- let stats;
1405
- try {
1406
- stats = fs2.statSync(expandedPath);
1407
- } catch {
1434
+ validateReadableFile(source, "sslrootcert", source.sslrootcert, configPath);
1435
+ }
1436
+ if (source.sslcert !== void 0 || source.sslkey !== void 0) {
1437
+ if (source.type !== "postgres") {
1408
1438
  throw new Error(
1409
- `Configuration file ${configPath}: source '${source.id}' sslrootcert file not found or not accessible: '${expandedPath}'`
1439
+ `Configuration file ${configPath}: source '${source.id}' has sslcert/sslkey but they are only supported for PostgreSQL.`
1410
1440
  );
1411
1441
  }
1412
- if (!stats.isFile()) {
1442
+ if (source.sslcert === void 0 || source.sslkey === void 0) {
1413
1443
  throw new Error(
1414
- `Configuration file ${configPath}: source '${source.id}' sslrootcert path is not a regular file: '${expandedPath}'`
1444
+ `Configuration file ${configPath}: source '${source.id}' has ${source.sslcert !== void 0 ? "sslcert" : "sslkey"} without ${source.sslcert !== void 0 ? "sslkey" : "sslcert"}. sslcert and sslkey must be set together for client certificate authentication`
1415
1445
  );
1416
1446
  }
1417
- try {
1418
- fs2.accessSync(expandedPath, fs2.constants.R_OK);
1419
- } catch {
1447
+ if (!CLIENT_CERT_SSL_MODES.includes(source.sslmode ?? "")) {
1420
1448
  throw new Error(
1421
- `Configuration file ${configPath}: source '${source.id}' sslrootcert file is not readable: '${expandedPath}'`
1449
+ `Configuration file ${configPath}: source '${source.id}' has sslcert/sslkey but sslmode is '${source.sslmode ?? "not set"}'. sslcert/sslkey require sslmode 'require', 'verify-ca' or 'verify-full'`
1422
1450
  );
1423
1451
  }
1452
+ validateReadableFile(source, "sslcert", source.sslcert, configPath);
1453
+ validateReadableFile(source, "sslkey", source.sslkey, configPath);
1424
1454
  }
1425
1455
  if (source.authentication !== void 0) {
1426
1456
  if (source.type !== "sqlserver") {
@@ -1536,8 +1566,10 @@ function processSourceConfigs(sources, configPath) {
1536
1566
  if (processed.ssh_key) {
1537
1567
  processed.ssh_key = expandHomeDir(processed.ssh_key);
1538
1568
  }
1539
- if (processed.sslrootcert) {
1540
- processed.sslrootcert = expandHomeDir(processed.sslrootcert);
1569
+ for (const field of ["sslrootcert", "sslcert", "sslkey"]) {
1570
+ if (processed[field]) {
1571
+ processed[field] = expandHomeDir(processed[field]);
1572
+ }
1541
1573
  }
1542
1574
  if (processed.type === "sqlite" && processed.database) {
1543
1575
  processed.database = resolveSqliteDatabasePath(processed.database, configPath);
@@ -1570,9 +1602,11 @@ function processSourceConfigs(sources, configPath) {
1570
1602
  if (!processed.sslmode && dsnSslmode) {
1571
1603
  processed.sslmode = dsnSslmode;
1572
1604
  }
1573
- const dsnSslrootcert = url.getSearchParam("sslrootcert");
1574
- if (!processed.sslrootcert && dsnSslrootcert) {
1575
- processed.sslrootcert = dsnSslrootcert;
1605
+ for (const field of ["sslrootcert", "sslcert", "sslkey"]) {
1606
+ const dsnValue = url.getSearchParam(field);
1607
+ if (!processed[field] && dsnValue) {
1608
+ processed[field] = dsnValue;
1609
+ }
1576
1610
  }
1577
1611
  } catch {
1578
1612
  }
@@ -1631,10 +1665,7 @@ function mergeSourceFieldsIntoDSN(dsn, source) {
1631
1665
  if (source.sslmode && !hasParam("sslmode")) {
1632
1666
  additions.push(`sslmode=${source.sslmode}`);
1633
1667
  }
1634
- if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !hasParam("sslrootcert")) {
1635
- const expandedCertPath = expandHomeDir(source.sslrootcert);
1636
- additions.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
1637
- }
1668
+ additions.push(...postgresSslFileParams(source).filter(({ key }) => !hasParam(key)).map(({ param }) => param));
1638
1669
  if (additions.length === 0) {
1639
1670
  return dsn;
1640
1671
  }
@@ -1648,6 +1679,23 @@ function mergeSourceFieldsIntoDSN(dsn, source) {
1648
1679
  }
1649
1680
  return `${dsn}${separator}${additions.join("&")}`;
1650
1681
  }
1682
+ function postgresSslFileParams(source) {
1683
+ if (source.type !== "postgres") {
1684
+ return [];
1685
+ }
1686
+ const params = [];
1687
+ const push = (key) => {
1688
+ params.push({ key, param: `${key}=${encodeURIComponent(expandHomeDir(source[key]))}` });
1689
+ };
1690
+ if (source.sslrootcert && (source.sslmode === "verify-ca" || source.sslmode === "verify-full")) {
1691
+ push("sslrootcert");
1692
+ }
1693
+ if (source.sslcert && source.sslkey && CLIENT_CERT_SSL_MODES.includes(source.sslmode ?? "")) {
1694
+ push("sslcert");
1695
+ push("sslkey");
1696
+ }
1697
+ return params;
1698
+ }
1651
1699
  function buildDSNFromSource(source) {
1652
1700
  if (source.dsn) {
1653
1701
  return mergeSourceFieldsIntoDSN(source.dsn, source);
@@ -1700,10 +1748,7 @@ function buildDSNFromSource(source) {
1700
1748
  if (source.sslmode && source.type !== "sqlite") {
1701
1749
  queryParams.push(`sslmode=${source.sslmode}`);
1702
1750
  }
1703
- if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full")) {
1704
- const expandedCertPath = expandHomeDir(source.sslrootcert);
1705
- queryParams.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
1706
- }
1751
+ queryParams.push(...postgresSslFileParams(source).map(({ param }) => param));
1707
1752
  if (queryParams.length > 0) {
1708
1753
  dsn += `?${queryParams.join("&")}`;
1709
1754
  }
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  resolveSourceConfigs,
19
19
  resolveTomlConfigPath,
20
20
  resolveTransport
21
- } from "./chunk-5FL4XMWG.js";
21
+ } from "./chunk-MJVIE3QY.js";
22
22
  import {
23
23
  loadConnectors
24
24
  } from "./chunk-WVVMH6FJ.js";
@@ -1913,7 +1913,7 @@ See documentation for more details on configuring database connections.
1913
1913
  const sources = sourceConfigsData.sources;
1914
1914
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1915
1915
  await connectorManager.connectWithSources(sources);
1916
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZTHDPLKP.js");
1916
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-C7X5G7PA.js");
1917
1917
  initializeToolRegistry2({
1918
1918
  sources: sourceConfigsData.sources,
1919
1919
  tools: sourceConfigsData.tools
@@ -2068,7 +2068,7 @@ See documentation for more details on configuring database connections.
2068
2068
 
2069
2069
  // src/index.ts
2070
2070
  var connectorModules = [
2071
- { load: () => import("./postgres-6XJ7V4J4.js"), name: "PostgreSQL", driver: "pg" },
2071
+ { load: () => import("./postgres-DC5BDEMU.js"), name: "PostgreSQL", driver: "pg" },
2072
2072
  { load: () => import("./sqlserver-AJUK6L55.js"), name: "SQL Server", driver: "mssql" },
2073
2073
  { load: () => import("./sqlite-AO2BKIC6.js"), name: "SQLite", driver: "node:sqlite" },
2074
2074
  { load: () => import("./mysql-XXIHWQGR.js"), name: "MySQL", driver: "mysql2" },
@@ -59,6 +59,21 @@ var postgresTypeParsers = {
59
59
  // src/connectors/postgres/index.ts
60
60
  var { Pool } = pg2;
61
61
  var POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS = 5e3;
62
+ var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
63
+ var SUPPORTED_SSL_MODES = ["disable", ...CLIENT_CERT_SSL_MODES];
64
+ async function readPemFile(filePath, label) {
65
+ const resolved = filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
66
+ try {
67
+ return await fs.promises.readFile(resolved, "utf-8");
68
+ } catch (err) {
69
+ throw new FailedToReadCertificate(
70
+ `Failed to read ${label} at '${resolved}': ${err instanceof Error ? err.message : String(err)}`
71
+ );
72
+ }
73
+ }
74
+ function isEncryptedPemKey(pem) {
75
+ return pem.includes("ENCRYPTED PRIVATE KEY") || pem.includes("Proc-Type: 4,ENCRYPTED");
76
+ }
62
77
  var PostgresDSNParser = class {
63
78
  async parse(dsn, config) {
64
79
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
@@ -85,13 +100,39 @@ Expected: ${expectedFormat}`
85
100
  };
86
101
  let sslmode;
87
102
  let sslrootcert;
103
+ let sslcert;
104
+ let sslkey;
88
105
  url.forEachSearchParam((value, key) => {
89
106
  if (key === "sslmode") {
90
107
  sslmode = value;
91
108
  } else if (key === "sslrootcert") {
92
109
  sslrootcert = value;
110
+ } else if (key === "sslcert") {
111
+ sslcert = value;
112
+ } else if (key === "sslkey") {
113
+ sslkey = value;
93
114
  }
94
115
  });
116
+ if (sslmode !== void 0 && !SUPPORTED_SSL_MODES.includes(sslmode)) {
117
+ throw new Error(
118
+ `Unsupported sslmode '${sslmode}'. Valid values: ${SUPPORTED_SSL_MODES.join(", ")}`
119
+ );
120
+ }
121
+ if (sslrootcert !== void 0 && sslmode !== "verify-ca" && sslmode !== "verify-full") {
122
+ throw new FailedToReadCertificate(
123
+ `sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got '${sslmode ?? "not set"}'). Use sslmode=verify-ca to verify the server certificate against it, or remove sslrootcert.`
124
+ );
125
+ }
126
+ if (sslcert === void 0 !== (sslkey === void 0)) {
127
+ throw new FailedToReadCertificate(
128
+ "sslcert and sslkey must be set together for client certificate authentication"
129
+ );
130
+ }
131
+ if (sslcert !== void 0 && !CLIENT_CERT_SSL_MODES.includes(sslmode ?? "")) {
132
+ throw new FailedToReadCertificate(
133
+ `sslcert/sslkey require sslmode to be one of ${CLIENT_CERT_SSL_MODES.join(", ")} (got '${sslmode ?? "not set"}')`
134
+ );
135
+ }
95
136
  if (sslmode === "disable") {
96
137
  poolConfig.ssl = false;
97
138
  } else if (sslmode === "require") {
@@ -102,18 +143,21 @@ Expected: ${expectedFormat}`
102
143
  sslConfig.checkServerIdentity = () => void 0;
103
144
  }
104
145
  if (sslrootcert) {
105
- const certPath = sslrootcert.startsWith("~/") ? path.join(os.homedir(), sslrootcert.slice(2)) : sslrootcert;
106
- try {
107
- sslConfig.ca = await fs.promises.readFile(certPath, "utf-8");
108
- } catch (err) {
109
- throw new FailedToReadCertificate(
110
- `Failed to read SSL root certificate at '${certPath}': ${err instanceof Error ? err.message : String(err)}`
111
- );
112
- }
146
+ sslConfig.ca = await readPemFile(sslrootcert, "SSL root certificate");
113
147
  }
114
148
  poolConfig.ssl = sslConfig;
115
- } else if (sslmode !== void 0) {
116
- poolConfig.ssl = true;
149
+ }
150
+ if (sslcert !== void 0 && sslkey !== void 0) {
151
+ const key = await readPemFile(sslkey, "SSL client key");
152
+ if (isEncryptedPemKey(key)) {
153
+ throw new FailedToReadCertificate(
154
+ `SSL client key at '${sslkey}' is encrypted; encrypted private keys are not supported. Decrypt it first, e.g. 'openssl pkey -in client.key -out client-plain.key'`
155
+ );
156
+ }
157
+ Object.assign(poolConfig.ssl, {
158
+ cert: await readPemFile(sslcert, "SSL client certificate"),
159
+ key
160
+ });
117
161
  }
118
162
  if (connectionTimeoutSeconds !== void 0) {
119
163
  poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
@@ -2,7 +2,7 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-5FL4XMWG.js";
5
+ } from "./chunk-MJVIE3QY.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
7
  import "./chunk-IXC4NQGC.js";
8
8
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite",
6
6
  "repository": {