@bytebase/dbhub 1.3.1 → 1.5.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.
- package/dist/{chunk-ABTVMHD6.js → chunk-55X2VY5D.js} +1 -1
- package/dist/chunk-5XSBOK3W.js +63 -0
- package/dist/{chunk-5FL4XMWG.js → chunk-PDKPK25P.js} +356 -106
- package/dist/chunk-SJ5BRPQA.js +500 -0
- package/dist/{chunk-YIGJHEU6.js → chunk-X5OCFRNL.js} +170 -27
- package/dist/index.js +116 -32
- package/dist/mariadb-RWOMZTA6.js +270 -0
- package/dist/mysql-ZXO4TWLG.js +312 -0
- package/dist/{oracle-OUKBU52X.js → oracle-UAVN3F4U.js} +2 -2
- package/dist/{postgres-6XJ7V4J4.js → postgres-KCFNDQLG.js} +179 -29
- package/dist/{registry-ZTHDPLKP.js → registry-ZIVIYCB2.js} +2 -1
- package/dist/{sqlite-AO2BKIC6.js → sqlite-WKY6543Q.js} +2 -2
- package/dist/{sqlserver-AJUK6L55.js → sqlserver-25MAE77Q.js} +312 -372
- package/package.json +1 -1
- package/dist/chunk-6SZKW6GD.js +0 -179
- package/dist/mariadb-L7Z55ROY.js +0 -551
- package/dist/mysql-XXIHWQGR.js +0 -601
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isDriverNotInstalled
|
|
3
3
|
} from "./chunk-WVVMH6FJ.js";
|
|
4
|
+
import {
|
|
5
|
+
MAX_QUERY_TIMEOUT_SECONDS
|
|
6
|
+
} from "./chunk-5XSBOK3W.js";
|
|
4
7
|
import {
|
|
5
8
|
ConnectorRegistry,
|
|
6
9
|
SafeURL,
|
|
@@ -27,8 +30,8 @@ var ALL_BUILTIN_TOOL_NAMES = [
|
|
|
27
30
|
];
|
|
28
31
|
|
|
29
32
|
// src/utils/ssh-tunnel.ts
|
|
30
|
-
import { Client } from "ssh2";
|
|
31
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
33
|
+
import ssh2, { Client } from "ssh2";
|
|
34
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
32
35
|
import { createServer } from "net";
|
|
33
36
|
|
|
34
37
|
// src/utils/ssh-config-parser.ts
|
|
@@ -114,6 +117,9 @@ function parseSSHConfig(hostAlias, configPath, options = {}) {
|
|
|
114
117
|
sshConfig.privateKey = defaultKey;
|
|
115
118
|
}
|
|
116
119
|
}
|
|
120
|
+
if (sshConfig.privateKey) {
|
|
121
|
+
sshConfig.privateKeyDiscovered = true;
|
|
122
|
+
}
|
|
117
123
|
if (hostConfig.ProxyJump) {
|
|
118
124
|
sshConfig.proxyJump = hostConfig.ProxyJump;
|
|
119
125
|
}
|
|
@@ -265,16 +271,61 @@ var SSHTunnel = class {
|
|
|
265
271
|
try {
|
|
266
272
|
const jumpHosts = config.resolvedJumpHosts ?? (config.proxyJump ? parseJumpHosts(config.proxyJump) : []);
|
|
267
273
|
const privateKeyBuffer = config.privateKey ? this.loadPrivateKey(config.privateKey) : void 0;
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
const agent = this.resolveAgent(config.agent);
|
|
275
|
+
if (!config.password && !privateKeyBuffer && !agent) {
|
|
276
|
+
throw new Error("Either password, privateKey, or an SSH agent (agent or SSH_AUTH_SOCK) must be provided for SSH authentication");
|
|
270
277
|
}
|
|
271
|
-
const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer);
|
|
278
|
+
const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer, agent);
|
|
272
279
|
return await this.createLocalTunnel(finalClient, options);
|
|
273
280
|
} catch (error) {
|
|
274
281
|
this.cleanup();
|
|
275
282
|
throw error;
|
|
276
283
|
}
|
|
277
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Pick the SSH agent socket: an explicitly configured one wins over the ambient
|
|
287
|
+
* SSH_AUTH_SOCK. A configured socket that does not exist is an error; a stale
|
|
288
|
+
* SSH_AUTH_SOCK is ignored, since the user never asked DBHub to use it.
|
|
289
|
+
*/
|
|
290
|
+
resolveAgent(configuredAgent) {
|
|
291
|
+
if (configuredAgent) {
|
|
292
|
+
if (!this.agentSocketExists(configuredAgent)) {
|
|
293
|
+
throw new Error(`SSH agent socket not found: ${configuredAgent}`);
|
|
294
|
+
}
|
|
295
|
+
return configuredAgent;
|
|
296
|
+
}
|
|
297
|
+
const ambientAgent = process.env.SSH_AUTH_SOCK;
|
|
298
|
+
if (!ambientAgent) {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
if (!this.agentSocketExists(ambientAgent)) {
|
|
302
|
+
console.warn(`Ignoring SSH_AUTH_SOCK: no SSH agent socket at ${ambientAgent}`);
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
return ambientAgent;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Windows agents are named pipes or Pageant rather than files, so only Unix
|
|
309
|
+
* socket paths are checked.
|
|
310
|
+
*/
|
|
311
|
+
agentSocketExists(agent) {
|
|
312
|
+
return process.platform === "win32" || existsSync(agent);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Why ssh2 would refuse this key, mirroring the checks its connect() runs before
|
|
316
|
+
* trying any auth method, or undefined when the key is usable.
|
|
317
|
+
*/
|
|
318
|
+
unusableKeyReason(privateKey, passphrase) {
|
|
319
|
+
const parsed = ssh2.utils.parseKey(privateKey, passphrase);
|
|
320
|
+
if (parsed instanceof Error) {
|
|
321
|
+
return parsed.message;
|
|
322
|
+
}
|
|
323
|
+
const key = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
324
|
+
if (key.getPrivatePEM() === null) {
|
|
325
|
+
return "not a private key";
|
|
326
|
+
}
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
278
329
|
/**
|
|
279
330
|
* Load an SSH private key, supporting both a file path (with symlink resolution)
|
|
280
331
|
* and base64-encoded key content.
|
|
@@ -303,14 +354,18 @@ var SSHTunnel = class {
|
|
|
303
354
|
* Establish a chain of SSH connections through jump hosts.
|
|
304
355
|
* @returns The final SSH client connected to the target host
|
|
305
356
|
*/
|
|
306
|
-
async establishChain(jumpHosts, targetConfig, privateKey) {
|
|
357
|
+
async establishChain(jumpHosts, targetConfig, privateKey, agent) {
|
|
307
358
|
let previousStream;
|
|
308
359
|
for (let i = 0; i < jumpHosts.length; i++) {
|
|
309
360
|
const jumpHost = jumpHosts[i];
|
|
310
361
|
const nextHost = i + 1 < jumpHosts.length ? jumpHosts[i + 1] : { host: targetConfig.host, port: targetConfig.port || 22 };
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
362
|
+
const hopAuth = {
|
|
363
|
+
password: targetConfig.password,
|
|
364
|
+
privateKey: jumpHost.privateKey ? this.loadPrivateKey(jumpHost.privateKey) : privateKey,
|
|
365
|
+
passphrase: jumpHost.passphrase ?? targetConfig.passphrase,
|
|
366
|
+
privateKeyDiscovered: jumpHost.privateKey ? true : targetConfig.privateKeyDiscovered,
|
|
367
|
+
agent
|
|
368
|
+
};
|
|
314
369
|
let client = null;
|
|
315
370
|
let forwardStream;
|
|
316
371
|
try {
|
|
@@ -320,9 +375,7 @@ var SSHTunnel = class {
|
|
|
320
375
|
port: jumpHost.port,
|
|
321
376
|
username: jumpHost.username || targetConfig.username
|
|
322
377
|
},
|
|
323
|
-
|
|
324
|
-
hopPrivateKey,
|
|
325
|
-
hopPassphrase,
|
|
378
|
+
hopAuth,
|
|
326
379
|
previousStream,
|
|
327
380
|
`jump host ${i + 1}`,
|
|
328
381
|
targetConfig.keepaliveInterval,
|
|
@@ -348,9 +401,13 @@ var SSHTunnel = class {
|
|
|
348
401
|
port: targetConfig.port || 22,
|
|
349
402
|
username: targetConfig.username
|
|
350
403
|
},
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
404
|
+
{
|
|
405
|
+
password: targetConfig.password,
|
|
406
|
+
privateKey,
|
|
407
|
+
passphrase: targetConfig.passphrase,
|
|
408
|
+
privateKeyDiscovered: targetConfig.privateKeyDiscovered,
|
|
409
|
+
agent
|
|
410
|
+
},
|
|
354
411
|
previousStream,
|
|
355
412
|
jumpHosts.length > 0 ? "target host" : void 0,
|
|
356
413
|
targetConfig.keepaliveInterval,
|
|
@@ -362,7 +419,7 @@ var SSHTunnel = class {
|
|
|
362
419
|
/**
|
|
363
420
|
* Connect to a single SSH host.
|
|
364
421
|
*/
|
|
365
|
-
connectToHost(hostInfo,
|
|
422
|
+
connectToHost(hostInfo, auth, sock, label, keepaliveInterval, keepaliveCountMax) {
|
|
366
423
|
return new Promise((resolve, reject) => {
|
|
367
424
|
const client = new Client();
|
|
368
425
|
const sshConfig = {
|
|
@@ -370,15 +427,28 @@ var SSHTunnel = class {
|
|
|
370
427
|
port: hostInfo.port,
|
|
371
428
|
username: hostInfo.username
|
|
372
429
|
};
|
|
430
|
+
const { password, privateKey, passphrase, agent } = auth;
|
|
373
431
|
if (password) {
|
|
374
432
|
sshConfig.password = password;
|
|
375
433
|
}
|
|
376
434
|
if (privateKey) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
435
|
+
const canSkip = auth.privateKeyDiscovered && Boolean(password || agent);
|
|
436
|
+
const unusableReason = canSkip ? this.unusableKeyReason(privateKey, passphrase) : void 0;
|
|
437
|
+
if (unusableReason) {
|
|
438
|
+
const desc = label || `${hostInfo.host}:${hostInfo.port}`;
|
|
439
|
+
console.warn(
|
|
440
|
+
`Skipping unusable SSH private key from ~/.ssh/config for ${desc} (${unusableReason}).`
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
sshConfig.privateKey = privateKey;
|
|
444
|
+
if (passphrase) {
|
|
445
|
+
sshConfig.passphrase = passphrase;
|
|
446
|
+
}
|
|
380
447
|
}
|
|
381
448
|
}
|
|
449
|
+
if (agent) {
|
|
450
|
+
sshConfig.agent = agent;
|
|
451
|
+
}
|
|
382
452
|
if (sock) {
|
|
383
453
|
sshConfig.sock = sock;
|
|
384
454
|
}
|
|
@@ -887,12 +957,14 @@ function resolveSSHConfig() {
|
|
|
887
957
|
}
|
|
888
958
|
if (args["ssh-key"]) {
|
|
889
959
|
config.privateKey = args["ssh-key"];
|
|
960
|
+
config.privateKeyDiscovered = false;
|
|
890
961
|
if (config.privateKey.startsWith("~/")) {
|
|
891
962
|
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
892
963
|
}
|
|
893
964
|
sources.push("ssh-key from command line");
|
|
894
965
|
} else if (process.env.SSH_KEY) {
|
|
895
966
|
config.privateKey = process.env.SSH_KEY;
|
|
967
|
+
config.privateKeyDiscovered = false;
|
|
896
968
|
if (config.privateKey.startsWith("~/")) {
|
|
897
969
|
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
898
970
|
}
|
|
@@ -905,6 +977,14 @@ function resolveSSHConfig() {
|
|
|
905
977
|
config.passphrase = process.env.SSH_PASSPHRASE;
|
|
906
978
|
sources.push("SSH_PASSPHRASE from environment");
|
|
907
979
|
}
|
|
980
|
+
const cliAgent = requireFlagValue("ssh-agent", args, "~/.ssh/agent.sock");
|
|
981
|
+
if (cliAgent) {
|
|
982
|
+
config.agent = cliAgent;
|
|
983
|
+
if (config.agent.startsWith("~/")) {
|
|
984
|
+
config.agent = path.join(process.env.HOME || "", config.agent.substring(2));
|
|
985
|
+
}
|
|
986
|
+
sources.push("ssh-agent from command line");
|
|
987
|
+
}
|
|
908
988
|
if (args["ssh-proxy-jump"]) {
|
|
909
989
|
config.proxyJump = args["ssh-proxy-jump"];
|
|
910
990
|
sources.push("ssh-proxy-jump from command line");
|
|
@@ -936,8 +1016,8 @@ function resolveSSHConfig() {
|
|
|
936
1016
|
if (!config.host || !config.username) {
|
|
937
1017
|
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
938
1018
|
}
|
|
939
|
-
if (!config.password && !config.privateKey) {
|
|
940
|
-
throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
|
|
1019
|
+
if (!config.password && !config.privateKey && !config.agent && !process.env.SSH_AUTH_SOCK) {
|
|
1020
|
+
throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key (or an SSH agent via --ssh-agent or SSH_AUTH_SOCK) for authentication");
|
|
941
1021
|
}
|
|
942
1022
|
return {
|
|
943
1023
|
config,
|
|
@@ -1021,7 +1101,9 @@ async function resolveSourceConfigs() {
|
|
|
1021
1101
|
source.ssh_user = sshResult.config.username;
|
|
1022
1102
|
source.ssh_password = sshResult.config.password;
|
|
1023
1103
|
source.ssh_key = sshResult.config.privateKey;
|
|
1104
|
+
source.ssh_key_discovered = sshResult.config.privateKeyDiscovered;
|
|
1024
1105
|
source.ssh_passphrase = sshResult.config.passphrase;
|
|
1106
|
+
source.ssh_agent = sshResult.config.agent;
|
|
1025
1107
|
source.ssh_keepalive_interval = sshResult.config.keepaliveInterval;
|
|
1026
1108
|
source.ssh_keepalive_count_max = sshResult.config.keepaliveCountMax;
|
|
1027
1109
|
}
|
|
@@ -1189,6 +1271,9 @@ function getRawDSNQueryParam(dsn, key) {
|
|
|
1189
1271
|
if (queryStart === -1) {
|
|
1190
1272
|
return null;
|
|
1191
1273
|
}
|
|
1274
|
+
if (key === "sslmode" && dsn.startsWith("sqlserver://")) {
|
|
1275
|
+
return new URLSearchParams(dsn.substring(queryStart + 1)).get(key);
|
|
1276
|
+
}
|
|
1192
1277
|
for (const pair of dsn.substring(queryStart + 1).split("&")) {
|
|
1193
1278
|
if (pair === "") {
|
|
1194
1279
|
continue;
|
|
@@ -1206,6 +1291,30 @@ function getRawDSNQueryParam(dsn, key) {
|
|
|
1206
1291
|
}
|
|
1207
1292
|
return null;
|
|
1208
1293
|
}
|
|
1294
|
+
var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
|
|
1295
|
+
function validateReadableFile(source, field, filePath, configPath) {
|
|
1296
|
+
const expandedPath = expandHomeDir(filePath);
|
|
1297
|
+
let stats;
|
|
1298
|
+
try {
|
|
1299
|
+
stats = fs2.statSync(expandedPath);
|
|
1300
|
+
} catch {
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`Configuration file ${configPath}: source '${source.id}' ${field} file not found or not accessible: '${expandedPath}'`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (!stats.isFile()) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`Configuration file ${configPath}: source '${source.id}' ${field} path is not a regular file: '${expandedPath}'`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
fs2.accessSync(expandedPath, fs2.constants.R_OK);
|
|
1312
|
+
} catch {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`Configuration file ${configPath}: source '${source.id}' ${field} file is not readable: '${expandedPath}'`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1209
1318
|
function validateDSNFieldConflicts(source, configPath) {
|
|
1210
1319
|
const conflict = (field, fieldValue, dsnValue) => {
|
|
1211
1320
|
throw new Error(
|
|
@@ -1256,12 +1365,22 @@ function validateDSNFieldConflicts(source, configPath) {
|
|
|
1256
1365
|
}
|
|
1257
1366
|
const dsnSslmode = getRawDSNQueryParam(source.dsn, "sslmode");
|
|
1258
1367
|
if (source.sslmode && dsnSslmode !== null && dsnSslmode !== source.sslmode) {
|
|
1368
|
+
if (source.type === "sqlserver") {
|
|
1369
|
+
throw new Error("Conflicting SQL Server sslmode. Set sslmode in only one place, or make the two values match.");
|
|
1370
|
+
}
|
|
1259
1371
|
conflict("sslmode", source.sslmode, dsnSslmode);
|
|
1260
1372
|
}
|
|
1261
1373
|
const dsnSslrootcert = getRawDSNQueryParam(source.dsn, "sslrootcert");
|
|
1262
1374
|
if (source.sslrootcert && dsnSslrootcert !== null && expandHomeDir(source.sslrootcert) !== expandHomeDir(dsnSslrootcert)) {
|
|
1263
1375
|
conflict("sslrootcert", expandHomeDir(source.sslrootcert), expandHomeDir(dsnSslrootcert));
|
|
1264
1376
|
}
|
|
1377
|
+
for (const field of ["sslcert", "sslkey"]) {
|
|
1378
|
+
const fieldValue = source[field];
|
|
1379
|
+
const dsnValue = getRawDSNQueryParam(source.dsn, field);
|
|
1380
|
+
if (fieldValue && dsnValue !== null && expandHomeDir(fieldValue) !== expandHomeDir(dsnValue)) {
|
|
1381
|
+
conflict(field, expandHomeDir(fieldValue), expandHomeDir(dsnValue));
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1265
1384
|
const dsnInstanceName = getRawDSNQueryParam(source.dsn, "instanceName");
|
|
1266
1385
|
if (source.instanceName && dsnInstanceName !== null && dsnInstanceName !== source.instanceName) {
|
|
1267
1386
|
conflict("instanceName", source.instanceName, dsnInstanceName);
|
|
@@ -1338,9 +1457,9 @@ function validateSourceConfig(source, configPath) {
|
|
|
1338
1457
|
}
|
|
1339
1458
|
}
|
|
1340
1459
|
if (source.query_timeout !== void 0) {
|
|
1341
|
-
if (
|
|
1460
|
+
if (!Number.isFinite(source.query_timeout) || source.query_timeout <= 0 || source.query_timeout > MAX_QUERY_TIMEOUT_SECONDS) {
|
|
1342
1461
|
throw new Error(
|
|
1343
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid query_timeout. Must be a positive number
|
|
1462
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid query_timeout. Must be a positive number of seconds, at most ${MAX_QUERY_TIMEOUT_SECONDS}.`
|
|
1344
1463
|
);
|
|
1345
1464
|
}
|
|
1346
1465
|
}
|
|
@@ -1363,6 +1482,13 @@ function validateSourceConfig(source, configPath) {
|
|
|
1363
1482
|
);
|
|
1364
1483
|
}
|
|
1365
1484
|
}
|
|
1485
|
+
if (source.ssh_agent !== void 0) {
|
|
1486
|
+
if (typeof source.ssh_agent !== "string" || source.ssh_agent.trim() === "") {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid ssh_agent. Must be a path to an SSH agent socket.`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1366
1492
|
if (source.sslmode !== void 0) {
|
|
1367
1493
|
if (source.type === "sqlite") {
|
|
1368
1494
|
throw new Error(
|
|
@@ -1377,6 +1503,7 @@ function validateSourceConfig(source, configPath) {
|
|
|
1377
1503
|
}
|
|
1378
1504
|
const verifyModesByType = {
|
|
1379
1505
|
postgres: ["verify-ca", "verify-full"],
|
|
1506
|
+
sqlserver: ["verify-full"],
|
|
1380
1507
|
oracle: ["verify-full"]
|
|
1381
1508
|
};
|
|
1382
1509
|
if ((source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !(verifyModesByType[source.type] ?? []).includes(source.sslmode)) {
|
|
@@ -1400,27 +1527,26 @@ function validateSourceConfig(source, configPath) {
|
|
|
1400
1527
|
`Configuration file ${configPath}: source '${source.id}' has sslrootcert but sslmode is '${source.sslmode ?? "not set"}'. sslrootcert requires sslmode 'verify-ca' or 'verify-full'`
|
|
1401
1528
|
);
|
|
1402
1529
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
} catch {
|
|
1530
|
+
validateReadableFile(source, "sslrootcert", source.sslrootcert, configPath);
|
|
1531
|
+
}
|
|
1532
|
+
if (source.sslcert !== void 0 || source.sslkey !== void 0) {
|
|
1533
|
+
if (source.type !== "postgres") {
|
|
1408
1534
|
throw new Error(
|
|
1409
|
-
`Configuration file ${configPath}: source '${source.id}'
|
|
1535
|
+
`Configuration file ${configPath}: source '${source.id}' has sslcert/sslkey but they are only supported for PostgreSQL.`
|
|
1410
1536
|
);
|
|
1411
1537
|
}
|
|
1412
|
-
if (
|
|
1538
|
+
if (source.sslcert === void 0 || source.sslkey === void 0) {
|
|
1413
1539
|
throw new Error(
|
|
1414
|
-
`Configuration file ${configPath}: source '${source.id}'
|
|
1540
|
+
`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
1541
|
);
|
|
1416
1542
|
}
|
|
1417
|
-
|
|
1418
|
-
fs2.accessSync(expandedPath, fs2.constants.R_OK);
|
|
1419
|
-
} catch {
|
|
1543
|
+
if (!CLIENT_CERT_SSL_MODES.includes(source.sslmode ?? "")) {
|
|
1420
1544
|
throw new Error(
|
|
1421
|
-
`Configuration file ${configPath}: source '${source.id}'
|
|
1545
|
+
`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
1546
|
);
|
|
1423
1547
|
}
|
|
1548
|
+
validateReadableFile(source, "sslcert", source.sslcert, configPath);
|
|
1549
|
+
validateReadableFile(source, "sslkey", source.sslkey, configPath);
|
|
1424
1550
|
}
|
|
1425
1551
|
if (source.authentication !== void 0) {
|
|
1426
1552
|
if (source.type !== "sqlserver") {
|
|
@@ -1536,8 +1662,14 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1536
1662
|
if (processed.ssh_key) {
|
|
1537
1663
|
processed.ssh_key = expandHomeDir(processed.ssh_key);
|
|
1538
1664
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1665
|
+
delete processed.ssh_key_discovered;
|
|
1666
|
+
if (typeof processed.ssh_agent === "string" && processed.ssh_agent) {
|
|
1667
|
+
processed.ssh_agent = expandHomeDir(processed.ssh_agent);
|
|
1668
|
+
}
|
|
1669
|
+
for (const field of ["sslrootcert", "sslcert", "sslkey"]) {
|
|
1670
|
+
if (processed[field]) {
|
|
1671
|
+
processed[field] = expandHomeDir(processed[field]);
|
|
1672
|
+
}
|
|
1541
1673
|
}
|
|
1542
1674
|
if (processed.type === "sqlite" && processed.database) {
|
|
1543
1675
|
processed.database = resolveSqliteDatabasePath(processed.database, configPath);
|
|
@@ -1567,12 +1699,14 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1567
1699
|
try {
|
|
1568
1700
|
const url = new SafeURL(processed.dsn);
|
|
1569
1701
|
const dsnSslmode = url.getSearchParam("sslmode");
|
|
1570
|
-
if (
|
|
1702
|
+
if (processed.sslmode === void 0 && dsnSslmode) {
|
|
1571
1703
|
processed.sslmode = dsnSslmode;
|
|
1572
1704
|
}
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
processed
|
|
1705
|
+
for (const field of ["sslrootcert", "sslcert", "sslkey"]) {
|
|
1706
|
+
const dsnValue = url.getSearchParam(field);
|
|
1707
|
+
if (!processed[field] && dsnValue) {
|
|
1708
|
+
processed[field] = dsnValue;
|
|
1709
|
+
}
|
|
1576
1710
|
}
|
|
1577
1711
|
} catch {
|
|
1578
1712
|
}
|
|
@@ -1631,10 +1765,7 @@ function mergeSourceFieldsIntoDSN(dsn, source) {
|
|
|
1631
1765
|
if (source.sslmode && !hasParam("sslmode")) {
|
|
1632
1766
|
additions.push(`sslmode=${source.sslmode}`);
|
|
1633
1767
|
}
|
|
1634
|
-
|
|
1635
|
-
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1636
|
-
additions.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1637
|
-
}
|
|
1768
|
+
additions.push(...postgresSslFileParams(source).filter(({ key }) => !hasParam(key)).map(({ param }) => param));
|
|
1638
1769
|
if (additions.length === 0) {
|
|
1639
1770
|
return dsn;
|
|
1640
1771
|
}
|
|
@@ -1648,6 +1779,23 @@ function mergeSourceFieldsIntoDSN(dsn, source) {
|
|
|
1648
1779
|
}
|
|
1649
1780
|
return `${dsn}${separator}${additions.join("&")}`;
|
|
1650
1781
|
}
|
|
1782
|
+
function postgresSslFileParams(source) {
|
|
1783
|
+
if (source.type !== "postgres") {
|
|
1784
|
+
return [];
|
|
1785
|
+
}
|
|
1786
|
+
const params = [];
|
|
1787
|
+
const push = (key) => {
|
|
1788
|
+
params.push({ key, param: `${key}=${encodeURIComponent(expandHomeDir(source[key]))}` });
|
|
1789
|
+
};
|
|
1790
|
+
if (source.sslrootcert && (source.sslmode === "verify-ca" || source.sslmode === "verify-full")) {
|
|
1791
|
+
push("sslrootcert");
|
|
1792
|
+
}
|
|
1793
|
+
if (source.sslcert && source.sslkey && CLIENT_CERT_SSL_MODES.includes(source.sslmode ?? "")) {
|
|
1794
|
+
push("sslcert");
|
|
1795
|
+
push("sslkey");
|
|
1796
|
+
}
|
|
1797
|
+
return params;
|
|
1798
|
+
}
|
|
1651
1799
|
function buildDSNFromSource(source) {
|
|
1652
1800
|
if (source.dsn) {
|
|
1653
1801
|
return mergeSourceFieldsIntoDSN(source.dsn, source);
|
|
@@ -1700,10 +1848,7 @@ function buildDSNFromSource(source) {
|
|
|
1700
1848
|
if (source.sslmode && source.type !== "sqlite") {
|
|
1701
1849
|
queryParams.push(`sslmode=${source.sslmode}`);
|
|
1702
1850
|
}
|
|
1703
|
-
|
|
1704
|
-
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1705
|
-
queryParams.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1706
|
-
}
|
|
1851
|
+
queryParams.push(...postgresSslFileParams(source).map(({ param }) => param));
|
|
1707
1852
|
if (queryParams.length > 0) {
|
|
1708
1853
|
dsn += `?${queryParams.join("&")}`;
|
|
1709
1854
|
}
|
|
@@ -1792,7 +1937,6 @@ function classifyConnectionError(error, connectorType, sourceId) {
|
|
|
1792
1937
|
var managerInstance = null;
|
|
1793
1938
|
var AWS_IAM_TOKEN_REFRESH_MS = 14 * 60 * 1e3;
|
|
1794
1939
|
var ConnectorManager = class {
|
|
1795
|
-
// Prevent race conditions
|
|
1796
1940
|
constructor() {
|
|
1797
1941
|
// Maps for multi-source support
|
|
1798
1942
|
this.connectors = /* @__PURE__ */ new Map();
|
|
@@ -1802,11 +1946,17 @@ var ConnectorManager = class {
|
|
|
1802
1946
|
this.sourceIds = [];
|
|
1803
1947
|
// Ordered list of source IDs (first is default)
|
|
1804
1948
|
this.iamRefreshTimers = /* @__PURE__ */ new Map();
|
|
1949
|
+
this.pendingIamRefreshes = /* @__PURE__ */ new Map();
|
|
1950
|
+
// In-flight refresh per source
|
|
1805
1951
|
this.isDisconnecting = false;
|
|
1806
1952
|
// Lazy connection support
|
|
1807
1953
|
this.lazySources = /* @__PURE__ */ new Map();
|
|
1808
1954
|
// Sources pending lazy connection
|
|
1809
1955
|
this.pendingConnections = /* @__PURE__ */ new Map();
|
|
1956
|
+
// Prevent race conditions
|
|
1957
|
+
// A socket timeout does not cancel a credential helper. Share its in-flight
|
|
1958
|
+
// attempt across retries of the same config, but never across changed profiles.
|
|
1959
|
+
this.pendingIamTokens = /* @__PURE__ */ new WeakMap();
|
|
1810
1960
|
if (!managerInstance) {
|
|
1811
1961
|
managerInstance = this;
|
|
1812
1962
|
}
|
|
@@ -1892,7 +2042,8 @@ var ConnectorManager = class {
|
|
|
1892
2042
|
*/
|
|
1893
2043
|
async connectSource(source) {
|
|
1894
2044
|
const sourceId = source.id;
|
|
1895
|
-
const
|
|
2045
|
+
const config = {};
|
|
2046
|
+
const dsn = await this.buildConnectionDSN(source, config);
|
|
1896
2047
|
console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
|
|
1897
2048
|
let actualDSN = dsn;
|
|
1898
2049
|
let tunnel;
|
|
@@ -1920,7 +2071,9 @@ var ConnectorManager = class {
|
|
|
1920
2071
|
username: username || "",
|
|
1921
2072
|
password: source.ssh_password,
|
|
1922
2073
|
privateKey: source.ssh_key || resolvedSSHConfig?.privateKey,
|
|
2074
|
+
privateKeyDiscovered: source.ssh_key ? source.ssh_key_discovered : resolvedSSHConfig?.privateKeyDiscovered,
|
|
1923
2075
|
passphrase: source.ssh_passphrase,
|
|
2076
|
+
agent: source.ssh_agent,
|
|
1924
2077
|
proxyJump,
|
|
1925
2078
|
resolvedJumpHosts,
|
|
1926
2079
|
keepaliveInterval: source.ssh_keepalive_interval,
|
|
@@ -1931,9 +2084,9 @@ var ConnectorManager = class {
|
|
|
1931
2084
|
`Source '${sourceId}': SSH tunnel requires ssh_user (or a matching Host entry in ~/.ssh/config with User)`
|
|
1932
2085
|
);
|
|
1933
2086
|
}
|
|
1934
|
-
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
2087
|
+
if (!sshConfig.password && !sshConfig.privateKey && !sshConfig.agent && !process.env.SSH_AUTH_SOCK) {
|
|
1935
2088
|
throw new Error(
|
|
1936
|
-
`Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key (or a matching Host entry in ~/.ssh/config with IdentityFile)`
|
|
2089
|
+
`Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key (or a matching Host entry in ~/.ssh/config with IdentityFile, or an SSH agent via ssh_agent or SSH_AUTH_SOCK)`
|
|
1937
2090
|
);
|
|
1938
2091
|
}
|
|
1939
2092
|
const url = new URL(dsn);
|
|
@@ -1960,37 +2113,37 @@ var ConnectorManager = class {
|
|
|
1960
2113
|
` SSH tunnel established through localhost:${tunnelInfo.localPort}`
|
|
1961
2114
|
);
|
|
1962
2115
|
}
|
|
1963
|
-
|
|
1964
|
-
if (!connectorPrototype) {
|
|
1965
|
-
throw new Error(
|
|
1966
|
-
`Source '${sourceId}': No connector found for DSN: ${actualDSN}`
|
|
1967
|
-
);
|
|
1968
|
-
}
|
|
1969
|
-
const connector = connectorPrototype.clone();
|
|
1970
|
-
connector.sourceId = sourceId;
|
|
1971
|
-
const config = {};
|
|
1972
|
-
if (source.connection_timeout !== void 0) {
|
|
1973
|
-
config.connectionTimeoutSeconds = source.connection_timeout;
|
|
1974
|
-
}
|
|
1975
|
-
if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
|
|
1976
|
-
config.queryTimeoutSeconds = source.query_timeout;
|
|
1977
|
-
}
|
|
1978
|
-
if (source.pool_max_connections !== void 0) {
|
|
1979
|
-
config.poolMaxConnections = source.pool_max_connections;
|
|
1980
|
-
}
|
|
1981
|
-
if (source.search_path) {
|
|
1982
|
-
config.searchPath = source.search_path;
|
|
1983
|
-
}
|
|
1984
|
-
if (source.timezone) {
|
|
1985
|
-
config.timezone = source.timezone;
|
|
1986
|
-
}
|
|
1987
|
-
if (source.charset) {
|
|
1988
|
-
config.charset = source.charset;
|
|
1989
|
-
}
|
|
1990
|
-
if (source.collation) {
|
|
1991
|
-
config.collation = source.collation;
|
|
1992
|
-
}
|
|
2116
|
+
let connector;
|
|
1993
2117
|
try {
|
|
2118
|
+
const connectorPrototype = ConnectorRegistry.getConnectorForDSN(actualDSN);
|
|
2119
|
+
if (!connectorPrototype) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`Source '${sourceId}': No connector found for DSN: ${actualDSN}`
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
connector = connectorPrototype.clone();
|
|
2125
|
+
connector.sourceId = sourceId;
|
|
2126
|
+
if (source.connection_timeout !== void 0) {
|
|
2127
|
+
config.connectionTimeoutSeconds = source.connection_timeout;
|
|
2128
|
+
}
|
|
2129
|
+
if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
|
|
2130
|
+
config.queryTimeoutSeconds = source.query_timeout;
|
|
2131
|
+
}
|
|
2132
|
+
if (source.pool_max_connections !== void 0) {
|
|
2133
|
+
config.poolMaxConnections = source.pool_max_connections;
|
|
2134
|
+
}
|
|
2135
|
+
if (source.search_path) {
|
|
2136
|
+
config.searchPath = source.search_path;
|
|
2137
|
+
}
|
|
2138
|
+
if (source.timezone) {
|
|
2139
|
+
config.timezone = source.timezone;
|
|
2140
|
+
}
|
|
2141
|
+
if (source.charset) {
|
|
2142
|
+
config.charset = source.charset;
|
|
2143
|
+
}
|
|
2144
|
+
if (source.collation) {
|
|
2145
|
+
config.collation = source.collation;
|
|
2146
|
+
}
|
|
1994
2147
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1995
2148
|
} catch (error) {
|
|
1996
2149
|
if (tunnel) {
|
|
@@ -2010,6 +2163,77 @@ var ConnectorManager = class {
|
|
|
2010
2163
|
this.sourceConfigs.set(sourceId, source);
|
|
2011
2164
|
this.scheduleIamRefresh(source);
|
|
2012
2165
|
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Add a single source without touching the others. Eager sources connect now;
|
|
2168
|
+
* lazy ones are registered and connect on first use. Used by the TOML hot reload
|
|
2169
|
+
* to apply only the entries that changed.
|
|
2170
|
+
*/
|
|
2171
|
+
async addSource(source) {
|
|
2172
|
+
if (this.sourceIds.includes(source.id)) {
|
|
2173
|
+
throw new Error(`Source '${source.id}' already exists`);
|
|
2174
|
+
}
|
|
2175
|
+
if (source.lazy) {
|
|
2176
|
+
this.registerLazySource(source);
|
|
2177
|
+
} else {
|
|
2178
|
+
await this.connectSource(source);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Disconnect and forget a single source, leaving every other source's pool and
|
|
2183
|
+
* tunnel untouched. Resolves silently for an unknown id.
|
|
2184
|
+
*/
|
|
2185
|
+
async removeSource(sourceId) {
|
|
2186
|
+
const pending = this.pendingConnections.get(sourceId);
|
|
2187
|
+
if (pending) {
|
|
2188
|
+
try {
|
|
2189
|
+
await pending;
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
const refresh = this.pendingIamRefreshes.get(sourceId);
|
|
2194
|
+
if (refresh) {
|
|
2195
|
+
await refresh;
|
|
2196
|
+
}
|
|
2197
|
+
const timer = this.iamRefreshTimers.get(sourceId);
|
|
2198
|
+
if (timer) {
|
|
2199
|
+
clearTimeout(timer);
|
|
2200
|
+
this.iamRefreshTimers.delete(sourceId);
|
|
2201
|
+
}
|
|
2202
|
+
const connector = this.connectors.get(sourceId);
|
|
2203
|
+
this.connectors.delete(sourceId);
|
|
2204
|
+
if (connector) {
|
|
2205
|
+
try {
|
|
2206
|
+
await connector.disconnect();
|
|
2207
|
+
console.error(`Disconnected from source '${sourceId}'`);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
console.error(`Error disconnecting from source '${sourceId}':`, error);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const tunnel = this.sshTunnels.get(sourceId);
|
|
2213
|
+
this.sshTunnels.delete(sourceId);
|
|
2214
|
+
if (tunnel) {
|
|
2215
|
+
try {
|
|
2216
|
+
await tunnel.close();
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
console.error(`Error closing SSH tunnel for source '${sourceId}':`, error);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
this.sourceConfigs.delete(sourceId);
|
|
2222
|
+
this.lazySources.delete(sourceId);
|
|
2223
|
+
this.pendingConnections.delete(sourceId);
|
|
2224
|
+
this.sourceIds = this.sourceIds.filter((id) => id !== sourceId);
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Reorder known sources to match `orderedIds` (the first entry is the default
|
|
2228
|
+
* source). Unknown ids are ignored; known ids missing from the list keep their
|
|
2229
|
+
* relative order after the listed ones.
|
|
2230
|
+
*/
|
|
2231
|
+
reorderSources(orderedIds) {
|
|
2232
|
+
const known = new Set(this.sourceIds);
|
|
2233
|
+
const ordered = orderedIds.filter((id) => known.has(id));
|
|
2234
|
+
const listed = new Set(ordered);
|
|
2235
|
+
this.sourceIds = [...ordered, ...this.sourceIds.filter((id) => !listed.has(id))];
|
|
2236
|
+
}
|
|
2013
2237
|
/**
|
|
2014
2238
|
* Close all database connections
|
|
2015
2239
|
*/
|
|
@@ -2039,6 +2263,7 @@ var ConnectorManager = class {
|
|
|
2039
2263
|
this.sourceConfigs.clear();
|
|
2040
2264
|
this.lazySources.clear();
|
|
2041
2265
|
this.pendingConnections.clear();
|
|
2266
|
+
this.pendingIamRefreshes.clear();
|
|
2042
2267
|
this.sourceIds = [];
|
|
2043
2268
|
this.isDisconnecting = false;
|
|
2044
2269
|
}
|
|
@@ -2159,25 +2384,29 @@ var ConnectorManager = class {
|
|
|
2159
2384
|
clearTimeout(existingTimer);
|
|
2160
2385
|
this.iamRefreshTimers.delete(sourceId);
|
|
2161
2386
|
}
|
|
2162
|
-
if (!source.aws_iam_auth) {
|
|
2387
|
+
if (!source.aws_iam_auth || source.type === "postgres") {
|
|
2163
2388
|
return;
|
|
2164
2389
|
}
|
|
2165
|
-
const timer = setTimeout(
|
|
2390
|
+
const timer = setTimeout(() => {
|
|
2166
2391
|
if (this.isDisconnecting) {
|
|
2167
2392
|
return;
|
|
2168
2393
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
this.
|
|
2394
|
+
const run = (async () => {
|
|
2395
|
+
try {
|
|
2396
|
+
await this.refreshIamSourceConnection(source);
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
console.error(
|
|
2399
|
+
`Error refreshing AWS IAM auth token for source '${sourceId}':`,
|
|
2400
|
+
error
|
|
2401
|
+
);
|
|
2402
|
+
} finally {
|
|
2403
|
+
this.pendingIamRefreshes.delete(sourceId);
|
|
2404
|
+
if (!this.isDisconnecting && this.ownsSource(source) && this.connectors.has(sourceId)) {
|
|
2405
|
+
this.scheduleIamRefresh(source);
|
|
2406
|
+
}
|
|
2179
2407
|
}
|
|
2180
|
-
}
|
|
2408
|
+
})();
|
|
2409
|
+
this.pendingIamRefreshes.set(sourceId, run);
|
|
2181
2410
|
}, AWS_IAM_TOKEN_REFRESH_MS);
|
|
2182
2411
|
timer.unref?.();
|
|
2183
2412
|
this.iamRefreshTimers.set(sourceId, timer);
|
|
@@ -2198,23 +2427,31 @@ var ConnectorManager = class {
|
|
|
2198
2427
|
await existingTunnel.close();
|
|
2199
2428
|
this.sshTunnels.delete(sourceId);
|
|
2200
2429
|
}
|
|
2201
|
-
if (this.isDisconnecting) {
|
|
2430
|
+
if (this.isDisconnecting || !this.ownsSource(source)) {
|
|
2202
2431
|
return;
|
|
2203
2432
|
}
|
|
2204
2433
|
try {
|
|
2205
2434
|
await this.connectSource(source);
|
|
2206
2435
|
} catch (error) {
|
|
2207
|
-
if (!this.isDisconnecting && this.
|
|
2436
|
+
if (!this.isDisconnecting && this.ownsSource(source)) {
|
|
2208
2437
|
this.lazySources.set(sourceId, source);
|
|
2209
2438
|
}
|
|
2210
2439
|
throw error;
|
|
2211
2440
|
}
|
|
2212
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* True while `source` is the config object registered under its id. Every
|
|
2444
|
+
* registration path stores the same object, so identity tells an in-flight
|
|
2445
|
+
* operation whether its source was removed or replaced underneath it.
|
|
2446
|
+
*/
|
|
2447
|
+
ownsSource(source) {
|
|
2448
|
+
return this.sourceConfigs.get(source.id) === source;
|
|
2449
|
+
}
|
|
2213
2450
|
/**
|
|
2214
2451
|
* Build a connection DSN, optionally replacing password with
|
|
2215
2452
|
* an AWS RDS IAM auth token when aws_iam_auth is enabled.
|
|
2216
2453
|
*/
|
|
2217
|
-
async buildConnectionDSN(source) {
|
|
2454
|
+
async buildConnectionDSN(source, config = {}) {
|
|
2218
2455
|
const dsn = buildDSNFromSource(source);
|
|
2219
2456
|
if (!source.aws_iam_auth) {
|
|
2220
2457
|
return dsn;
|
|
@@ -2240,13 +2477,26 @@ var ConnectorManager = class {
|
|
|
2240
2477
|
`Source '${source.id}': unable to resolve host, username, or port for AWS IAM authentication`
|
|
2241
2478
|
);
|
|
2242
2479
|
}
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2480
|
+
const password = () => {
|
|
2481
|
+
let pending = this.pendingIamTokens.get(source);
|
|
2482
|
+
if (!pending) {
|
|
2483
|
+
pending = generateRdsAuthToken({
|
|
2484
|
+
hostname,
|
|
2485
|
+
port,
|
|
2486
|
+
username,
|
|
2487
|
+
region: source.aws_region,
|
|
2488
|
+
profile: source.aws_profile
|
|
2489
|
+
}).finally(() => {
|
|
2490
|
+
this.pendingIamTokens.delete(source);
|
|
2491
|
+
});
|
|
2492
|
+
this.pendingIamTokens.set(source, pending);
|
|
2493
|
+
}
|
|
2494
|
+
return pending;
|
|
2495
|
+
};
|
|
2496
|
+
if (source.type === "postgres") {
|
|
2497
|
+
config.password = password;
|
|
2498
|
+
}
|
|
2499
|
+
const token = source.type === "postgres" ? "" : await password();
|
|
2250
2500
|
const queryParams = new Map(parsed.searchParams);
|
|
2251
2501
|
const currentSslMode = queryParams.get("sslmode");
|
|
2252
2502
|
if (currentSslMode !== "verify-ca" && currentSslMode !== "verify-full") {
|