neon 3.0.0 → 3.1.1

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.
Files changed (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,602 +1,441 @@
1
- /**
2
- * TLS negotiation for the psql wire layer (WP-02).
3
- *
4
- * Two responsibilities:
5
- *
6
- * 1. Drive the PG-flavoured SSL handshake. Postgres negotiates TLS
7
- * *before* the protocol startup: client sends an `SSLRequest`
8
- * (8-byte fixed message), server replies with a single byte —
9
- * 'S' to accept, 'N' to refuse. On 'S' we wrap the existing socket
10
- * with `tls.connect({ socket })`; on 'N' we either bail (require/
11
- * verify-*) or fall through plaintext (prefer/allow/disable).
12
- *
13
- * 2. Extract `tls-server-end-point` channel-binding material from the
14
- * negotiated TLS session for SCRAM-SHA-256-PLUS. Per RFC 5929 §4 the
15
- * data is the hash of the peer certificate computed with the cert's
16
- * own signature hash, unless that hash is MD5 or SHA-1 — in which
17
- * case the binding uses SHA-256. libpq's policy (`fe-secure-openssl.c`
18
- * `PgChannelBinding`) is "always SHA-256 of the DER cert", which is
19
- * also what the PG server expects. We follow libpq.
20
- *
21
- * Notes:
22
- * - We deliberately do NOT validate the server cert here; that's the
23
- * caller's responsibility (pass `tlsOpts.rejectUnauthorized` etc.). The
24
- * ssl-mode → tls-options mapping lives in `connection.ts`.
25
- * - `verify-ca` and `verify-full` differ only in hostname checking, which
26
- * Node's `tls.connect` performs automatically when `checkServerIdentity`
27
- * is the default and `servername` is set. The connection layer wires
28
- * `servername` to the configured host before calling us.
29
- */
30
- import { createHash, createPrivateKey } from "node:crypto";
31
- import { appendFileSync, promises as fs } from "node:fs";
1
+ import { SSLRequest } from "./protocol.js";
2
+ import { appendFileSync, promises } from "node:fs";
32
3
  import * as path from "node:path";
4
+ import { createHash, createPrivateKey } from "node:crypto";
33
5
  import * as tls from "node:tls";
34
- import { SSLRequest } from "./protocol.js";
6
+ //#region src/psql/wire/tls.ts
7
+ /**
8
+ * TLS negotiation for the psql wire layer (WP-02).
9
+ *
10
+ * Two responsibilities:
11
+ *
12
+ * 1. Drive the PG-flavoured SSL handshake. Postgres negotiates TLS
13
+ * *before* the protocol startup: client sends an `SSLRequest`
14
+ * (8-byte fixed message), server replies with a single byte —
15
+ * 'S' to accept, 'N' to refuse. On 'S' we wrap the existing socket
16
+ * with `tls.connect({ socket })`; on 'N' we either bail (require/
17
+ * verify-*) or fall through plaintext (prefer/allow/disable).
18
+ *
19
+ * 2. Extract `tls-server-end-point` channel-binding material from the
20
+ * negotiated TLS session for SCRAM-SHA-256-PLUS. Per RFC 5929 §4 the
21
+ * data is the hash of the peer certificate computed with the cert's
22
+ * own signature hash, unless that hash is MD5 or SHA-1 — in which
23
+ * case the binding uses SHA-256. libpq's policy (`fe-secure-openssl.c`
24
+ * `PgChannelBinding`) is "always SHA-256 of the DER cert", which is
25
+ * also what the PG server expects. We follow libpq.
26
+ *
27
+ * Notes:
28
+ * - We deliberately do NOT validate the server cert here; that's the
29
+ * caller's responsibility (pass `tlsOpts.rejectUnauthorized` etc.). The
30
+ * ssl-mode → tls-options mapping lives in `connection.ts`.
31
+ * - `verify-ca` and `verify-full` differ only in hostname checking, which
32
+ * Node's `tls.connect` performs automatically when `checkServerIdentity`
33
+ * is the default and `servername` is set. The connection layer wires
34
+ * `servername` to the configured host before calling us.
35
+ */
35
36
  /**
36
- * Hash of the peer cert for `tls-server-end-point`. libpq always uses SHA-256
37
- * of the DER-encoded certificate; we match that.
38
- *
39
- * Exposed for tests so we can stub the peer cert.
40
- */
41
- export function computeChannelBindingData(cert) {
42
- // `cert.raw` is the DER-encoded certificate. Strict typing in @types/node
43
- // marks it as `Buffer | undefined` on some versions, hence the guard.
44
- const raw = cert.raw;
45
- if (!raw || raw.length === 0) {
46
- throw new Error("TLS channel binding: peer certificate has no DER bytes");
47
- }
48
- return createHash("sha256").update(raw).digest();
37
+ * Hash of the peer cert for `tls-server-end-point`. libpq always uses SHA-256
38
+ * of the DER-encoded certificate; we match that.
39
+ *
40
+ * Exposed for tests so we can stub the peer cert.
41
+ */
42
+ function computeChannelBindingData(cert) {
43
+ const raw = cert.raw;
44
+ if (!raw || raw.length === 0) throw new Error("TLS channel binding: peer certificate has no DER bytes");
45
+ return createHash("sha256").update(raw).digest();
49
46
  }
50
47
  /**
51
- * Send SSLRequest, read the 1-byte server response, and either upgrade the
52
- * socket to TLS or stay plain (depending on `sslMode`).
53
- *
54
- * `tlsOpts` is passed through to `tls.connect` — the connection layer fills
55
- * in `host`, `servername`, `ca`, `rejectUnauthorized`, etc. before calling.
56
- *
57
- * `fileOpts` carries libpq-style PEM file paths (`sslcert`, `sslkey`,
58
- * `sslrootcert`, `sslcrl`). Each present path is read from disk before the
59
- * TLS handshake and threaded into the corresponding tls.connect option
60
- * (`cert` / `key` / `ca` / `crl`). Read failures bubble out as
61
- * `could not read ssl<…>: <message>` so the caller sees the libpq diagnostic
62
- * shape rather than a bare ENOENT.
63
- *
64
- * `fileOpts.sslkeylogfile`, when set, is pre-checked for writability here and
65
- * wired to a `'keylog'` listener on the upgraded socket so TLS session keys
66
- * are appended for offline decryption.
67
- *
68
- * `negotiation` selects how TLS is started (libpq `sslnegotiation`):
69
- * - `'postgres'` (default): send `SSLRequest` and await the 'S'/'N' reply.
70
- * - `'direct'`: skip `SSLRequest` and start the TLS handshake immediately
71
- * on the raw socket (PG 17+). The caller must have set
72
- * `tlsOpts.ALPNProtocols` to `['postgresql']`; there is no plaintext
73
- * fallback, so a server that does not speak TLS surfaces the handshake
74
- * failure rather than a quiet downgrade.
75
- */
76
- export async function negotiateTls(socket, sslMode, tlsOpts = {}, fileOpts = {}, negotiation = "postgres") {
77
- if (sslMode === "disable") {
78
- return { kind: "plain", socket };
79
- }
80
- // Direct SSL (libpq `sslnegotiation=direct`, PG 17+): skip the `SSLRequest`
81
- // probe and start the TLS handshake straight away. The parse layer has
82
- // already rejected weak sslmodes, so this path is only reached with an
83
- // encrypted mode — never falling back to plaintext.
84
- if (negotiation === "direct") {
85
- const mergedOpts = await loadTlsFileOptions(tlsOpts, fileOpts, sslMode);
86
- return upgradeToTls(socket, mergedOpts, fileOpts.sslkeylogfile, fileOpts.sslkey,
87
- /* requireAlpn */ true);
88
- }
89
- const reply = await sendSslRequest(socket);
90
- if (reply === "S") {
91
- const mergedOpts = await loadTlsFileOptions(tlsOpts, fileOpts, sslMode);
92
- return upgradeToTls(socket, mergedOpts, fileOpts.sslkeylogfile, fileOpts.sslkey);
93
- }
94
- // reply === 'N': server refused TLS.
95
- if (sslMode === "require" ||
96
- sslMode === "verify-ca" ||
97
- sslMode === "verify-full") {
98
- throw new Error(`SSL connection required (sslmode=${sslMode}) but server refused (replied 'N')`);
99
- }
100
- // 'allow' / 'prefer': fall back to plain text.
101
- return { kind: "plain", socket };
48
+ * Send SSLRequest, read the 1-byte server response, and either upgrade the
49
+ * socket to TLS or stay plain (depending on `sslMode`).
50
+ *
51
+ * `tlsOpts` is passed through to `tls.connect` — the connection layer fills
52
+ * in `host`, `servername`, `ca`, `rejectUnauthorized`, etc. before calling.
53
+ *
54
+ * `fileOpts` carries libpq-style PEM file paths (`sslcert`, `sslkey`,
55
+ * `sslrootcert`, `sslcrl`). Each present path is read from disk before the
56
+ * TLS handshake and threaded into the corresponding tls.connect option
57
+ * (`cert` / `key` / `ca` / `crl`). Read failures bubble out as
58
+ * `could not read ssl<…>: <message>` so the caller sees the libpq diagnostic
59
+ * shape rather than a bare ENOENT.
60
+ *
61
+ * `fileOpts.sslkeylogfile`, when set, is pre-checked for writability here and
62
+ * wired to a `'keylog'` listener on the upgraded socket so TLS session keys
63
+ * are appended for offline decryption.
64
+ *
65
+ * `negotiation` selects how TLS is started (libpq `sslnegotiation`):
66
+ * - `'postgres'` (default): send `SSLRequest` and await the 'S'/'N' reply.
67
+ * - `'direct'`: skip `SSLRequest` and start the TLS handshake immediately
68
+ * on the raw socket (PG 17+). The caller must have set
69
+ * `tlsOpts.ALPNProtocols` to `['postgresql']`; there is no plaintext
70
+ * fallback, so a server that does not speak TLS surfaces the handshake
71
+ * failure rather than a quiet downgrade.
72
+ */
73
+ async function negotiateTls(socket, sslMode, tlsOpts = {}, fileOpts = {}, negotiation = "postgres") {
74
+ if (sslMode === "disable") return {
75
+ kind: "plain",
76
+ socket
77
+ };
78
+ if (negotiation === "direct") return upgradeToTls(socket, await loadTlsFileOptions(tlsOpts, fileOpts, sslMode), fileOpts.sslkeylogfile, fileOpts.sslkey, true);
79
+ if (await sendSslRequest(socket) === "S") return upgradeToTls(socket, await loadTlsFileOptions(tlsOpts, fileOpts, sslMode), fileOpts.sslkeylogfile, fileOpts.sslkey);
80
+ if (sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full") throw new Error(`SSL connection required (sslmode=${sslMode}) but server refused (replied 'N')`);
81
+ return {
82
+ kind: "plain",
83
+ socket
84
+ };
102
85
  }
103
86
  /**
104
- * Read each non-empty file path in `fileOpts` and merge the bytes onto a
105
- * shallow copy of `tlsOpts`. Each ENOENT / EACCES / permission error is
106
- * surfaced as `could not read ssl<file>: <reason>` so users immediately
107
- * know which option pointed at a bad path.
108
- *
109
- * Behaviour-defining details:
110
- *
111
- * - `sslrootcert` is only read when `sslMode` is `verify-ca` /
112
- * `verify-full`. Lower modes (require / prefer / allow) accept the
113
- * server cert without consulting the trust anchor, matching libpq's
114
- * policy of never opening the file in those modes. Tests can pass
115
- * `'verify-ca'` as the sentinel to force the eager read for any
116
- * non-disable mode (the default if omitted).
117
- * - `sslpassword` is plumbed into `tls.connect` as `passphrase`; if
118
- * unset OpenSSL leaves an encrypted key un-decryptable and the
119
- * handshake errors with a "bad decrypt" diagnostic.
120
- *
121
- * Exported for tests (`tls.test.ts` swaps in a mocked `tls.connect`).
122
- */
123
- export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
124
- const merged = { ...tlsOpts };
125
- // libpq only opens the trust-anchor file in modes that actually
126
- // validate the chain. Mirror that so a stale / placeholder
127
- // `sslrootcert=` doesn't blow up sslmode=require connections.
128
- const needsRootCert = sslMode === undefined ||
129
- sslMode === "verify-ca" ||
130
- sslMode === "verify-full";
131
- if (needsRootCert &&
132
- fileOpts.sslrootcert !== undefined &&
133
- fileOpts.sslrootcert !== "") {
134
- if (fileOpts.sslrootcert === "system") {
135
- // `sslrootcert=system`: use the OS / OpenSSL trust store instead of a
136
- // file. libpq's OpenSSL build honours OpenSSL's $SSL_CERT_FILE (a single
137
- // bundle) and $SSL_CERT_DIR (a directory of hashed CA files); we read
138
- // both. With neither set, leaving `ca` unset makes Node fall back to its
139
- // built-in root store. `rejectUnauthorized` (set by the connection layer
140
- // for verify-* modes) is left intact.
141
- const systemCas = [];
142
- const sslCertFile = process.env.SSL_CERT_FILE;
143
- if (sslCertFile !== undefined && sslCertFile !== "") {
144
- systemCas.push(await readPem("sslrootcert", sslCertFile, "CERTIFICATE"));
145
- }
146
- const sslCertDir = process.env.SSL_CERT_DIR;
147
- if (sslCertDir !== undefined && sslCertDir !== "") {
148
- systemCas.push(...(await readCaDir(sslCertDir)));
149
- }
150
- if (systemCas.length === 1) {
151
- merged.ca = systemCas[0];
152
- }
153
- else if (systemCas.length > 1) {
154
- merged.ca = systemCas;
155
- }
156
- }
157
- else {
158
- merged.ca = await readPem("sslrootcert", fileOpts.sslrootcert, "CERTIFICATE");
159
- }
160
- }
161
- // libpq `sslcertmode` gates whether the client cert/key are sent.
162
- // - `disable`: skip loading them entirely, even when configured.
163
- // - `require`: a cert MUST be configured (we only honour an explicit
164
- // `sslcert`, not libpq's default `~/.postgresql/postgresql.crt`).
165
- // - `allow` / unset: current behaviour (send when present).
166
- const certMode = fileOpts.sslcertmode ?? "allow";
167
- const clientCert = fileOpts.sslcert !== undefined && fileOpts.sslcert !== ""
168
- ? fileOpts.sslcert
169
- : undefined;
170
- if (certMode === "require" && clientCert === undefined) {
171
- throw new Error(`sslcertmode value "require" requires a client certificate`);
172
- }
173
- if (certMode !== "disable") {
174
- if (clientCert !== undefined) {
175
- merged.cert = await readPem("sslcert", clientCert, "CERTIFICATE");
176
- }
177
- if (fileOpts.sslkey !== undefined && fileOpts.sslkey !== "") {
178
- await assertKeyPermissions(fileOpts.sslkey);
179
- merged.key = await readPem("sslkey", fileOpts.sslkey, "PRIVATE KEY");
180
- }
181
- }
182
- // CRLs come from a single file (`sslcrl`) and/or every file in a directory
183
- // (`sslcrldir`). Node's `crl` option accepts an array of PEM buffers, so we
184
- // collect each source and only set `crl` when at least one was read.
185
- const crls = [];
186
- if (fileOpts.sslcrl !== undefined && fileOpts.sslcrl !== "") {
187
- crls.push(await readPem("sslcrl", fileOpts.sslcrl));
188
- }
189
- if (fileOpts.sslcrldir !== undefined && fileOpts.sslcrldir !== "") {
190
- crls.push(...(await readCrlDir(fileOpts.sslcrldir)));
191
- }
192
- if (crls.length === 1) {
193
- merged.crl = crls[0];
194
- }
195
- else if (crls.length > 1) {
196
- merged.crl = crls;
197
- }
198
- // sslpassword is plumbed through verbatim — OpenSSL applies it when it
199
- // sees an encrypted key. Empty string is "no passphrase" (libpq's
200
- // convention) so we skip it.
201
- if (fileOpts.sslpassword !== undefined && fileOpts.sslpassword !== "") {
202
- merged.passphrase = fileOpts.sslpassword;
203
- }
204
- // sslkeylogfile is not a tls.connect option; the keylog listener is wired
205
- // in `upgradeToTls`. We pre-check it here (the home of file diagnostics)
206
- // by opening it for append, so an unwritable path fails fast at connect
207
- // time with `could not open sslkeylogfile "<path>": <reason>` rather than
208
- // silently dropping keys mid-handshake.
209
- if (fileOpts.sslkeylogfile !== undefined && fileOpts.sslkeylogfile !== "") {
210
- await assertKeyLogFileWritable(fileOpts.sslkeylogfile);
211
- }
212
- return merged;
87
+ * Read each non-empty file path in `fileOpts` and merge the bytes onto a
88
+ * shallow copy of `tlsOpts`. Each ENOENT / EACCES / permission error is
89
+ * surfaced as `could not read ssl<file>: <reason>` so users immediately
90
+ * know which option pointed at a bad path.
91
+ *
92
+ * Behaviour-defining details:
93
+ *
94
+ * - `sslrootcert` is only read when `sslMode` is `verify-ca` /
95
+ * `verify-full`. Lower modes (require / prefer / allow) accept the
96
+ * server cert without consulting the trust anchor, matching libpq's
97
+ * policy of never opening the file in those modes. Tests can pass
98
+ * `'verify-ca'` as the sentinel to force the eager read for any
99
+ * non-disable mode (the default if omitted).
100
+ * - `sslpassword` is plumbed into `tls.connect` as `passphrase`; if
101
+ * unset OpenSSL leaves an encrypted key un-decryptable and the
102
+ * handshake errors with a "bad decrypt" diagnostic.
103
+ *
104
+ * Exported for tests (`tls.test.ts` swaps in a mocked `tls.connect`).
105
+ */
106
+ async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
107
+ const merged = { ...tlsOpts };
108
+ if ((sslMode === void 0 || sslMode === "verify-ca" || sslMode === "verify-full") && fileOpts.sslrootcert !== void 0 && fileOpts.sslrootcert !== "") if (fileOpts.sslrootcert === "system") {
109
+ const systemCas = [];
110
+ const sslCertFile = process.env.SSL_CERT_FILE;
111
+ if (sslCertFile !== void 0 && sslCertFile !== "") systemCas.push(await readPem("sslrootcert", sslCertFile, "CERTIFICATE"));
112
+ const sslCertDir = process.env.SSL_CERT_DIR;
113
+ if (sslCertDir !== void 0 && sslCertDir !== "") systemCas.push(...await readCaDir(sslCertDir));
114
+ if (systemCas.length === 1) merged.ca = systemCas[0];
115
+ else if (systemCas.length > 1) merged.ca = systemCas;
116
+ } else merged.ca = await readPem("sslrootcert", fileOpts.sslrootcert, "CERTIFICATE");
117
+ const certMode = fileOpts.sslcertmode ?? "allow";
118
+ const clientCert = fileOpts.sslcert !== void 0 && fileOpts.sslcert !== "" ? fileOpts.sslcert : void 0;
119
+ if (certMode === "require" && clientCert === void 0) throw new Error(`sslcertmode value "require" requires a client certificate`);
120
+ if (certMode !== "disable") {
121
+ if (clientCert !== void 0) merged.cert = await readPem("sslcert", clientCert, "CERTIFICATE");
122
+ if (fileOpts.sslkey !== void 0 && fileOpts.sslkey !== "") {
123
+ await assertKeyPermissions(fileOpts.sslkey);
124
+ merged.key = await readPem("sslkey", fileOpts.sslkey, "PRIVATE KEY");
125
+ }
126
+ }
127
+ const crls = [];
128
+ if (fileOpts.sslcrl !== void 0 && fileOpts.sslcrl !== "") crls.push(await readPem("sslcrl", fileOpts.sslcrl));
129
+ if (fileOpts.sslcrldir !== void 0 && fileOpts.sslcrldir !== "") crls.push(...await readCrlDir(fileOpts.sslcrldir));
130
+ if (crls.length === 1) merged.crl = crls[0];
131
+ else if (crls.length > 1) merged.crl = crls;
132
+ if (fileOpts.sslpassword !== void 0 && fileOpts.sslpassword !== "") merged.passphrase = fileOpts.sslpassword;
133
+ if (fileOpts.sslkeylogfile !== void 0 && fileOpts.sslkeylogfile !== "") await assertKeyLogFileWritable(fileOpts.sslkeylogfile);
134
+ return merged;
213
135
  }
214
136
  /**
215
- * Pre-flight the `sslkeylogfile` target by opening it for append (creating
216
- * it if absent) and immediately closing the handle. Surfaces libpq-style
217
- * `could not open sslkeylogfile "<path>": <reason>` on any failure (e.g. an
218
- * unwritable directory) before the handshake starts.
219
- */
137
+ * Pre-flight the `sslkeylogfile` target by opening it for append (creating
138
+ * it if absent) and immediately closing the handle. Surfaces libpq-style
139
+ * `could not open sslkeylogfile "<path>": <reason>` on any failure (e.g. an
140
+ * unwritable directory) before the handshake starts.
141
+ */
220
142
  async function assertKeyLogFileWritable(filePath) {
221
- try {
222
- const handle = await fs.open(filePath, "a");
223
- await handle.close();
224
- }
225
- catch (err) {
226
- const reason = err instanceof Error ? err.message : String(err);
227
- throw new Error(`could not open sslkeylogfile "${filePath}": ${reason}`);
228
- }
143
+ try {
144
+ await (await promises.open(filePath, "a")).close();
145
+ } catch (err) {
146
+ const reason = err instanceof Error ? err.message : String(err);
147
+ throw new Error(`could not open sslkeylogfile "${filePath}": ${reason}`);
148
+ }
229
149
  }
230
150
  /** True when the bytes already carry a `-----BEGIN ...-----` PEM header. */
231
151
  function isPemArmored(bytes) {
232
- // A PEM file is ASCII text; scan a bounded prefix (skipping leading
233
- // whitespace libpq tolerates) for the armor marker. DER is binary and will
234
- // not contain this token at the front.
235
- const head = bytes.subarray(0, 64).toString("latin1");
236
- return head.includes("-----BEGIN");
152
+ return bytes.subarray(0, 64).toString("latin1").includes("-----BEGIN");
237
153
  }
238
154
  /**
239
- * Wrap raw DER bytes in the requested PEM armor: base64 the DER, split into
240
- * 64-char lines (the PEM convention), and bracket with the BEGIN/END markers.
241
- */
155
+ * Wrap raw DER bytes in the requested PEM armor: base64 the DER, split into
156
+ * 64-char lines (the PEM convention), and bracket with the BEGIN/END markers.
157
+ */
242
158
  function derToPem(der, armor) {
243
- const b64 = der.toString("base64");
244
- const lines = b64.match(/.{1,64}/g) ?? [];
245
- const body = lines.join("\n");
246
- const pem = `-----BEGIN ${armor}-----\n${body}\n-----END ${armor}-----\n`;
247
- return Buffer.from(pem, "ascii");
159
+ const pem = `-----BEGIN ${armor}-----\n${(der.toString("base64").match(/.{1,64}/g) ?? []).join("\n")}\n-----END ${armor}-----\n`;
160
+ return Buffer.from(pem, "ascii");
248
161
  }
249
162
  /**
250
- * Convert a DER-encoded private key to canonical PKCS#8 PEM, the way libpq
251
- * relies on OpenSSL to sniff `sslkey` format. A DER key may be PKCS#8
252
- * (`PrivateKeyInfo`), PKCS#1 (bare RSA `RSAPrivateKey`), or SEC1 (bare EC
253
- * `ECPrivateKey`) — and the encoding `openssl pkey -outform der` produces for
254
- * RSA differs across OpenSSL versions (3.0.x emits a form that blind PKCS#8
255
- * armor cannot load: `DECODER routines::unsupported`). Rather than guess the
256
- * armor, let `crypto.createPrivateKey` decode each candidate type and
257
- * re-export a single canonical PKCS#8 PEM that `tls.connect` always accepts.
258
- */
163
+ * Convert a DER-encoded private key to canonical PKCS#8 PEM, the way libpq
164
+ * relies on OpenSSL to sniff `sslkey` format. A DER key may be PKCS#8
165
+ * (`PrivateKeyInfo`), PKCS#1 (bare RSA `RSAPrivateKey`), or SEC1 (bare EC
166
+ * `ECPrivateKey`) — and the encoding `openssl pkey -outform der` produces for
167
+ * RSA differs across OpenSSL versions (3.0.x emits a form that blind PKCS#8
168
+ * armor cannot load: `DECODER routines::unsupported`). Rather than guess the
169
+ * armor, let `crypto.createPrivateKey` decode each candidate type and
170
+ * re-export a single canonical PKCS#8 PEM that `tls.connect` always accepts.
171
+ */
259
172
  function derPrivateKeyToPem(der) {
260
- let lastErr;
261
- for (const type of ["pkcs8", "pkcs1", "sec1"]) {
262
- try {
263
- const key = createPrivateKey({ key: der, format: "der", type });
264
- const pem = key.export({ format: "pem", type: "pkcs8" });
265
- return typeof pem === "string" ? Buffer.from(pem, "ascii") : pem;
266
- }
267
- catch (err) {
268
- lastErr = err;
269
- }
270
- }
271
- const reason = lastErr instanceof Error ? lastErr.message : String(lastErr);
272
- throw new Error(`sslkey is DER but could not be decoded as PKCS#8, PKCS#1, or SEC1: ${reason}`);
173
+ let lastErr;
174
+ for (const type of [
175
+ "pkcs8",
176
+ "pkcs1",
177
+ "sec1"
178
+ ]) try {
179
+ const pem = createPrivateKey({
180
+ key: der,
181
+ format: "der",
182
+ type
183
+ }).export({
184
+ format: "pem",
185
+ type: "pkcs8"
186
+ });
187
+ return typeof pem === "string" ? Buffer.from(pem, "ascii") : pem;
188
+ } catch (err) {
189
+ lastErr = err;
190
+ }
191
+ const reason = lastErr instanceof Error ? lastErr.message : String(lastErr);
192
+ throw new Error(`sslkey is DER but could not be decoded as PKCS#8, PKCS#1, or SEC1: ${reason}`);
273
193
  }
274
194
  /**
275
- * Read a libpq SSL file, returning PEM bytes ready for `tls.connect`. If the
276
- * file is already PEM-armored it's returned verbatim; otherwise it's treated
277
- * as DER and converted in-memory using {@link derToPem} with `derArmor`
278
- * (matching libpq's PEM-or-DER auto-detection). When `derArmor` is omitted the
279
- * file is returned as-is even if not PEM (used for CRLs, where DER conversion
280
- * is out of scope).
281
- */
195
+ * Read a libpq SSL file, returning PEM bytes ready for `tls.connect`. If the
196
+ * file is already PEM-armored it's returned verbatim; otherwise it's treated
197
+ * as DER and converted in-memory using {@link derToPem} with `derArmor`
198
+ * (matching libpq's PEM-or-DER auto-detection). When `derArmor` is omitted the
199
+ * file is returned as-is even if not PEM (used for CRLs, where DER conversion
200
+ * is out of scope).
201
+ */
282
202
  async function readPem(label, filePath, derArmor) {
283
- let bytes;
284
- try {
285
- bytes = await fs.readFile(filePath);
286
- }
287
- catch (err) {
288
- const reason = err instanceof Error ? err.message : String(err);
289
- throw new Error(`could not read ${label} "${filePath}": ${reason}`);
290
- }
291
- if (derArmor !== undefined && !isPemArmored(bytes)) {
292
- // Private keys need format-aware decoding (PKCS#8 / PKCS#1 / SEC1);
293
- // certs are a single ASN.1 shape and wrap directly.
294
- return derArmor === "PRIVATE KEY"
295
- ? derPrivateKeyToPem(bytes)
296
- : derToPem(bytes, derArmor);
297
- }
298
- return bytes;
203
+ let bytes;
204
+ try {
205
+ bytes = await promises.readFile(filePath);
206
+ } catch (err) {
207
+ const reason = err instanceof Error ? err.message : String(err);
208
+ throw new Error(`could not read ${label} "${filePath}": ${reason}`);
209
+ }
210
+ if (derArmor !== void 0 && !isPemArmored(bytes)) return derArmor === "PRIVATE KEY" ? derPrivateKeyToPem(bytes) : derToPem(bytes, derArmor);
211
+ return bytes;
299
212
  }
300
213
  /**
301
- * libpq-style permission guard for the client private key (`sslkey`). libpq
302
- * (`fe-secure-openssl.c`) `stat()`s the key file and refuses to load it when
303
- * it is a regular file with any group or world access bits set, unless it is
304
- * root-owned with at most `u=rw,g=r` (0640). Mirroring that keeps an
305
- * accidentally world-readable key from being used silently.
306
- *
307
- * The check is a no-op on Windows, where the POSIX mode bits are not
308
- * meaningful (matching libpq, which `#ifndef WIN32`-guards the same check),
309
- * and when the key path is a directory / special file (only regular files
310
- * carry a private key here).
311
- */
214
+ * libpq-style permission guard for the client private key (`sslkey`). libpq
215
+ * (`fe-secure-openssl.c`) `stat()`s the key file and refuses to load it when
216
+ * it is a regular file with any group or world access bits set, unless it is
217
+ * root-owned with at most `u=rw,g=r` (0640). Mirroring that keeps an
218
+ * accidentally world-readable key from being used silently.
219
+ *
220
+ * The check is a no-op on Windows, where the POSIX mode bits are not
221
+ * meaningful (matching libpq, which `#ifndef WIN32`-guards the same check),
222
+ * and when the key path is a directory / special file (only regular files
223
+ * carry a private key here).
224
+ */
312
225
  async function assertKeyPermissions(keyPath) {
313
- if (process.platform === "win32")
314
- return;
315
- let stat;
316
- try {
317
- stat = await fs.stat(keyPath);
318
- }
319
- catch (err) {
320
- const reason = err instanceof Error ? err.message : String(err);
321
- throw new Error(`could not read sslkey "${keyPath}": ${reason}`);
322
- }
323
- if (!stat.isFile())
324
- return;
325
- // Low 9 mode bits: rwx for user/group/other.
326
- const mode = stat.mode & 0o777;
327
- const groupOrWorld = mode & 0o077;
328
- if (groupOrWorld === 0)
329
- return;
330
- // Root-owned keys are allowed to be u=rw,g=r (0640) or less, matching
331
- // libpq's relaxed allowance for system-managed keys: no bits outside the
332
- // 0640 mask may be set.
333
- if (stat.uid === 0 && (mode & ~0o640) === 0) {
334
- return;
335
- }
336
- throw new Error(`private key file "${keyPath}" has group or world access`);
226
+ if (process.platform === "win32") return;
227
+ let stat;
228
+ try {
229
+ stat = await promises.stat(keyPath);
230
+ } catch (err) {
231
+ const reason = err instanceof Error ? err.message : String(err);
232
+ throw new Error(`could not read sslkey "${keyPath}": ${reason}`);
233
+ }
234
+ if (!stat.isFile()) return;
235
+ const mode = stat.mode & 511;
236
+ if ((mode & 63) === 0) return;
237
+ if (stat.uid === 0 && (mode & -417) === 0) return;
238
+ throw new Error(`private key file "${keyPath}" has group or world access`);
337
239
  }
338
240
  /**
339
- * Read every regular file in an `sslcrldir` directory and return their PEM
340
- * bytes. Subdirectories are skipped (libpq's c_rehash-style directory only
341
- * holds hashed CRL files). A failure to list the directory or read any file
342
- * surfaces with the `sslcrldir` label so the caller sees which option was
343
- * misconfigured.
344
- */
241
+ * Read every regular file in an `sslcrldir` directory and return their PEM
242
+ * bytes. Subdirectories are skipped (libpq's c_rehash-style directory only
243
+ * holds hashed CRL files). A failure to list the directory or read any file
244
+ * surfaces with the `sslcrldir` label so the caller sees which option was
245
+ * misconfigured.
246
+ */
345
247
  async function readCrlDir(dirPath) {
346
- let entries;
347
- try {
348
- entries = await fs.readdir(dirPath, { withFileTypes: true });
349
- }
350
- catch (err) {
351
- const reason = err instanceof Error ? err.message : String(err);
352
- throw new Error(`could not read sslcrldir "${dirPath}": ${reason}`);
353
- }
354
- const out = [];
355
- for (const entry of entries) {
356
- if (!entry.isFile())
357
- continue;
358
- out.push(await readPem("sslcrldir", path.join(dirPath, entry.name)));
359
- }
360
- return out;
248
+ let entries;
249
+ try {
250
+ entries = await promises.readdir(dirPath, { withFileTypes: true });
251
+ } catch (err) {
252
+ const reason = err instanceof Error ? err.message : String(err);
253
+ throw new Error(`could not read sslcrldir "${dirPath}": ${reason}`);
254
+ }
255
+ const out = [];
256
+ for (const entry of entries) {
257
+ if (!entry.isFile()) continue;
258
+ out.push(await readPem("sslcrldir", path.join(dirPath, entry.name)));
259
+ }
260
+ return out;
361
261
  }
362
262
  /**
363
- * Read every regular file in an OpenSSL `$SSL_CERT_DIR` (the hashed-dir
364
- * convention honoured by `sslrootcert=system`) and return their PEM bytes.
365
- * Mirrors {@link readCrlDir}: subdirectories are skipped and a DER-format
366
- * file is auto-converted to PEM. A failure to list the directory or read any
367
- * file surfaces as `could not read SSL_CERT_DIR "<path>": <reason>`.
368
- *
369
- * Exported for tests.
370
- */
371
- export async function readCaDir(dirPath) {
372
- let entries;
373
- try {
374
- entries = await fs.readdir(dirPath, { withFileTypes: true });
375
- }
376
- catch (err) {
377
- const reason = err instanceof Error ? err.message : String(err);
378
- throw new Error(`could not read SSL_CERT_DIR "${dirPath}": ${reason}`);
379
- }
380
- const out = [];
381
- for (const entry of entries) {
382
- if (!entry.isFile())
383
- continue;
384
- out.push(await readPem("SSL_CERT_DIR", path.join(dirPath, entry.name), "CERTIFICATE"));
385
- }
386
- return out;
263
+ * Read every regular file in an OpenSSL `$SSL_CERT_DIR` (the hashed-dir
264
+ * convention honoured by `sslrootcert=system`) and return their PEM bytes.
265
+ * Mirrors {@link readCrlDir}: subdirectories are skipped and a DER-format
266
+ * file is auto-converted to PEM. A failure to list the directory or read any
267
+ * file surfaces as `could not read SSL_CERT_DIR "<path>": <reason>`.
268
+ *
269
+ * Exported for tests.
270
+ */
271
+ async function readCaDir(dirPath) {
272
+ let entries;
273
+ try {
274
+ entries = await promises.readdir(dirPath, { withFileTypes: true });
275
+ } catch (err) {
276
+ const reason = err instanceof Error ? err.message : String(err);
277
+ throw new Error(`could not read SSL_CERT_DIR "${dirPath}": ${reason}`);
278
+ }
279
+ const out = [];
280
+ for (const entry of entries) {
281
+ if (!entry.isFile()) continue;
282
+ out.push(await readPem("SSL_CERT_DIR", path.join(dirPath, entry.name), "CERTIFICATE"));
283
+ }
284
+ return out;
387
285
  }
388
286
  /**
389
- * Send SSLRequest and pull off the 1-byte server response. The byte is
390
- * outside the regular framed protocol (it has no length / type header), so
391
- * we can't reuse MessageParser; instead we peel off one byte and push any
392
- * remainder back onto the socket via `unshift`.
393
- *
394
- * Exported for tests.
395
- */
396
- export function sendSslRequest(socket) {
397
- return new Promise((resolve, reject) => {
398
- const onError = (err) => {
399
- cleanup();
400
- reject(err);
401
- };
402
- const onData = (chunk) => {
403
- if (chunk.length === 0)
404
- return;
405
- const first = String.fromCharCode(chunk[0]);
406
- if (first !== "S" && first !== "N") {
407
- cleanup();
408
- reject(new Error(`Unexpected SSLRequest response byte 0x${chunk[0].toString(16)}`));
409
- return;
410
- }
411
- // Any extra bytes belong to subsequent messages; push them back.
412
- if (chunk.length > 1) {
413
- socket.unshift(chunk.subarray(1));
414
- }
415
- cleanup();
416
- resolve(first);
417
- };
418
- const cleanup = () => {
419
- socket.removeListener("data", onData);
420
- socket.removeListener("error", onError);
421
- };
422
- socket.on("data", onData);
423
- socket.on("error", onError);
424
- socket.write(SSLRequest());
425
- });
287
+ * Send SSLRequest and pull off the 1-byte server response. The byte is
288
+ * outside the regular framed protocol (it has no length / type header), so
289
+ * we can't reuse MessageParser; instead we peel off one byte and push any
290
+ * remainder back onto the socket via `unshift`.
291
+ *
292
+ * Exported for tests.
293
+ */
294
+ function sendSslRequest(socket) {
295
+ return new Promise((resolve, reject) => {
296
+ const onError = (err) => {
297
+ cleanup();
298
+ reject(err);
299
+ };
300
+ const onData = (chunk) => {
301
+ if (chunk.length === 0) return;
302
+ const first = String.fromCharCode(chunk[0]);
303
+ if (first !== "S" && first !== "N") {
304
+ cleanup();
305
+ reject(/* @__PURE__ */ new Error(`Unexpected SSLRequest response byte 0x${chunk[0].toString(16)}`));
306
+ return;
307
+ }
308
+ if (chunk.length > 1) socket.unshift(chunk.subarray(1));
309
+ cleanup();
310
+ resolve(first);
311
+ };
312
+ const cleanup = () => {
313
+ socket.removeListener("data", onData);
314
+ socket.removeListener("error", onError);
315
+ };
316
+ socket.on("data", onData);
317
+ socket.on("error", onError);
318
+ socket.write(SSLRequest());
319
+ });
426
320
  }
427
321
  /**
428
- * Translate a Node/OpenSSL TLS handshake error into libpq-style wording so
429
- * our diagnostics match upstream psql/libpq exactly (the cases asserted by
430
- * upstream `001_ssltests.pl`). Unrecognised errors pass through unchanged.
431
- * The original error is preserved on `cause` for callers that introspect.
432
- *
433
- * - Chain-verification failures (`ERR_TLS_CERT_ALTNAME_INVALID` excluded)
434
- * → `certificate verify failed` (libpq's `SSL error: certificate verify
435
- * failed`).
436
- * - Hostname mismatch (`ERR_TLS_CERT_ALTNAME_INVALID`, Node's
437
- * "Hostname/IP does not match certificate's altnames") →
438
- * `server certificate for "<host>" does not match host name "<host>"`.
439
- * - Encrypted-key decrypt failures (`ERR_OSSL_BAD_DECRYPT`, or an OpenSSL
440
- * message containing `bad decrypt`) → libpq's
441
- * `could not load private key file "<path>": <openssl text>` shape. The
442
- * raw OpenSSL text (which carries the `bad decrypt` token upstream's
443
- * `001_ssltests.pl` matches on) is preserved in the message tail. When the
444
- * key path is unknown the path segment is omitted but the `bad decrypt`
445
- * token is still surfaced.
446
- *
447
- * `keyPath`, when supplied, is the configured `sslkey` path — used only to
448
- * fill libpq's `private key file "<path>"` phrasing on a decrypt failure.
449
- *
450
- * Exported for unit tests.
451
- */
452
- export function mapTlsHandshakeError(err, servername, keyPath) {
453
- const code = err.code;
454
- const msg = err.message;
455
- // Encrypted client-key decrypt failure (wrong / missing `sslpassword`).
456
- // OpenSSL throws this synchronously out of `tls.connect`; reshape it to
457
- // libpq's `could not load private key file "<path>": ... bad decrypt`.
458
- if (code === "ERR_OSSL_BAD_DECRYPT" || /bad decrypt/i.test(msg)) {
459
- const where = keyPath !== undefined && keyPath !== ""
460
- ? `private key file "${keyPath}"`
461
- : "private key file";
462
- const mapped = new Error(`could not load ${where}: ${msg}`);
463
- mapped.cause = err;
464
- return mapped;
465
- }
466
- if (code === "ERR_TLS_CERT_ALTNAME_INVALID") {
467
- const host = servername ?? "";
468
- const mapped = new Error(`server certificate for "${host}" does not match host name "${host}"`);
469
- mapped.cause = err;
470
- return mapped;
471
- }
472
- // OpenSSL chain-verification failures surface with a `code` like
473
- // `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `DEPTH_ZERO_SELF_SIGNED_CERT`,
474
- // `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED`, etc. libpq collapses
475
- // them all to `certificate verify failed`.
476
- const isVerifyFailure = code !== undefined &&
477
- code !== "ERR_TLS_CERT_ALTNAME_INVALID" &&
478
- /CERT|SIGNATURE|SELF_SIGNED|UNABLE_TO|CHAIN|EXPIRED|NOT_YET_VALID|INVALID_CA/.test(code);
479
- if (isVerifyFailure || /certificate verify failed/i.test(msg)) {
480
- const mapped = new Error("certificate verify failed");
481
- mapped.cause = err;
482
- return mapped;
483
- }
484
- return err;
322
+ * Translate a Node/OpenSSL TLS handshake error into libpq-style wording so
323
+ * our diagnostics match upstream psql/libpq exactly (the cases asserted by
324
+ * upstream `001_ssltests.pl`). Unrecognised errors pass through unchanged.
325
+ * The original error is preserved on `cause` for callers that introspect.
326
+ *
327
+ * - Chain-verification failures (`ERR_TLS_CERT_ALTNAME_INVALID` excluded)
328
+ * → `certificate verify failed` (libpq's `SSL error: certificate verify
329
+ * failed`).
330
+ * - Hostname mismatch (`ERR_TLS_CERT_ALTNAME_INVALID`, Node's
331
+ * "Hostname/IP does not match certificate's altnames") →
332
+ * `server certificate for "<host>" does not match host name "<host>"`.
333
+ * - Encrypted-key decrypt failures (`ERR_OSSL_BAD_DECRYPT`, or an OpenSSL
334
+ * message containing `bad decrypt`) → libpq's
335
+ * `could not load private key file "<path>": <openssl text>` shape. The
336
+ * raw OpenSSL text (which carries the `bad decrypt` token upstream's
337
+ * `001_ssltests.pl` matches on) is preserved in the message tail. When the
338
+ * key path is unknown the path segment is omitted but the `bad decrypt`
339
+ * token is still surfaced.
340
+ *
341
+ * `keyPath`, when supplied, is the configured `sslkey` path — used only to
342
+ * fill libpq's `private key file "<path>"` phrasing on a decrypt failure.
343
+ *
344
+ * Exported for unit tests.
345
+ */
346
+ function mapTlsHandshakeError(err, servername, keyPath) {
347
+ const code = err.code;
348
+ const msg = err.message;
349
+ if (code === "ERR_OSSL_BAD_DECRYPT" || /bad decrypt/i.test(msg)) {
350
+ const where = keyPath !== void 0 && keyPath !== "" ? `private key file "${keyPath}"` : "private key file";
351
+ const mapped = /* @__PURE__ */ new Error(`could not load ${where}: ${msg}`);
352
+ mapped.cause = err;
353
+ return mapped;
354
+ }
355
+ if (code === "ERR_TLS_CERT_ALTNAME_INVALID") {
356
+ const host = servername ?? "";
357
+ const mapped = /* @__PURE__ */ new Error(`server certificate for "${host}" does not match host name "${host}"`);
358
+ mapped.cause = err;
359
+ return mapped;
360
+ }
361
+ if (code !== void 0 && code !== "ERR_TLS_CERT_ALTNAME_INVALID" && /CERT|SIGNATURE|SELF_SIGNED|UNABLE_TO|CHAIN|EXPIRED|NOT_YET_VALID|INVALID_CA/.test(code) || /certificate verify failed/i.test(msg)) {
362
+ const mapped = /* @__PURE__ */ new Error("certificate verify failed");
363
+ mapped.cause = err;
364
+ return mapped;
365
+ }
366
+ return err;
485
367
  }
486
368
  /** Pull the SNI `servername` from the TLS options for error messages. */
487
369
  function getServername(tlsOpts) {
488
- return typeof tlsOpts.servername === "string"
489
- ? tlsOpts.servername
490
- : undefined;
370
+ return typeof tlsOpts.servername === "string" ? tlsOpts.servername : void 0;
491
371
  }
492
- function upgradeToTls(socket, tlsOpts, sslkeylogfile, keyPath,
493
- // Direct SSL (sslnegotiation=direct, PG17+) REQUIRES the server to select
494
- // ALPN `postgresql` — a protocol-confusion defense, since there is no
495
- // SSLRequest probe to confirm a postgres endpoint. libpq aborts when the
496
- // negotiated ALPN isn't `postgresql`; we mirror that (review item #9).
497
- requireAlpn = false) {
498
- return new Promise((resolve, reject) => {
499
- let tlsSocket;
500
- const cleanup = () => {
501
- tlsSocket?.removeListener("error", onError);
502
- };
503
- const onError = (err) => {
504
- cleanup();
505
- reject(mapTlsHandshakeError(err, getServername(tlsOpts), keyPath));
506
- };
507
- // OpenSSL surfaces an un-decryptable client key (wrong / missing
508
- // `sslpassword`) by throwing synchronously out of `tls.connect` rather
509
- // than emitting `'error'`. Catch it here so it flows through the same
510
- // libpq-wording mapper as asynchronous handshake failures.
511
- try {
512
- tlsSocket = tls.connect({
513
- ...tlsOpts,
514
- socket,
515
- }, () => {
516
- cleanup();
517
- // `tlsSocket` is always assigned by the time this async handshake
518
- // callback fires (tls.connect returns it synchronously above); the
519
- // guard simply narrows the `| undefined` for the type checker.
520
- const established = tlsSocket;
521
- if (established === undefined)
522
- return;
523
- // Enforce the mandatory `postgresql` ALPN for direct SSL. Without
524
- // it a TLS terminator / HTTPS proxy with a valid host cert would be
525
- // accepted and the startup packet sent in the blind (review #9).
526
- if (requireAlpn &&
527
- established.alpnProtocol !== "postgresql") {
528
- cleanup();
529
- established.destroy();
530
- reject(new Error("direct SSL connection requires ALPN, but the server did " +
531
- 'not negotiate the "postgresql" protocol'));
532
- return;
533
- }
534
- let channelBindingData = null;
535
- try {
536
- // Prefer the modern X509Certificate API (Node 15.6+): it returns a
537
- // proper `X509Certificate` instance whose `.raw` is the
538
- // DER-encoded cert. Falls back to the legacy
539
- // `getPeerCertificate(true)` for compatibility.
540
- const x509 = established.getPeerX509Certificate?.();
541
- if (x509?.raw && x509.raw.length > 0) {
542
- channelBindingData = createHash("sha256")
543
- .update(x509.raw)
544
- .digest();
545
- }
546
- else {
547
- // `detailed = true` gets us the full peer cert chain. Some
548
- // Node/OpenSSL combinations leave `.raw` undefined on the legacy
549
- // API when `rejectUnauthorized: false`; in that case we have to
550
- // accept that channel binding is unavailable.
551
- const peerCert = established.getPeerCertificate(true);
552
- if (peerCert?.raw && peerCert.raw.length > 0) {
553
- channelBindingData =
554
- computeChannelBindingData(peerCert);
555
- }
556
- }
557
- }
558
- catch {
559
- // Best-effort: a missing peer cert => no channel binding. SASL
560
- // path will fall back to SCRAM-SHA-256 (non-PLUS).
561
- channelBindingData = null;
562
- }
563
- resolve({
564
- kind: "tls",
565
- socket: established,
566
- channelBindingData,
567
- });
568
- });
569
- }
570
- catch (err) {
571
- reject(mapTlsHandshakeError(err instanceof Error ? err : new Error(String(err)), getServername(tlsOpts), keyPath));
572
- return;
573
- }
574
- tlsSocket.on("error", onError);
575
- // libpq `sslkeylogfile`: append each emitted key-log line so the
576
- // handshake can be decrypted offline. The path was pre-checked for
577
- // writability in loadTlsFileOptions.
578
- if (sslkeylogfile !== undefined && sslkeylogfile !== "") {
579
- attachKeyLogListener(tlsSocket, sslkeylogfile);
580
- }
581
- });
372
+ function upgradeToTls(socket, tlsOpts, sslkeylogfile, keyPath, requireAlpn = false) {
373
+ return new Promise((resolve, reject) => {
374
+ let tlsSocket;
375
+ const cleanup = () => {
376
+ tlsSocket?.removeListener("error", onError);
377
+ };
378
+ const onError = (err) => {
379
+ cleanup();
380
+ reject(mapTlsHandshakeError(err, getServername(tlsOpts), keyPath));
381
+ };
382
+ try {
383
+ tlsSocket = tls.connect({
384
+ ...tlsOpts,
385
+ socket
386
+ }, () => {
387
+ cleanup();
388
+ const established = tlsSocket;
389
+ if (established === void 0) return;
390
+ if (requireAlpn && established.alpnProtocol !== "postgresql") {
391
+ cleanup();
392
+ established.destroy();
393
+ reject(/* @__PURE__ */ new Error("direct SSL connection requires ALPN, but the server did not negotiate the \"postgresql\" protocol"));
394
+ return;
395
+ }
396
+ let channelBindingData = null;
397
+ try {
398
+ const x509 = established.getPeerX509Certificate?.();
399
+ if (x509?.raw && x509.raw.length > 0) channelBindingData = createHash("sha256").update(x509.raw).digest();
400
+ else {
401
+ const peerCert = established.getPeerCertificate(true);
402
+ if (peerCert?.raw && peerCert.raw.length > 0) channelBindingData = computeChannelBindingData(peerCert);
403
+ }
404
+ } catch {
405
+ channelBindingData = null;
406
+ }
407
+ resolve({
408
+ kind: "tls",
409
+ socket: established,
410
+ channelBindingData
411
+ });
412
+ });
413
+ } catch (err) {
414
+ reject(mapTlsHandshakeError(err instanceof Error ? err : new Error(String(err)), getServername(tlsOpts), keyPath));
415
+ return;
416
+ }
417
+ tlsSocket.on("error", onError);
418
+ if (sslkeylogfile !== void 0 && sslkeylogfile !== "") attachKeyLogListener(tlsSocket, sslkeylogfile);
419
+ });
582
420
  }
583
421
  /**
584
- * Wire a TLSSocket's `'keylog'` event to append each emitted key-log line to
585
- * `filePath`. Node emits one already-newline-terminated Buffer per event. A
586
- * write that fails after the pre-check (e.g. the directory was removed
587
- * mid-session) is re-emitted as a socket `'error'` rather than crashing the
588
- * process.
589
- *
590
- * Accepts a minimal event-emitter shape so it can be unit-tested against a
591
- * fake socket without a real TLS handshake. Exported for tests.
592
- */
593
- export function attachKeyLogListener(socket, filePath) {
594
- socket.on("keylog", (line) => {
595
- try {
596
- appendFileSync(filePath, line);
597
- }
598
- catch (err) {
599
- socket.emit("error", err instanceof Error ? err : new Error(String(err)));
600
- }
601
- });
422
+ * Wire a TLSSocket's `'keylog'` event to append each emitted key-log line to
423
+ * `filePath`. Node emits one already-newline-terminated Buffer per event. A
424
+ * write that fails after the pre-check (e.g. the directory was removed
425
+ * mid-session) is re-emitted as a socket `'error'` rather than crashing the
426
+ * process.
427
+ *
428
+ * Accepts a minimal event-emitter shape so it can be unit-tested against a
429
+ * fake socket without a real TLS handshake. Exported for tests.
430
+ */
431
+ function attachKeyLogListener(socket, filePath) {
432
+ socket.on("keylog", (line) => {
433
+ try {
434
+ appendFileSync(filePath, line);
435
+ } catch (err) {
436
+ socket.emit("error", err instanceof Error ? err : new Error(String(err)));
437
+ }
438
+ });
602
439
  }
440
+ //#endregion
441
+ export { attachKeyLogListener, computeChannelBindingData, loadTlsFileOptions, mapTlsHandshakeError, negotiateTls, readCaDir, sendSslRequest };