@bytebase/dbhub 0.13.2 → 0.15.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.
@@ -62,161 +62,11 @@ var ConnectorRegistry = _ConnectorRegistry;
62
62
 
63
63
  // src/utils/ssh-tunnel.ts
64
64
  import { Client } from "ssh2";
65
- import { readFileSync } from "fs";
65
+ import { readFileSync as readFileSync2 } from "fs";
66
66
  import { createServer } from "net";
67
- var SSHTunnel = class {
68
- constructor() {
69
- this.sshClient = null;
70
- this.localServer = null;
71
- this.tunnelInfo = null;
72
- this.isConnected = false;
73
- }
74
- /**
75
- * Establish an SSH tunnel
76
- * @param config SSH connection configuration
77
- * @param options Tunnel options including target host and port
78
- * @returns Promise resolving to tunnel information including local port
79
- */
80
- async establish(config, options) {
81
- if (this.isConnected) {
82
- throw new Error("SSH tunnel is already established");
83
- }
84
- return new Promise((resolve, reject) => {
85
- this.sshClient = new Client();
86
- const sshConfig = {
87
- host: config.host,
88
- port: config.port || 22,
89
- username: config.username
90
- };
91
- if (config.password) {
92
- sshConfig.password = config.password;
93
- } else if (config.privateKey) {
94
- try {
95
- const privateKey = readFileSync(config.privateKey);
96
- sshConfig.privateKey = privateKey;
97
- if (config.passphrase) {
98
- sshConfig.passphrase = config.passphrase;
99
- }
100
- } catch (error) {
101
- reject(new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`));
102
- return;
103
- }
104
- } else {
105
- reject(new Error("Either password or privateKey must be provided for SSH authentication"));
106
- return;
107
- }
108
- this.sshClient.on("error", (err) => {
109
- this.cleanup();
110
- reject(new Error(`SSH connection error: ${err.message}`));
111
- });
112
- this.sshClient.on("ready", () => {
113
- console.error("SSH connection established");
114
- this.localServer = createServer((localSocket) => {
115
- this.sshClient.forwardOut(
116
- "127.0.0.1",
117
- 0,
118
- options.targetHost,
119
- options.targetPort,
120
- (err, stream) => {
121
- if (err) {
122
- console.error("SSH forward error:", err);
123
- localSocket.end();
124
- return;
125
- }
126
- localSocket.pipe(stream).pipe(localSocket);
127
- stream.on("error", (err2) => {
128
- console.error("SSH stream error:", err2);
129
- localSocket.end();
130
- });
131
- localSocket.on("error", (err2) => {
132
- console.error("Local socket error:", err2);
133
- stream.end();
134
- });
135
- }
136
- );
137
- });
138
- const localPort = options.localPort || 0;
139
- this.localServer.listen(localPort, "127.0.0.1", () => {
140
- const address = this.localServer.address();
141
- if (!address || typeof address === "string") {
142
- this.cleanup();
143
- reject(new Error("Failed to get local server address"));
144
- return;
145
- }
146
- this.tunnelInfo = {
147
- localPort: address.port,
148
- targetHost: options.targetHost,
149
- targetPort: options.targetPort
150
- };
151
- this.isConnected = true;
152
- console.error(`SSH tunnel established: localhost:${address.port} -> ${options.targetHost}:${options.targetPort}`);
153
- resolve(this.tunnelInfo);
154
- });
155
- this.localServer.on("error", (err) => {
156
- this.cleanup();
157
- reject(new Error(`Local server error: ${err.message}`));
158
- });
159
- });
160
- this.sshClient.connect(sshConfig);
161
- });
162
- }
163
- /**
164
- * Close the SSH tunnel and clean up resources
165
- */
166
- async close() {
167
- if (!this.isConnected) {
168
- return;
169
- }
170
- return new Promise((resolve) => {
171
- this.cleanup();
172
- this.isConnected = false;
173
- console.error("SSH tunnel closed");
174
- resolve();
175
- });
176
- }
177
- /**
178
- * Clean up resources
179
- */
180
- cleanup() {
181
- if (this.localServer) {
182
- this.localServer.close();
183
- this.localServer = null;
184
- }
185
- if (this.sshClient) {
186
- this.sshClient.end();
187
- this.sshClient = null;
188
- }
189
- this.tunnelInfo = null;
190
- }
191
- /**
192
- * Get current tunnel information
193
- */
194
- getTunnelInfo() {
195
- return this.tunnelInfo;
196
- }
197
- /**
198
- * Check if tunnel is connected
199
- */
200
- getIsConnected() {
201
- return this.isConnected;
202
- }
203
- };
204
-
205
- // src/config/toml-loader.ts
206
- import fs2 from "fs";
207
- import path2 from "path";
208
- import { homedir as homedir3 } from "os";
209
- import toml from "@iarna/toml";
210
-
211
- // src/config/env.ts
212
- import dotenv from "dotenv";
213
- import path from "path";
214
- import fs from "fs";
215
- import { fileURLToPath } from "url";
216
- import { homedir as homedir2 } from "os";
217
67
 
218
68
  // src/utils/ssh-config-parser.ts
219
- import { readFileSync as readFileSync2, existsSync } from "fs";
69
+ import { readFileSync, realpathSync, statSync } from "fs";
220
70
  import { homedir } from "os";
221
71
  import { join } from "path";
222
72
  import SSHConfig from "ssh-config";
@@ -232,28 +82,38 @@ function expandTilde(filePath) {
232
82
  }
233
83
  return filePath;
234
84
  }
235
- function fileExists(filePath) {
85
+ function resolveSymlink(filePath) {
86
+ const expandedPath = expandTilde(filePath);
236
87
  try {
237
- return existsSync(expandTilde(filePath));
88
+ return realpathSync(expandedPath);
89
+ } catch {
90
+ return expandedPath;
91
+ }
92
+ }
93
+ function isFile(filePath) {
94
+ try {
95
+ const stat = statSync(filePath);
96
+ return stat.isFile();
238
97
  } catch {
239
98
  return false;
240
99
  }
241
100
  }
242
101
  function findDefaultSSHKey() {
243
102
  for (const keyPath of DEFAULT_SSH_KEYS) {
244
- if (fileExists(keyPath)) {
245
- return expandTilde(keyPath);
103
+ const resolvedPath = resolveSymlink(keyPath);
104
+ if (isFile(resolvedPath)) {
105
+ return resolvedPath;
246
106
  }
247
107
  }
248
108
  return void 0;
249
109
  }
250
110
  function parseSSHConfig(hostAlias, configPath) {
251
- const sshConfigPath = configPath;
252
- if (!existsSync(sshConfigPath)) {
111
+ const sshConfigPath = resolveSymlink(configPath);
112
+ if (!isFile(sshConfigPath)) {
253
113
  return null;
254
114
  }
255
115
  try {
256
- const configContent = readFileSync2(sshConfigPath, "utf8");
116
+ const configContent = readFileSync(sshConfigPath, "utf8");
257
117
  const config = SSHConfig.parse(configContent);
258
118
  const hostConfig = config.compute(hostAlias);
259
119
  if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
@@ -273,9 +133,9 @@ function parseSSHConfig(hostAlias, configPath) {
273
133
  }
274
134
  if (hostConfig.IdentityFile) {
275
135
  const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
276
- const expandedPath = expandTilde(identityFile);
277
- if (fileExists(expandedPath)) {
278
- sshConfig.privateKey = expandedPath;
136
+ const resolvedPath = resolveSymlink(identityFile);
137
+ if (isFile(resolvedPath)) {
138
+ sshConfig.privateKey = resolvedPath;
279
139
  }
280
140
  }
281
141
  if (!sshConfig.privateKey) {
@@ -284,8 +144,11 @@ function parseSSHConfig(hostAlias, configPath) {
284
144
  sshConfig.privateKey = defaultKey;
285
145
  }
286
146
  }
287
- if (hostConfig.ProxyJump || hostConfig.ProxyCommand) {
288
- console.error("Warning: ProxyJump/ProxyCommand in SSH config is not yet supported by DBHub");
147
+ if (hostConfig.ProxyJump) {
148
+ sshConfig.proxyJump = hostConfig.ProxyJump;
149
+ }
150
+ if (hostConfig.ProxyCommand) {
151
+ console.error("Warning: ProxyCommand in SSH config is not supported by DBHub. Use ProxyJump instead.");
289
152
  }
290
153
  if (!sshConfig.host || !sshConfig.username) {
291
154
  return null;
@@ -308,6 +171,332 @@ function looksLikeSSHAlias(host) {
308
171
  }
309
172
  return true;
310
173
  }
174
+ function validatePort(port, jumpHostStr) {
175
+ if (isNaN(port) || port <= 0 || port > 65535) {
176
+ throw new Error(`Invalid port number in "${jumpHostStr}": port must be between 1 and 65535`);
177
+ }
178
+ }
179
+ function parseJumpHost(jumpHostStr) {
180
+ let username;
181
+ let host;
182
+ let port = 22;
183
+ let remaining = jumpHostStr.trim();
184
+ if (!remaining) {
185
+ throw new Error("Jump host string cannot be empty");
186
+ }
187
+ const atIndex = remaining.indexOf("@");
188
+ if (atIndex !== -1) {
189
+ const extractedUsername = remaining.substring(0, atIndex).trim();
190
+ if (extractedUsername) {
191
+ username = extractedUsername;
192
+ }
193
+ remaining = remaining.substring(atIndex + 1);
194
+ }
195
+ if (remaining.startsWith("[")) {
196
+ const closeBracket = remaining.indexOf("]");
197
+ if (closeBracket !== -1) {
198
+ host = remaining.substring(1, closeBracket);
199
+ const afterBracket = remaining.substring(closeBracket + 1);
200
+ if (afterBracket.startsWith(":")) {
201
+ const parsedPort = parseInt(afterBracket.substring(1), 10);
202
+ validatePort(parsedPort, jumpHostStr);
203
+ port = parsedPort;
204
+ }
205
+ } else {
206
+ throw new Error(`Invalid ProxyJump host "${jumpHostStr}": missing closing bracket in IPv6 address`);
207
+ }
208
+ } else {
209
+ const lastColon = remaining.lastIndexOf(":");
210
+ if (lastColon !== -1) {
211
+ const potentialPort = remaining.substring(lastColon + 1);
212
+ if (/^\d+$/.test(potentialPort)) {
213
+ host = remaining.substring(0, lastColon);
214
+ const parsedPort = parseInt(potentialPort, 10);
215
+ validatePort(parsedPort, jumpHostStr);
216
+ port = parsedPort;
217
+ } else {
218
+ host = remaining;
219
+ }
220
+ } else {
221
+ host = remaining;
222
+ }
223
+ }
224
+ if (!host) {
225
+ throw new Error(`Invalid jump host format: "${jumpHostStr}" - host cannot be empty`);
226
+ }
227
+ return { host, port, username };
228
+ }
229
+ function parseJumpHosts(proxyJump) {
230
+ if (!proxyJump || proxyJump.trim() === "" || proxyJump.toLowerCase() === "none") {
231
+ return [];
232
+ }
233
+ return proxyJump.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(parseJumpHost);
234
+ }
235
+
236
+ // src/utils/ssh-tunnel.ts
237
+ var SSHTunnel = class {
238
+ constructor() {
239
+ this.sshClients = [];
240
+ // All SSH clients in the chain
241
+ this.localServer = null;
242
+ this.tunnelInfo = null;
243
+ this.isConnected = false;
244
+ }
245
+ /**
246
+ * Establish an SSH tunnel, optionally through jump hosts (ProxyJump).
247
+ * @param config SSH connection configuration
248
+ * @param options Tunnel options including target host and port
249
+ * @returns Promise resolving to tunnel information including local port
250
+ */
251
+ async establish(config, options) {
252
+ if (this.isConnected) {
253
+ throw new Error("SSH tunnel is already established");
254
+ }
255
+ this.isConnected = true;
256
+ try {
257
+ const jumpHosts = config.proxyJump ? parseJumpHosts(config.proxyJump) : [];
258
+ let privateKeyBuffer;
259
+ if (config.privateKey) {
260
+ try {
261
+ const resolvedKeyPath = resolveSymlink(config.privateKey);
262
+ privateKeyBuffer = readFileSync2(resolvedKeyPath);
263
+ } catch (error) {
264
+ throw new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ }
267
+ if (!config.password && !privateKeyBuffer) {
268
+ throw new Error("Either password or privateKey must be provided for SSH authentication");
269
+ }
270
+ const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer);
271
+ return await this.createLocalTunnel(finalClient, options);
272
+ } catch (error) {
273
+ this.cleanup();
274
+ throw error;
275
+ }
276
+ }
277
+ /**
278
+ * Establish a chain of SSH connections through jump hosts.
279
+ * @returns The final SSH client connected to the target host
280
+ */
281
+ async establishChain(jumpHosts, targetConfig, privateKey) {
282
+ let previousStream;
283
+ for (let i = 0; i < jumpHosts.length; i++) {
284
+ const jumpHost = jumpHosts[i];
285
+ const nextHost = i + 1 < jumpHosts.length ? jumpHosts[i + 1] : { host: targetConfig.host, port: targetConfig.port || 22 };
286
+ let client = null;
287
+ let forwardStream;
288
+ try {
289
+ client = await this.connectToHost(
290
+ {
291
+ host: jumpHost.host,
292
+ port: jumpHost.port,
293
+ username: jumpHost.username || targetConfig.username
294
+ },
295
+ targetConfig.password,
296
+ privateKey,
297
+ targetConfig.passphrase,
298
+ previousStream,
299
+ `jump host ${i + 1}`
300
+ );
301
+ console.error(` \u2192 Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`);
302
+ forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port);
303
+ } catch (error) {
304
+ if (client) {
305
+ try {
306
+ client.end();
307
+ } catch {
308
+ }
309
+ }
310
+ throw error;
311
+ }
312
+ this.sshClients.push(client);
313
+ previousStream = forwardStream;
314
+ }
315
+ const finalClient = await this.connectToHost(
316
+ {
317
+ host: targetConfig.host,
318
+ port: targetConfig.port || 22,
319
+ username: targetConfig.username
320
+ },
321
+ targetConfig.password,
322
+ privateKey,
323
+ targetConfig.passphrase,
324
+ previousStream,
325
+ jumpHosts.length > 0 ? "target host" : void 0
326
+ );
327
+ this.sshClients.push(finalClient);
328
+ return finalClient;
329
+ }
330
+ /**
331
+ * Connect to a single SSH host.
332
+ */
333
+ connectToHost(hostInfo, password, privateKey, passphrase, sock, label) {
334
+ return new Promise((resolve, reject) => {
335
+ const client = new Client();
336
+ const sshConfig = {
337
+ host: hostInfo.host,
338
+ port: hostInfo.port,
339
+ username: hostInfo.username
340
+ };
341
+ if (password) {
342
+ sshConfig.password = password;
343
+ }
344
+ if (privateKey) {
345
+ sshConfig.privateKey = privateKey;
346
+ if (passphrase) {
347
+ sshConfig.passphrase = passphrase;
348
+ }
349
+ }
350
+ if (sock) {
351
+ sshConfig.sock = sock;
352
+ }
353
+ const onError = (err) => {
354
+ client.removeListener("ready", onReady);
355
+ client.destroy();
356
+ reject(new Error(`SSH connection error${label ? ` (${label})` : ""}: ${err.message}`));
357
+ };
358
+ const onReady = () => {
359
+ client.removeListener("error", onError);
360
+ const desc = label || `${hostInfo.host}:${hostInfo.port}`;
361
+ console.error(`SSH connection established: ${desc}`);
362
+ resolve(client);
363
+ };
364
+ client.on("error", onError);
365
+ client.on("ready", onReady);
366
+ client.connect(sshConfig);
367
+ });
368
+ }
369
+ /**
370
+ * Forward a connection through an SSH client to a target host.
371
+ */
372
+ forwardTo(client, targetHost, targetPort) {
373
+ return new Promise((resolve, reject) => {
374
+ client.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream) => {
375
+ if (err) {
376
+ reject(new Error(`SSH forward error: ${err.message}`));
377
+ return;
378
+ }
379
+ resolve(stream);
380
+ });
381
+ });
382
+ }
383
+ /**
384
+ * Create the local server that tunnels connections to the database.
385
+ */
386
+ createLocalTunnel(sshClient, options) {
387
+ return new Promise((resolve, reject) => {
388
+ let settled = false;
389
+ this.localServer = createServer((localSocket) => {
390
+ sshClient.forwardOut(
391
+ "127.0.0.1",
392
+ 0,
393
+ options.targetHost,
394
+ options.targetPort,
395
+ (err, stream) => {
396
+ if (err) {
397
+ console.error("SSH forward error:", err);
398
+ localSocket.end();
399
+ return;
400
+ }
401
+ localSocket.pipe(stream).pipe(localSocket);
402
+ stream.on("error", (err2) => {
403
+ console.error("SSH stream error:", err2);
404
+ localSocket.end();
405
+ });
406
+ localSocket.on("error", (err2) => {
407
+ console.error("Local socket error:", err2);
408
+ stream.end();
409
+ });
410
+ }
411
+ );
412
+ });
413
+ this.localServer.on("error", (err) => {
414
+ if (!settled) {
415
+ settled = true;
416
+ reject(new Error(`Local server error: ${err.message}`));
417
+ } else {
418
+ console.error("Local server error after tunnel established:", err);
419
+ this.cleanup();
420
+ }
421
+ });
422
+ const localPort = options.localPort || 0;
423
+ this.localServer.listen(localPort, "127.0.0.1", () => {
424
+ const address = this.localServer.address();
425
+ if (!address || typeof address === "string") {
426
+ if (!settled) {
427
+ settled = true;
428
+ reject(new Error("Failed to get local server address"));
429
+ }
430
+ return;
431
+ }
432
+ this.tunnelInfo = {
433
+ localPort: address.port,
434
+ targetHost: options.targetHost,
435
+ targetPort: options.targetPort
436
+ };
437
+ console.error(`SSH tunnel established: localhost:${address.port} \u2192 ${options.targetHost}:${options.targetPort}`);
438
+ settled = true;
439
+ resolve(this.tunnelInfo);
440
+ });
441
+ });
442
+ }
443
+ /**
444
+ * Close the SSH tunnel and clean up resources
445
+ */
446
+ async close() {
447
+ if (!this.isConnected) {
448
+ return;
449
+ }
450
+ return new Promise((resolve) => {
451
+ this.cleanup();
452
+ console.error("SSH tunnel closed");
453
+ resolve();
454
+ });
455
+ }
456
+ /**
457
+ * Clean up resources. Closes all SSH clients in reverse order (innermost first).
458
+ */
459
+ cleanup() {
460
+ if (this.localServer) {
461
+ this.localServer.close();
462
+ this.localServer = null;
463
+ }
464
+ for (let i = this.sshClients.length - 1; i >= 0; i--) {
465
+ try {
466
+ this.sshClients[i].end();
467
+ } catch {
468
+ }
469
+ }
470
+ this.sshClients = [];
471
+ this.tunnelInfo = null;
472
+ this.isConnected = false;
473
+ }
474
+ /**
475
+ * Get current tunnel information
476
+ */
477
+ getTunnelInfo() {
478
+ return this.tunnelInfo;
479
+ }
480
+ /**
481
+ * Check if tunnel is connected
482
+ */
483
+ getIsConnected() {
484
+ return this.isConnected;
485
+ }
486
+ };
487
+
488
+ // src/config/toml-loader.ts
489
+ import fs2 from "fs";
490
+ import path2 from "path";
491
+ import { homedir as homedir3 } from "os";
492
+ import toml from "@iarna/toml";
493
+
494
+ // src/config/env.ts
495
+ import dotenv from "dotenv";
496
+ import path from "path";
497
+ import fs from "fs";
498
+ import { fileURLToPath } from "url";
499
+ import { homedir as homedir2 } from "os";
311
500
 
312
501
  // src/utils/safe-url.ts
313
502
  var SafeURL = class {
@@ -815,6 +1004,13 @@ function resolveSSHConfig() {
815
1004
  config.passphrase = process.env.SSH_PASSPHRASE;
816
1005
  sources.push("SSH_PASSPHRASE from environment");
817
1006
  }
1007
+ if (args["ssh-proxy-jump"]) {
1008
+ config.proxyJump = args["ssh-proxy-jump"];
1009
+ sources.push("ssh-proxy-jump from command line");
1010
+ } else if (process.env.SSH_PROXY_JUMP) {
1011
+ config.proxyJump = process.env.SSH_PROXY_JUMP;
1012
+ sources.push("SSH_PROXY_JUMP from environment");
1013
+ }
818
1014
  if (!config.host || !config.username) {
819
1015
  throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
820
1016
  }
@@ -917,8 +1113,13 @@ function loadTomlConfig() {
917
1113
  try {
918
1114
  const fileContent = fs2.readFileSync(configPath, "utf-8");
919
1115
  const parsedToml = toml.parse(fileContent);
920
- validateTomlConfig(parsedToml, configPath);
1116
+ if (!Array.isArray(parsedToml.sources)) {
1117
+ throw new Error(
1118
+ `Configuration file ${configPath}: must contain a [[sources]] array. Use [[sources]] syntax for array of tables in TOML.`
1119
+ );
1120
+ }
921
1121
  const sources = processSourceConfigs(parsedToml.sources, configPath);
1122
+ validateTomlConfig({ ...parsedToml, sources }, configPath);
922
1123
  return {
923
1124
  sources,
924
1125
  tools: parsedToml.tools,
@@ -960,11 +1161,6 @@ id = "my_db"
960
1161
  dsn = "postgres://..."`
961
1162
  );
962
1163
  }
963
- if (!Array.isArray(config.sources)) {
964
- throw new Error(
965
- `Configuration file ${configPath}: 'sources' must be an array. Use [[sources]] syntax for array of tables in TOML.`
966
- );
967
- }
968
1164
  if (config.sources.length === 0) {
969
1165
  throw new Error(
970
1166
  `Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
@@ -1041,11 +1237,6 @@ function validateToolsConfig(tools, sources, configPath) {
1041
1237
  `Configuration file ${configPath}: custom tool '${tool.name}' must have 'description' and 'statement' fields`
1042
1238
  );
1043
1239
  }
1044
- if (tool.readonly !== void 0 || tool.max_rows !== void 0) {
1045
- throw new Error(
1046
- `Configuration file ${configPath}: custom tool '${tool.name}' cannot have readonly or max_rows fields (these are only valid for ${BUILTIN_TOOL_EXECUTE_SQL} tool)`
1047
- );
1048
- }
1049
1240
  }
1050
1241
  if (tool.max_rows !== void 0) {
1051
1242
  if (typeof tool.max_rows !== "number" || tool.max_rows <= 0) {
@@ -1054,6 +1245,11 @@ function validateToolsConfig(tools, sources, configPath) {
1054
1245
  );
1055
1246
  }
1056
1247
  }
1248
+ if (tool.readonly !== void 0 && typeof tool.readonly !== "boolean") {
1249
+ throw new Error(
1250
+ `Configuration file ${configPath}: tool '${tool.name}' has invalid readonly. Must be a boolean (true or false).`
1251
+ );
1252
+ }
1057
1253
  }
1058
1254
  }
1059
1255
  function validateSourceConfig(source, configPath) {
@@ -1081,10 +1277,10 @@ function validateSourceConfig(source, configPath) {
1081
1277
  );
1082
1278
  }
1083
1279
  }
1084
- if (source.request_timeout !== void 0) {
1085
- if (typeof source.request_timeout !== "number" || source.request_timeout <= 0) {
1280
+ if (source.query_timeout !== void 0) {
1281
+ if (typeof source.query_timeout !== "number" || source.query_timeout <= 0) {
1086
1282
  throw new Error(
1087
- `Configuration file ${configPath}: source '${source.id}' has invalid request_timeout. Must be a positive number (in seconds).`
1283
+ `Configuration file ${configPath}: source '${source.id}' has invalid query_timeout. Must be a positive number (in seconds).`
1088
1284
  );
1089
1285
  }
1090
1286
  }
@@ -1095,6 +1291,64 @@ function validateSourceConfig(source, configPath) {
1095
1291
  );
1096
1292
  }
1097
1293
  }
1294
+ if (source.sslmode !== void 0) {
1295
+ if (source.type === "sqlite") {
1296
+ throw new Error(
1297
+ `Configuration file ${configPath}: source '${source.id}' has sslmode but SQLite does not support SSL. Remove the sslmode field for SQLite sources.`
1298
+ );
1299
+ }
1300
+ const validSslModes = ["disable", "require"];
1301
+ if (!validSslModes.includes(source.sslmode)) {
1302
+ throw new Error(
1303
+ `Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
1304
+ );
1305
+ }
1306
+ }
1307
+ if (source.authentication !== void 0) {
1308
+ if (source.type !== "sqlserver") {
1309
+ throw new Error(
1310
+ `Configuration file ${configPath}: source '${source.id}' has authentication but it is only supported for SQL Server.`
1311
+ );
1312
+ }
1313
+ const validAuthMethods = ["ntlm", "azure-active-directory-access-token"];
1314
+ if (!validAuthMethods.includes(source.authentication)) {
1315
+ throw new Error(
1316
+ `Configuration file ${configPath}: source '${source.id}' has invalid authentication '${source.authentication}'. Valid values: ${validAuthMethods.join(", ")}`
1317
+ );
1318
+ }
1319
+ if (source.authentication === "ntlm" && !source.domain) {
1320
+ throw new Error(
1321
+ `Configuration file ${configPath}: source '${source.id}' uses NTLM authentication but 'domain' is not specified.`
1322
+ );
1323
+ }
1324
+ }
1325
+ if (source.domain !== void 0) {
1326
+ if (source.type !== "sqlserver") {
1327
+ throw new Error(
1328
+ `Configuration file ${configPath}: source '${source.id}' has domain but it is only supported for SQL Server.`
1329
+ );
1330
+ }
1331
+ if (source.authentication === void 0) {
1332
+ throw new Error(
1333
+ `Configuration file ${configPath}: source '${source.id}' has domain but authentication is not set. Add authentication = "ntlm" to use Windows domain authentication.`
1334
+ );
1335
+ }
1336
+ if (source.authentication !== "ntlm") {
1337
+ throw new Error(
1338
+ `Configuration file ${configPath}: source '${source.id}' has domain but authentication is set to '${source.authentication}'. Domain is only valid with authentication = "ntlm".`
1339
+ );
1340
+ }
1341
+ }
1342
+ if (source.readonly !== void 0) {
1343
+ throw new Error(
1344
+ `Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
1345
+ );
1346
+ }
1347
+ if (source.max_rows !== void 0) {
1348
+ throw new Error(
1349
+ `Configuration file ${configPath}: source '${source.id}' has 'max_rows' field, but max_rows must be configured per-tool, not per-source. Move 'max_rows' to [[tools]] configuration instead.`
1350
+ );
1351
+ }
1098
1352
  }
1099
1353
  function processSourceConfigs(sources, configPath) {
1100
1354
  return sources.map((source) => {
@@ -1154,9 +1408,15 @@ function buildDSNFromSource(source) {
1154
1408
  }
1155
1409
  return `sqlite:///${source.database}`;
1156
1410
  }
1157
- if (!source.host || !source.user || !source.password || !source.database) {
1411
+ const passwordRequired = source.authentication !== "azure-active-directory-access-token";
1412
+ if (!source.host || !source.user || !source.database) {
1413
+ throw new Error(
1414
+ `Source '${source.id}': missing required connection parameters. Required: type, host, user, database`
1415
+ );
1416
+ }
1417
+ if (passwordRequired && !source.password) {
1158
1418
  throw new Error(
1159
- `Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
1419
+ `Source '${source.id}': password is required. (Password is optional only for azure-active-directory-access-token authentication)`
1160
1420
  );
1161
1421
  }
1162
1422
  const port = source.port || getDefaultPortForType(source.type);
@@ -1164,11 +1424,26 @@ function buildDSNFromSource(source) {
1164
1424
  throw new Error(`Source '${source.id}': unable to determine port`);
1165
1425
  }
1166
1426
  const encodedUser = encodeURIComponent(source.user);
1167
- const encodedPassword = encodeURIComponent(source.password);
1427
+ const encodedPassword = source.password ? encodeURIComponent(source.password) : "";
1168
1428
  const encodedDatabase = encodeURIComponent(source.database);
1169
1429
  let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
1170
- if (source.type === "sqlserver" && source.instanceName) {
1171
- dsn += `?instanceName=${encodeURIComponent(source.instanceName)}`;
1430
+ const queryParams = [];
1431
+ if (source.type === "sqlserver") {
1432
+ if (source.instanceName) {
1433
+ queryParams.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
1434
+ }
1435
+ if (source.authentication) {
1436
+ queryParams.push(`authentication=${encodeURIComponent(source.authentication)}`);
1437
+ }
1438
+ if (source.domain) {
1439
+ queryParams.push(`domain=${encodeURIComponent(source.domain)}`);
1440
+ }
1441
+ }
1442
+ if (source.sslmode && source.type !== "sqlite") {
1443
+ queryParams.push(`sslmode=${source.sslmode}`);
1444
+ }
1445
+ if (queryParams.length > 0) {
1446
+ dsn += `?${queryParams.join("&")}`;
1172
1447
  }
1173
1448
  return dsn;
1174
1449
  }
@@ -1181,7 +1456,6 @@ var ConnectorManager = class {
1181
1456
  // Maps for multi-source support
1182
1457
  this.connectors = /* @__PURE__ */ new Map();
1183
1458
  this.sshTunnels = /* @__PURE__ */ new Map();
1184
- this.executeOptions = /* @__PURE__ */ new Map();
1185
1459
  this.sourceConfigs = /* @__PURE__ */ new Map();
1186
1460
  // Store original source configs
1187
1461
  this.sourceIds = [];
@@ -1197,6 +1471,7 @@ var ConnectorManager = class {
1197
1471
  if (sources.length === 0) {
1198
1472
  throw new Error("No sources provided");
1199
1473
  }
1474
+ console.error(`Connecting to ${sources.length} database source(s)...`);
1200
1475
  for (const source of sources) {
1201
1476
  await this.connectSource(source);
1202
1477
  }
@@ -1207,6 +1482,7 @@ var ConnectorManager = class {
1207
1482
  async connectSource(source) {
1208
1483
  const sourceId = source.id;
1209
1484
  const dsn = buildDSNFromSource(source);
1485
+ console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
1210
1486
  let actualDSN = dsn;
1211
1487
  if (source.ssh_host) {
1212
1488
  if (!source.ssh_user) {
@@ -1220,7 +1496,8 @@ var ConnectorManager = class {
1220
1496
  username: source.ssh_user,
1221
1497
  password: source.ssh_password,
1222
1498
  privateKey: source.ssh_key,
1223
- passphrase: source.ssh_passphrase
1499
+ passphrase: source.ssh_passphrase,
1500
+ proxyJump: source.ssh_proxy_jump
1224
1501
  };
1225
1502
  if (!sshConfig.password && !sshConfig.privateKey) {
1226
1503
  throw new Error(
@@ -1255,8 +1532,8 @@ var ConnectorManager = class {
1255
1532
  if (source.connection_timeout !== void 0) {
1256
1533
  config.connectionTimeoutSeconds = source.connection_timeout;
1257
1534
  }
1258
- if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
1259
- config.requestTimeoutSeconds = source.request_timeout;
1535
+ if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
1536
+ config.queryTimeoutSeconds = source.query_timeout;
1260
1537
  }
1261
1538
  if (source.readonly !== void 0) {
1262
1539
  config.readonly = source.readonly;
@@ -1265,14 +1542,6 @@ var ConnectorManager = class {
1265
1542
  this.connectors.set(sourceId, connector);
1266
1543
  this.sourceIds.push(sourceId);
1267
1544
  this.sourceConfigs.set(sourceId, source);
1268
- const options = {};
1269
- if (source.max_rows !== void 0) {
1270
- options.maxRows = source.max_rows;
1271
- }
1272
- if (source.readonly !== void 0) {
1273
- options.readonly = source.readonly;
1274
- }
1275
- this.executeOptions.set(sourceId, options);
1276
1545
  }
1277
1546
  /**
1278
1547
  * Close all database connections
@@ -1295,7 +1564,6 @@ var ConnectorManager = class {
1295
1564
  }
1296
1565
  this.connectors.clear();
1297
1566
  this.sshTunnels.clear();
1298
- this.executeOptions.clear();
1299
1567
  this.sourceConfigs.clear();
1300
1568
  this.sourceIds = [];
1301
1569
  }
@@ -1340,25 +1608,6 @@ var ConnectorManager = class {
1340
1608
  }
1341
1609
  return managerInstance.getConnector(sourceId);
1342
1610
  }
1343
- /**
1344
- * Get execute options for SQL execution
1345
- * @param sourceId - Optional source ID. If not provided, returns default options
1346
- */
1347
- getExecuteOptions(sourceId) {
1348
- const id = sourceId || this.sourceIds[0];
1349
- return this.executeOptions.get(id) || {};
1350
- }
1351
- /**
1352
- * Get the current execute options
1353
- * This is used by tool handlers
1354
- * @param sourceId - Optional source ID. If not provided, returns default options
1355
- */
1356
- static getCurrentExecuteOptions(sourceId) {
1357
- if (!managerInstance) {
1358
- throw new Error("ConnectorManager not initialized");
1359
- }
1360
- return managerInstance.getExecuteOptions(sourceId);
1361
- }
1362
1611
  /**
1363
1612
  * Get all available source IDs
1364
1613
  */
@@ -1812,11 +2061,9 @@ export {
1812
2061
  isDemoMode,
1813
2062
  resolveTransport,
1814
2063
  resolvePort,
1815
- redactDSN,
1816
2064
  resolveSourceConfigs,
1817
2065
  BUILTIN_TOOL_EXECUTE_SQL,
1818
2066
  BUILTIN_TOOL_SEARCH_OBJECTS,
1819
- buildDSNFromSource,
1820
2067
  ConnectorManager,
1821
2068
  mapArgumentsToArray,
1822
2069
  ToolRegistry,