@bytebase/dbhub 1.4.0 → 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,24 +58,83 @@ var postgresTypeParsers = {
56
58
  }
57
59
  };
58
60
 
59
- // src/connectors/postgres/index.ts
60
- var { Pool } = pg2;
61
- var POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS = 5e3;
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";
62
66
  var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
63
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
+ }
64
79
  async function readPemFile(filePath, label) {
65
- const resolved = filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
80
+ const resolved = resolvePemPath(filePath);
66
81
  try {
67
82
  return await fs.promises.readFile(resolved, "utf-8");
68
83
  } catch (err) {
69
- throw new FailedToReadCertificate(
70
- `Failed to read ${label} at '${resolved}': ${err instanceof Error ? err.message : String(err)}`
71
- );
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);
72
93
  }
73
94
  }
74
95
  function isEncryptedPemKey(pem) {
75
96
  return pem.includes("ENCRYPTED PRIVATE KEY") || pem.includes("Proc-Type: 4,ENCRYPTED");
76
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
+
136
+ // src/connectors/postgres/index.ts
137
+ var { Pool } = pg3;
77
138
  var PostgresDSNParser = class {
78
139
  async parse(dsn, config) {
79
140
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
@@ -96,7 +157,7 @@ Expected: ${expectedFormat}`
96
157
  database: url.pathname ? url.pathname.substring(1) : "",
97
158
  // Remove leading '/' if exists
98
159
  user: url.username,
99
- password: url.password
160
+ password: config?.password ?? url.password
100
161
  };
101
162
  let sslmode;
102
163
  let sslrootcert;
@@ -143,29 +204,30 @@ Expected: ${expectedFormat}`
143
204
  sslConfig.checkServerIdentity = () => void 0;
144
205
  }
145
206
  if (sslrootcert) {
146
- sslConfig.ca = await readPemFile(sslrootcert, "SSL root certificate");
207
+ sslConfig.ca = await readPemFile(sslrootcert, SSL_ROOT_CERT_LABEL);
147
208
  }
148
209
  poolConfig.ssl = sslConfig;
149
210
  }
150
211
  if (sslcert !== void 0 && sslkey !== void 0) {
151
- const key = await readPemFile(sslkey, "SSL client key");
212
+ const key = await readPemFile(sslkey, SSL_CLIENT_KEY_LABEL);
152
213
  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
- );
214
+ throw new FailedToReadCertificate(encryptedKeyMessage(sslkey));
156
215
  }
157
216
  Object.assign(poolConfig.ssl, {
158
- cert: await readPemFile(sslcert, "SSL client certificate"),
217
+ cert: await readPemFile(sslcert, SSL_CLIENT_CERT_LABEL),
159
218
  key
160
219
  });
161
220
  }
221
+ if (sslrootcert !== void 0 || sslcert !== void 0) {
222
+ poolConfig.Client = rotatingPemClient({ sslrootcert, sslcert, sslkey });
223
+ }
162
224
  if (connectionTimeoutSeconds !== void 0) {
163
225
  poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
164
226
  }
165
227
  if (queryTimeoutSeconds !== void 0) {
166
228
  const queryTimeoutMs = queryTimeoutSeconds * 1e3;
167
229
  poolConfig.statement_timeout = queryTimeoutMs;
168
- poolConfig.query_timeout = queryTimeoutMs + POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS;
230
+ poolConfig.query_timeout = clientQueryTimeoutMs(queryTimeoutMs);
169
231
  }
170
232
  if (poolMaxConnections !== void 0) {
171
233
  poolConfig.max = poolMaxConnections;
@@ -639,6 +701,8 @@ var PostgresConnector = class _PostgresConnector {
639
701
  throw new Error("Not connected to database");
640
702
  }
641
703
  const client = await this.pool.connect();
704
+ const backendPid = client.processID;
705
+ let isClientDiscarded = false;
642
706
  try {
643
707
  const statements = splitSQLStatements(sql, "postgres");
644
708
  if (statements.length === 1) {
@@ -660,10 +724,7 @@ var PostgresConnector = class _PostgresConnector {
660
724
  SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
661
725
  return { resultSets: [resultSet2] };
662
726
  } catch (error) {
663
- try {
664
- await client.query("ROLLBACK");
665
- } catch {
666
- }
727
+ await this.rollbackQuietly(client, error);
667
728
  throw error;
668
729
  }
669
730
  }
@@ -704,16 +765,61 @@ var PostgresConnector = class _PostgresConnector {
704
765
  }
705
766
  await client.query("COMMIT");
706
767
  } catch (error) {
707
- try {
708
- await client.query("ROLLBACK");
709
- } catch {
710
- }
768
+ await this.rollbackQuietly(client, error);
711
769
  throw error;
712
770
  }
713
771
  return { resultSets };
714
772
  }
773
+ } catch (error) {
774
+ if (isClientSideTimeout(error)) {
775
+ isClientDiscarded = true;
776
+ client.release(true);
777
+ await this.cancelBackend(backendPid);
778
+ }
779
+ throw error;
715
780
  } finally {
716
- 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 {
717
823
  }
718
824
  }
719
825
  };
@@ -2,8 +2,9 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-MJVIE3QY.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,