@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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ CANCEL_QUERY_TIMEOUT_MS,
3
+ clientQueryTimeoutMs,
4
+ isClientSideTimeout
5
+ } from "./chunk-5XSBOK3W.js";
1
6
  import {
2
7
  toNullableNumber
3
8
  } from "./chunk-FU2ZJE4E.js";
@@ -7,8 +12,8 @@ import {
7
12
  import {
8
13
  SQLRowLimiter,
9
14
  closeQuietly
10
- } from "./chunk-YIGJHEU6.js";
11
- import "./chunk-ABTVMHD6.js";
15
+ } from "./chunk-X5OCFRNL.js";
16
+ import "./chunk-55X2VY5D.js";
12
17
  import {
13
18
  ConnectorRegistry,
14
19
  SafeURL,
@@ -17,10 +22,7 @@ import {
17
22
  } from "./chunk-IXC4NQGC.js";
18
23
 
19
24
  // src/connectors/postgres/index.ts
20
- import fs from "fs";
21
- import os from "os";
22
- import path from "path";
23
- import pg2 from "pg";
25
+ import pg3 from "pg";
24
26
 
25
27
  // src/connectors/postgres/failed-to-read-certificate.ts
26
28
  var FailedToReadCertificate = class extends Error {
@@ -56,9 +58,83 @@ var postgresTypeParsers = {
56
58
  }
57
59
  };
58
60
 
61
+ // src/connectors/postgres/ssl-pems.ts
62
+ import fs from "fs";
63
+ import os from "os";
64
+ import path from "path";
65
+ import pg2 from "pg";
66
+ var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
67
+ var SUPPORTED_SSL_MODES = ["disable", ...CLIENT_CERT_SSL_MODES];
68
+ var SSL_ROOT_CERT_LABEL = "SSL root certificate";
69
+ var SSL_CLIENT_CERT_LABEL = "SSL client certificate";
70
+ var SSL_CLIENT_KEY_LABEL = "SSL client key";
71
+ function resolvePemPath(filePath) {
72
+ return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
73
+ }
74
+ function failedToRead(label, resolved, err) {
75
+ return new FailedToReadCertificate(
76
+ `Failed to read ${label} at '${resolved}': ${err instanceof Error ? err.message : String(err)}`
77
+ );
78
+ }
79
+ async function readPemFile(filePath, label) {
80
+ const resolved = resolvePemPath(filePath);
81
+ try {
82
+ return await fs.promises.readFile(resolved, "utf-8");
83
+ } catch (err) {
84
+ throw failedToRead(label, resolved, err);
85
+ }
86
+ }
87
+ function readPemFileSync(filePath, label) {
88
+ const resolved = resolvePemPath(filePath);
89
+ try {
90
+ return fs.readFileSync(resolved, "utf-8");
91
+ } catch (err) {
92
+ throw failedToRead(label, resolved, err);
93
+ }
94
+ }
95
+ function isEncryptedPemKey(pem) {
96
+ return pem.includes("ENCRYPTED PRIVATE KEY") || pem.includes("Proc-Type: 4,ENCRYPTED");
97
+ }
98
+ function encryptedKeyMessage(keyPath) {
99
+ return `SSL client key at '${keyPath}' is encrypted; encrypted private keys are not supported. Decrypt it first, e.g. 'openssl pkey -in client.key -out client-plain.key'`;
100
+ }
101
+ function loadPems(paths) {
102
+ const pems = {};
103
+ if (paths.sslrootcert !== void 0) {
104
+ pems.ca = readPemFileSync(paths.sslrootcert, SSL_ROOT_CERT_LABEL);
105
+ }
106
+ if (paths.sslcert !== void 0 && paths.sslkey !== void 0) {
107
+ const key = readPemFileSync(paths.sslkey, SSL_CLIENT_KEY_LABEL);
108
+ if (isEncryptedPemKey(key)) {
109
+ throw new FailedToReadCertificate(encryptedKeyMessage(paths.sslkey));
110
+ }
111
+ pems.key = key;
112
+ pems.cert = readPemFileSync(paths.sslcert, SSL_CLIENT_CERT_LABEL);
113
+ }
114
+ return pems;
115
+ }
116
+ function rotatingPemClient(paths) {
117
+ return class RotatingPemClient extends pg2.Client {
118
+ connect(callback) {
119
+ let pems;
120
+ try {
121
+ pems = loadPems(paths);
122
+ } catch (err) {
123
+ const error = err instanceof Error ? err : new Error(String(err));
124
+ if (callback) {
125
+ callback(error);
126
+ return;
127
+ }
128
+ return Promise.reject(error);
129
+ }
130
+ Object.assign(this.ssl, pems);
131
+ return callback ? super.connect(callback) : super.connect();
132
+ }
133
+ };
134
+ }
135
+
59
136
  // src/connectors/postgres/index.ts
60
- var { Pool } = pg2;
61
- var POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS = 5e3;
137
+ var { Pool } = pg3;
62
138
  var PostgresDSNParser = class {
63
139
  async parse(dsn, config) {
64
140
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
@@ -81,17 +157,43 @@ Expected: ${expectedFormat}`
81
157
  database: url.pathname ? url.pathname.substring(1) : "",
82
158
  // Remove leading '/' if exists
83
159
  user: url.username,
84
- password: url.password
160
+ password: config?.password ?? url.password
85
161
  };
86
162
  let sslmode;
87
163
  let sslrootcert;
164
+ let sslcert;
165
+ let sslkey;
88
166
  url.forEachSearchParam((value, key) => {
89
167
  if (key === "sslmode") {
90
168
  sslmode = value;
91
169
  } else if (key === "sslrootcert") {
92
170
  sslrootcert = value;
171
+ } else if (key === "sslcert") {
172
+ sslcert = value;
173
+ } else if (key === "sslkey") {
174
+ sslkey = value;
93
175
  }
94
176
  });
177
+ if (sslmode !== void 0 && !SUPPORTED_SSL_MODES.includes(sslmode)) {
178
+ throw new Error(
179
+ `Unsupported sslmode '${sslmode}'. Valid values: ${SUPPORTED_SSL_MODES.join(", ")}`
180
+ );
181
+ }
182
+ if (sslrootcert !== void 0 && sslmode !== "verify-ca" && sslmode !== "verify-full") {
183
+ throw new FailedToReadCertificate(
184
+ `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.`
185
+ );
186
+ }
187
+ if (sslcert === void 0 !== (sslkey === void 0)) {
188
+ throw new FailedToReadCertificate(
189
+ "sslcert and sslkey must be set together for client certificate authentication"
190
+ );
191
+ }
192
+ if (sslcert !== void 0 && !CLIENT_CERT_SSL_MODES.includes(sslmode ?? "")) {
193
+ throw new FailedToReadCertificate(
194
+ `sslcert/sslkey require sslmode to be one of ${CLIENT_CERT_SSL_MODES.join(", ")} (got '${sslmode ?? "not set"}')`
195
+ );
196
+ }
95
197
  if (sslmode === "disable") {
96
198
  poolConfig.ssl = false;
97
199
  } else if (sslmode === "require") {
@@ -102,18 +204,22 @@ Expected: ${expectedFormat}`
102
204
  sslConfig.checkServerIdentity = () => void 0;
103
205
  }
104
206
  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
- }
207
+ sslConfig.ca = await readPemFile(sslrootcert, SSL_ROOT_CERT_LABEL);
113
208
  }
114
209
  poolConfig.ssl = sslConfig;
115
- } else if (sslmode !== void 0) {
116
- poolConfig.ssl = true;
210
+ }
211
+ if (sslcert !== void 0 && sslkey !== void 0) {
212
+ const key = await readPemFile(sslkey, SSL_CLIENT_KEY_LABEL);
213
+ if (isEncryptedPemKey(key)) {
214
+ throw new FailedToReadCertificate(encryptedKeyMessage(sslkey));
215
+ }
216
+ Object.assign(poolConfig.ssl, {
217
+ cert: await readPemFile(sslcert, SSL_CLIENT_CERT_LABEL),
218
+ key
219
+ });
220
+ }
221
+ if (sslrootcert !== void 0 || sslcert !== void 0) {
222
+ poolConfig.Client = rotatingPemClient({ sslrootcert, sslcert, sslkey });
117
223
  }
118
224
  if (connectionTimeoutSeconds !== void 0) {
119
225
  poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
@@ -121,7 +227,7 @@ Expected: ${expectedFormat}`
121
227
  if (queryTimeoutSeconds !== void 0) {
122
228
  const queryTimeoutMs = queryTimeoutSeconds * 1e3;
123
229
  poolConfig.statement_timeout = queryTimeoutMs;
124
- poolConfig.query_timeout = queryTimeoutMs + POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS;
230
+ poolConfig.query_timeout = clientQueryTimeoutMs(queryTimeoutMs);
125
231
  }
126
232
  if (poolMaxConnections !== void 0) {
127
233
  poolConfig.max = poolMaxConnections;
@@ -595,6 +701,8 @@ var PostgresConnector = class _PostgresConnector {
595
701
  throw new Error("Not connected to database");
596
702
  }
597
703
  const client = await this.pool.connect();
704
+ const backendPid = client.processID;
705
+ let isClientDiscarded = false;
598
706
  try {
599
707
  const statements = splitSQLStatements(sql, "postgres");
600
708
  if (statements.length === 1) {
@@ -616,10 +724,7 @@ var PostgresConnector = class _PostgresConnector {
616
724
  SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
617
725
  return { resultSets: [resultSet2] };
618
726
  } catch (error) {
619
- try {
620
- await client.query("ROLLBACK");
621
- } catch {
622
- }
727
+ await this.rollbackQuietly(client, error);
623
728
  throw error;
624
729
  }
625
730
  }
@@ -660,16 +765,61 @@ var PostgresConnector = class _PostgresConnector {
660
765
  }
661
766
  await client.query("COMMIT");
662
767
  } catch (error) {
663
- try {
664
- await client.query("ROLLBACK");
665
- } catch {
666
- }
768
+ await this.rollbackQuietly(client, error);
667
769
  throw error;
668
770
  }
669
771
  return { resultSets };
670
772
  }
773
+ } catch (error) {
774
+ if (isClientSideTimeout(error)) {
775
+ isClientDiscarded = true;
776
+ client.release(true);
777
+ await this.cancelBackend(backendPid);
778
+ }
779
+ throw error;
671
780
  } finally {
672
- client.release();
781
+ if (!isClientDiscarded) {
782
+ client.release();
783
+ }
784
+ }
785
+ }
786
+ /**
787
+ * Best-effort rollback after a failed statement, so a failed ROLLBACK (e.g.
788
+ * a dropped connection) cannot mask the original error. Skipped for a
789
+ * client-side timeout: the client is still busy with the abandoned
790
+ * statement, so the ROLLBACK would queue behind it (see isClientSideTimeout).
791
+ * executeSQL discards that client instead.
792
+ */
793
+ async rollbackQuietly(client, error) {
794
+ if (isClientSideTimeout(error)) {
795
+ return;
796
+ }
797
+ try {
798
+ await client.query("ROLLBACK");
799
+ } catch {
800
+ }
801
+ }
802
+ /**
803
+ * Best-effort server-side cancel for a statement abandoned by the
804
+ * client-side timeout, sent over a separate pooled connection. Closing the
805
+ * abandoned client's socket is not enough: PostgreSQL only notices a
806
+ * vanished client when it next writes to it, so a statement that is
807
+ * sleeping or computing would run to completion.
808
+ *
809
+ * Bounded by its own short timeout (CANCEL_QUERY_TIMEOUT_MS), independent
810
+ * of the user's query_timeout. pool.query discards its client on error, so
811
+ * a cancel that itself times out does not leak a stuck client.
812
+ */
813
+ async cancelBackend(backendPid) {
814
+ if (!this.pool || backendPid === null) return;
815
+ try {
816
+ const cancel = {
817
+ text: "SELECT pg_cancel_backend($1)",
818
+ values: [backendPid],
819
+ query_timeout: CANCEL_QUERY_TIMEOUT_MS
820
+ };
821
+ await this.pool.query(cancel);
822
+ } catch {
673
823
  }
674
824
  }
675
825
  };
@@ -2,8 +2,9 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-5FL4XMWG.js";
5
+ } from "./chunk-PDKPK25P.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
+ import "./chunk-5XSBOK3W.js";
7
8
  import "./chunk-IXC4NQGC.js";
8
9
  export {
9
10
  ToolRegistry,
@@ -4,8 +4,8 @@ import {
4
4
  import {
5
5
  SQLRowLimiter,
6
6
  closeQuietly
7
- } from "./chunk-YIGJHEU6.js";
8
- import "./chunk-ABTVMHD6.js";
7
+ } from "./chunk-X5OCFRNL.js";
8
+ import "./chunk-55X2VY5D.js";
9
9
  import {
10
10
  ConnectorRegistry,
11
11
  SafeURL,