neon 2.1.1 → 2.29.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.
Files changed (166) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +601 -0
  3. package/dist/analytics.js +156 -0
  4. package/dist/api.js +665 -0
  5. package/dist/auth.js +127 -0
  6. package/dist/callback.html +51 -0
  7. package/dist/cli.js +9 -0
  8. package/dist/commands/auth.js +214 -0
  9. package/dist/commands/bootstrap.js +481 -0
  10. package/dist/commands/branches.js +481 -0
  11. package/dist/commands/bucket.js +543 -0
  12. package/dist/commands/checkout.js +289 -0
  13. package/dist/commands/config.js +544 -0
  14. package/dist/commands/connection_string.js +172 -0
  15. package/dist/commands/data_api.js +285 -0
  16. package/dist/commands/databases.js +82 -0
  17. package/dist/commands/deploy.js +26 -0
  18. package/dist/commands/dev.js +698 -0
  19. package/dist/commands/env.js +166 -0
  20. package/dist/commands/functions.js +373 -0
  21. package/dist/commands/index.js +54 -0
  22. package/dist/commands/init.js +73 -0
  23. package/dist/commands/ip_allow.js +137 -0
  24. package/dist/commands/link.js +1121 -0
  25. package/dist/commands/neon_auth.js +1028 -0
  26. package/dist/commands/operations.js +28 -0
  27. package/dist/commands/orgs.js +24 -0
  28. package/dist/commands/projects.js +372 -0
  29. package/dist/commands/psql.js +62 -0
  30. package/dist/commands/roles.js +65 -0
  31. package/dist/commands/schema_diff.js +151 -0
  32. package/dist/commands/set_context.js +29 -0
  33. package/dist/commands/status.js +40 -0
  34. package/dist/commands/user.js +15 -0
  35. package/dist/commands/vpc_endpoints.js +134 -0
  36. package/dist/config.js +11 -0
  37. package/dist/config_format.js +72 -0
  38. package/dist/context.js +177 -0
  39. package/dist/current_branch_fast_path.js +55 -0
  40. package/dist/dev/env.js +240 -0
  41. package/dist/dev/functions.js +70 -0
  42. package/dist/dev/inputs.js +63 -0
  43. package/dist/dev/runtime.js +146 -0
  44. package/dist/env.js +36 -0
  45. package/dist/env_file.js +159 -0
  46. package/dist/errors.js +80 -0
  47. package/dist/functions_api.js +44 -0
  48. package/dist/help.js +146 -0
  49. package/dist/index.js +234 -0
  50. package/dist/log.js +18 -0
  51. package/dist/parameters.gen.js +480 -0
  52. package/dist/pkg.js +25 -0
  53. package/dist/psql/cli.js +53 -0
  54. package/dist/psql/command/cmd_cond.js +437 -0
  55. package/dist/psql/command/cmd_connect.js +820 -0
  56. package/dist/psql/command/cmd_copy.js +1035 -0
  57. package/dist/psql/command/cmd_describe.js +1815 -0
  58. package/dist/psql/command/cmd_format.js +922 -0
  59. package/dist/psql/command/cmd_io.js +2193 -0
  60. package/dist/psql/command/cmd_lo.js +393 -0
  61. package/dist/psql/command/cmd_meta.js +970 -0
  62. package/dist/psql/command/cmd_misc.js +187 -0
  63. package/dist/psql/command/cmd_pipeline.js +1148 -0
  64. package/dist/psql/command/cmd_restrict.js +171 -0
  65. package/dist/psql/command/cmd_show.js +766 -0
  66. package/dist/psql/command/dispatch.js +343 -0
  67. package/dist/psql/command/inputQueue.js +42 -0
  68. package/dist/psql/command/shared.js +71 -0
  69. package/dist/psql/complete/filenames.js +139 -0
  70. package/dist/psql/complete/index.js +104 -0
  71. package/dist/psql/complete/matcher.js +315 -0
  72. package/dist/psql/complete/psqlVars.js +247 -0
  73. package/dist/psql/complete/queries.js +493 -0
  74. package/dist/psql/complete/rules.js +2424 -0
  75. package/dist/psql/core/common.js +1253 -0
  76. package/dist/psql/core/help.js +576 -0
  77. package/dist/psql/core/mainloop.js +1360 -0
  78. package/dist/psql/core/prompt.js +440 -0
  79. package/dist/psql/core/settings.js +684 -0
  80. package/dist/psql/core/sqlHelp.js +1066 -0
  81. package/dist/psql/core/startup.js +846 -0
  82. package/dist/psql/core/syncVars.js +116 -0
  83. package/dist/psql/core/variables.js +287 -0
  84. package/dist/psql/describe/formatters.js +1290 -0
  85. package/dist/psql/describe/processNamePattern.js +270 -0
  86. package/dist/psql/describe/queries.js +2378 -0
  87. package/dist/psql/describe/versionGate.js +43 -0
  88. package/dist/psql/index.js +2030 -0
  89. package/dist/psql/io/history.js +299 -0
  90. package/dist/psql/io/input.js +120 -0
  91. package/dist/psql/io/lineEditor/buffer.js +325 -0
  92. package/dist/psql/io/lineEditor/complete.js +227 -0
  93. package/dist/psql/io/lineEditor/filename.js +159 -0
  94. package/dist/psql/io/lineEditor/index.js +894 -0
  95. package/dist/psql/io/lineEditor/keymap.js +745 -0
  96. package/dist/psql/io/lineEditor/vt100.js +363 -0
  97. package/dist/psql/io/pgpass.js +202 -0
  98. package/dist/psql/io/pgservice.js +194 -0
  99. package/dist/psql/io/psqlrc.js +422 -0
  100. package/dist/psql/print/aligned.js +1765 -0
  101. package/dist/psql/print/asciidoc.js +248 -0
  102. package/dist/psql/print/crosstab.js +463 -0
  103. package/dist/psql/print/csv.js +95 -0
  104. package/dist/psql/print/html.js +258 -0
  105. package/dist/psql/print/json.js +96 -0
  106. package/dist/psql/print/latex.js +396 -0
  107. package/dist/psql/print/pager.js +267 -0
  108. package/dist/psql/print/troff.js +258 -0
  109. package/dist/psql/print/unaligned.js +119 -0
  110. package/dist/psql/print/units.js +135 -0
  111. package/dist/psql/scanner/slash.js +515 -0
  112. package/dist/psql/scanner/sql.js +914 -0
  113. package/dist/psql/scanner/stringutils.js +394 -0
  114. package/dist/psql/types/backslash.js +1 -0
  115. package/dist/psql/types/connection.js +1 -0
  116. package/dist/psql/types/index.js +7 -0
  117. package/dist/psql/types/printer.js +1 -0
  118. package/dist/psql/types/repl.js +1 -0
  119. package/dist/psql/types/scanner.js +24 -0
  120. package/dist/psql/types/settings.js +1 -0
  121. package/dist/psql/types/variables.js +1 -0
  122. package/dist/psql/wire/connection.js +2858 -0
  123. package/dist/psql/wire/copy.js +108 -0
  124. package/dist/psql/wire/notify.js +59 -0
  125. package/dist/psql/wire/pipeline.js +521 -0
  126. package/dist/psql/wire/protocol.js +466 -0
  127. package/dist/psql/wire/sasl.js +296 -0
  128. package/dist/psql/wire/tls.js +602 -0
  129. package/dist/storage_api.js +147 -0
  130. package/dist/test_utils/fixtures.js +122 -0
  131. package/dist/test_utils/oauth_server.js +9 -0
  132. package/dist/types.js +1 -0
  133. package/dist/utils/api_enums.js +33 -0
  134. package/dist/utils/auth.js +5 -0
  135. package/dist/utils/branch_notice.js +22 -0
  136. package/dist/utils/branch_picker.js +103 -0
  137. package/dist/utils/compute_units.js +28 -0
  138. package/dist/utils/enrichers.js +161 -0
  139. package/dist/utils/esbuild.js +158 -0
  140. package/dist/utils/formats.js +18 -0
  141. package/dist/utils/middlewares.js +20 -0
  142. package/dist/utils/package_manager.js +68 -0
  143. package/dist/utils/point_in_time.js +56 -0
  144. package/dist/utils/psql.js +120 -0
  145. package/dist/utils/string.js +5 -0
  146. package/dist/utils/ui.js +59 -0
  147. package/dist/utils/zip.js +4 -0
  148. package/dist/writer.js +97 -0
  149. package/package.json +117 -14
  150. package/.jshintrc +0 -3
  151. package/CHANGELOG.markdown +0 -22
  152. package/LICENSE +0 -21
  153. package/README.markdown +0 -76
  154. package/bower.json +0 -23
  155. package/license.txt +0 -9
  156. package/neon.js +0 -234
  157. package/stdlib/bubbling_support.js +0 -32
  158. package/stdlib/custom_event.js +0 -54
  159. package/stdlib/custom_event_support.js +0 -195
  160. package/stdlib/index.js +0 -7
  161. package/stdlib/node_support.js +0 -123
  162. package/stdlib/widget.js +0 -340
  163. package/test/neon_browser.html +0 -11
  164. package/test/neon_stdlib_browser.html +0 -15
  165. package/test/neon_stdlib_test.js +0 -84
  166. package/test/neon_test.js +0 -64
@@ -0,0 +1,2030 @@
1
+ import { existsSync, promises as fs } from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { createCondStack } from "./command/cmd_cond.js";
5
+ import { defaultRegistry } from "./command/dispatch.js";
6
+ import { EXIT_BADCONN, EXIT_FAILURE, EXIT_SUCCESS, EXIT_USER, runMainLoop, } from "./core/mainloop.js";
7
+ import { applyEnvOverrides, defaultSettings } from "./core/settings.js";
8
+ import { applyStartupArgs, parseStartupArgs } from "./core/startup.js";
9
+ import { syncConnectionVars } from "./core/syncVars.js";
10
+ import { createVarStore } from "./core/variables.js";
11
+ import { loadPgPass } from "./io/pgpass.js";
12
+ import { loadPgServices } from "./io/pgservice.js";
13
+ import { executeInputString, loadPsqlrc } from "./io/psqlrc.js";
14
+ import { PgConnection } from "./wire/connection.js";
15
+ /**
16
+ * Embedded TypeScript psql entrypoint.
17
+ *
18
+ * Argv shape mirrors the legacy native-psql call site:
19
+ * argv[0] = connection URI (postgresql://user:pw@host:port/db?sslmode=...)
20
+ * OR an empty string `''` when the caller provides the
21
+ * connection target via libpq flags (-h/-p/-U/-d) and/or PG*
22
+ * env. The downstream layered resolver picks those up just like
23
+ * upstream psql does.
24
+ * argv[1..] = forwarded psql args (parsed by `parseStartupArgs`).
25
+ */
26
+ export const runPsql = async (argv, stdio = {}) => {
27
+ const stdin = stdio.stdin ?? process.stdin;
28
+ const stdout = stdio.stdout ?? process.stdout;
29
+ const stderr = stdio.stderr ?? process.stderr;
30
+ const connectionUri = argv[0] ?? "";
31
+ // Parse argv[0] in one of three shapes:
32
+ // - URI scheme (`postgres://…` / `postgresql://…`): the URI-partial
33
+ // parser handles authority, query, and `?service=…`.
34
+ // - libpq conninfo string (`key=value …`, no scheme): `parseConninfo`
35
+ // extracts each known key (including `service`).
36
+ // - Bare database name (e.g. `mydb`): no parsing; the rest of the
37
+ // resolver picks up host/port/user/etc. from env/pgpass/service/
38
+ // defaults.
39
+ //
40
+ // `looksLikeConnectionString` (libpq parity: `recognized_connection_
41
+ // string()`) decides between the first two and the third.
42
+ //
43
+ // When `connectionUri` is empty (the standalone-psql shim case), we
44
+ // skip parsing entirely and rely on libpq flags + env to populate the
45
+ // ConnectOptions layers.
46
+ let uriPartial = {};
47
+ let uriService;
48
+ if (connectionUri !== "" && looksLikeConnectionString(connectionUri)) {
49
+ try {
50
+ if (connectionUri.startsWith("postgres://") ||
51
+ connectionUri.startsWith("postgresql://")) {
52
+ uriPartial = parseConnectionUriPartial(connectionUri);
53
+ uriService = parseConnectionUriService(connectionUri);
54
+ }
55
+ else {
56
+ // Bare `key=value …` conninfo string. `parseConninfo` parks the
57
+ // service name on a private `_service` staging slot (it's not
58
+ // part of ConnectOptions); pull it out so the layered resolver
59
+ // can look it up.
60
+ const parsed = parseConninfo(connectionUri);
61
+ if (typeof parsed._service === "string" &&
62
+ parsed._service.length > 0) {
63
+ uriService = parsed._service;
64
+ }
65
+ delete parsed._service;
66
+ uriPartial = parsed;
67
+ }
68
+ }
69
+ catch (err) {
70
+ stderr.write(`psql: error: ${err.message}\n`);
71
+ return EXIT_BADCONN;
72
+ }
73
+ }
74
+ // Parse psql args (argv[1..]). argv[0] is the connection URI consumed above.
75
+ const parsed = parseStartupArgs(argv.slice(1));
76
+ if ("kind" in parsed) {
77
+ if (parsed.kind === "help" || parsed.kind === "version") {
78
+ stdout.write(parsed.message);
79
+ if (!parsed.message.endsWith("\n"))
80
+ stdout.write("\n");
81
+ return EXIT_SUCCESS;
82
+ }
83
+ stderr.write(`psql: error: ${parsed.message}\n`);
84
+ return EXIT_FAILURE;
85
+ }
86
+ const vars = createVarStore();
87
+ const settings = defaultSettings(vars);
88
+ applyEnvOverrides(settings, process.env);
89
+ // Track interactive-ness from the actual stdin we'll read.
90
+ settings.notty = !stdin.isTTY;
91
+ // Resolve external configuration sources (pgpass, pg_service.conf) before
92
+ // running the layered merge. `loadPgPass` always degrades silently to
93
+ // an empty result. `loadPgServices` only errors when the user named a
94
+ // missing file via `$PGSERVICEFILE` (libpq parity for `006_service.pl`);
95
+ // bubble that out as a connection error.
96
+ const pgpassEntries = await loadPgPass(undefined, {
97
+ env: process.env,
98
+ stderr,
99
+ });
100
+ let services;
101
+ try {
102
+ services = await loadPgServices();
103
+ }
104
+ catch (err) {
105
+ stderr.write(`psql: error: ${err.message}\n`);
106
+ return EXIT_BADCONN;
107
+ }
108
+ let resolved;
109
+ try {
110
+ resolved = applyStartupArgs(parsed, settings, undefined, {
111
+ env: process.env,
112
+ uriPartial,
113
+ serviceName: uriService,
114
+ pgpassEntries,
115
+ services,
116
+ });
117
+ }
118
+ catch (err) {
119
+ // `resolveLayeredConnect` throws on unknown service name (libpq
120
+ // parity). Surface as a connection-setup error and bail.
121
+ stderr.write(`psql: error: ${err.message}\n`);
122
+ return EXIT_BADCONN;
123
+ }
124
+ const { connect: connectOpts, preActions } = resolved;
125
+ let connection;
126
+ try {
127
+ connection = await PgConnection.connect(connectOpts);
128
+ }
129
+ catch (err) {
130
+ const e = err;
131
+ stderr.write(`psql: error: connection to server failed: ${e.message ?? String(err)}\n`);
132
+ return EXIT_BADCONN;
133
+ }
134
+ settings.db = connection;
135
+ // Mirror upstream psql's SyncVariables(): populate the connection-driven
136
+ // psql vars (DBNAME/USER/HOST/PORT/ENCODING/SERVER_VERSION_*) so scripts
137
+ // can interpolate `:DBNAME`, `:USER`, etc. from the first prompt onward.
138
+ syncConnectionVars(settings.vars, connection);
139
+ const registry = defaultRegistry();
140
+ const cond = createCondStack();
141
+ const ctx = {
142
+ settings,
143
+ registry,
144
+ cond,
145
+ stdin,
146
+ stdout,
147
+ stderr,
148
+ };
149
+ try {
150
+ // Startup banner — mirrors upstream psql's
151
+ // psql (<client>, server <server>)
152
+ // SSL connection (protocol: …, cipher: …)
153
+ // Type "help" for help.
154
+ // Suppressed in quiet mode and when stdin isn't a TTY (scripted use).
155
+ if (!settings.quiet && !settings.notty && preActions.length === 0) {
156
+ writeStartupBanner(connection, stdout);
157
+ }
158
+ // Run .psqlrc unless -X was specified.
159
+ await loadPsqlrc(ctx, { skip: parsed.noPsqlrc, env: process.env });
160
+ // If the user supplied -c / -f actions, execute them sequentially and
161
+ // exit (mirrors upstream psql behaviour). Otherwise, fall through to
162
+ // the REPL.
163
+ if (preActions.length > 0) {
164
+ return await runPreActions(ctx, preActions, parsed.singleTransaction);
165
+ }
166
+ return await runMainLoop(ctx);
167
+ }
168
+ finally {
169
+ try {
170
+ await connection.close();
171
+ }
172
+ catch {
173
+ // ignore close errors
174
+ }
175
+ }
176
+ };
177
+ /**
178
+ * Execute the ordered list of `-c` / `-f` actions and return the upstream
179
+ * psql exit code.
180
+ *
181
+ * Upstream `process_psqlrc_and_targets()` (in `startup.c`) plus the
182
+ * dispatcher in `MainLoop()` cooperate to give each switch one of three
183
+ * outcomes:
184
+ *
185
+ * - `-c "SQL or \backslash"`:
186
+ * * a client-side failure (e.g. bad `\copy`) marks the SWITCH itself
187
+ * as failed → the overall exit code is non-zero, even when
188
+ * ON_ERROR_STOP is off and the transaction commits.
189
+ * * a server-side failure follows the same rule.
190
+ *
191
+ * - `-f file`:
192
+ * * `process_file()` returns a success status by default, so a
193
+ * failing statement inside the file does NOT bubble up to the
194
+ * outer exit code (without ON_ERROR_STOP). Only an I/O failure
195
+ * opening the file flips the switch status.
196
+ *
197
+ * - `--single-transaction`:
198
+ * * before the FIRST action, issue `BEGIN`. After the LAST action,
199
+ * issue `COMMIT` (success) or `ROLLBACK` (when ON_ERROR_STOP fired
200
+ * and we stopped early).
201
+ * * Without ON_ERROR_STOP, the transaction commits even when some
202
+ * individual statements failed — the failing statements only
203
+ * influence the exit code (see the `-c` / `-f` distinction above).
204
+ */
205
+ const runPreActions = async (ctx, preActions, singleTransaction) => {
206
+ const { settings, stderr } = ctx;
207
+ let status = EXIT_SUCCESS;
208
+ let beganTransaction = false;
209
+ let earlyStopOnError = false;
210
+ let connectionLost = false;
211
+ // --single-transaction: wrap the entire batch in BEGIN ... COMMIT/ROLLBACK.
212
+ // We do this with `db.execSimple` directly so the wrapper does not itself
213
+ // count as a "failed switch" for exit-code purposes.
214
+ if (singleTransaction && settings.db) {
215
+ try {
216
+ await settings.db.execSimple("BEGIN");
217
+ beganTransaction = true;
218
+ }
219
+ catch (err) {
220
+ const msg = err instanceof Error ? err.message : String(err);
221
+ stderr.write(`psql: error: could not begin transaction: ${msg}\n`);
222
+ return EXIT_FAILURE;
223
+ }
224
+ }
225
+ for (const action of preActions) {
226
+ if (connectionLost)
227
+ break;
228
+ if (action.kind === "command") {
229
+ let outcome;
230
+ try {
231
+ outcome = await executeInputString(action.sql, ctx, {
232
+ print: true,
233
+ });
234
+ }
235
+ catch (err) {
236
+ // Defensive: executeInputString shouldn't throw, but if a downstream
237
+ // command bubbles an exception we still want to surface it as a
238
+ // failed switch rather than crashing.
239
+ const msg = err instanceof Error ? err.message : String(err);
240
+ stderr.write(`psql: ERROR: ${msg}\n`);
241
+ status = EXIT_FAILURE;
242
+ if (settings.onErrorStop) {
243
+ earlyStopOnError = true;
244
+ break;
245
+ }
246
+ continue;
247
+ }
248
+ if (outcome.connectionLost) {
249
+ connectionLost = true;
250
+ status = EXIT_BADCONN;
251
+ break;
252
+ }
253
+ // For `-c`: any failure flips the exit code to EXIT_FAILURE (1) — real
254
+ // psql exits 1 for a `-c` error regardless of ON_ERROR_STOP, NOT the
255
+ // EXIT_USER (3) used for ON_ERROR_STOP aborts in script/stdin mode
256
+ // (verified on psql 18.4). The `\copy` errors are on stderr.
257
+ if (outcome.hadError || outcome.stoppedOnError) {
258
+ status = EXIT_FAILURE;
259
+ }
260
+ if (outcome.stoppedOnError) {
261
+ earlyStopOnError = true;
262
+ break;
263
+ }
264
+ }
265
+ else {
266
+ // -f file: I/O failure (missing file, permission denied) is a hard
267
+ // EXIT_FAILURE on the switch. Per-statement failures inside the file
268
+ // are SWALLOWED for exit-code purposes (mirrors `process_file()` in
269
+ // upstream, which only escalates to a stop when ON_ERROR_STOP fires).
270
+ let contents;
271
+ try {
272
+ contents = await fs.readFile(action.path, "utf8");
273
+ }
274
+ catch (err) {
275
+ const msg = err instanceof Error ? err.message : String(err);
276
+ stderr.write(`psql: error: ${msg}\n`);
277
+ status = EXIT_FAILURE;
278
+ if (settings.onErrorStop) {
279
+ earlyStopOnError = true;
280
+ break;
281
+ }
282
+ continue;
283
+ }
284
+ let outcome;
285
+ try {
286
+ outcome = await executeInputString(contents, ctx, {
287
+ print: true,
288
+ });
289
+ }
290
+ catch (err) {
291
+ const msg = err instanceof Error ? err.message : String(err);
292
+ stderr.write(`psql: ERROR: ${msg}\n`);
293
+ status = EXIT_USER;
294
+ if (settings.onErrorStop) {
295
+ earlyStopOnError = true;
296
+ break;
297
+ }
298
+ continue;
299
+ }
300
+ if (outcome.connectionLost) {
301
+ connectionLost = true;
302
+ status = EXIT_BADCONN;
303
+ break;
304
+ }
305
+ if (outcome.stoppedOnError) {
306
+ status = EXIT_USER;
307
+ earlyStopOnError = true;
308
+ break;
309
+ }
310
+ // Per-statement errors inside the file do NOT propagate to the outer
311
+ // exit code (matches upstream `process_file` returning success).
312
+ }
313
+ }
314
+ // Wrap up the single-transaction envelope. If we stopped early on error
315
+ // and ON_ERROR_STOP fired, roll back. Otherwise commit — upstream commits
316
+ // even when individual statements failed without ON_ERROR_STOP.
317
+ if (beganTransaction && settings.db && !connectionLost) {
318
+ const closing = earlyStopOnError ? "ROLLBACK" : "COMMIT";
319
+ try {
320
+ await settings.db.execSimple(closing);
321
+ }
322
+ catch (err) {
323
+ // If COMMIT fails the data is gone; surface it but don't override an
324
+ // existing error status. If we tried to COMMIT cleanly and that
325
+ // failed, escalate to EXIT_FAILURE so the caller knows the batch
326
+ // didn't go through.
327
+ const msg = err instanceof Error ? err.message : String(err);
328
+ stderr.write(`psql: error: ${closing} failed: ${msg}\n`);
329
+ if (status === EXIT_SUCCESS)
330
+ status = EXIT_FAILURE;
331
+ }
332
+ }
333
+ return status;
334
+ };
335
+ const writeStartupBanner = (connection, out) => {
336
+ const serverVersion = connection.parameterStatus("server_version") ?? "unknown";
337
+ // Client identifier. Matches upstream's `psql (18.4, server X.Y)` shape
338
+ // but signals that this is the embedded TS implementation so users can tell
339
+ // when they're on the fallback path.
340
+ out.write(`psql-ts (neonctl, server ${serverVersion})\n`);
341
+ const tls = connection.getTlsInfo();
342
+ if (tls) {
343
+ const parts = [
344
+ `protocol: ${tls.protocol}`,
345
+ `cipher: ${tls.cipher}`,
346
+ `compression: ${tls.compression}`,
347
+ ];
348
+ if (tls.alpn)
349
+ parts.push(`ALPN: ${tls.alpn}`);
350
+ out.write(`SSL connection (${parts.join(", ")})\n`);
351
+ }
352
+ out.write('Type "help" for help.\n\n');
353
+ };
354
+ // Recognized libpq connection parameter keywords (subset matching
355
+ // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS).
356
+ // We don't necessarily honor every value (e.g. hostaddr, krbsrvname), but we
357
+ // recognize them as valid keys so callers don't get a spurious "unknown key"
358
+ // rejection for a libpq-spec key they expect to work.
359
+ const KNOWN_QUERY_KEYS = new Set([
360
+ "host",
361
+ "hostaddr",
362
+ "port",
363
+ "dbname",
364
+ "user",
365
+ "password",
366
+ "passfile",
367
+ "channel_binding",
368
+ "require_auth",
369
+ "connect_timeout",
370
+ "client_encoding",
371
+ "options",
372
+ "application_name",
373
+ "fallback_application_name",
374
+ "keepalives",
375
+ "keepalives_idle",
376
+ "keepalives_interval",
377
+ "keepalives_count",
378
+ "sslmode",
379
+ "sslnegotiation",
380
+ "sslcompression",
381
+ "sslcert",
382
+ "sslkey",
383
+ "sslcertmode",
384
+ "sslrootcert",
385
+ "sslcrl",
386
+ "sslcrldir",
387
+ "sslkeylogfile",
388
+ "sslsni",
389
+ "requirepeer",
390
+ "ssl_min_protocol_version",
391
+ "ssl_max_protocol_version",
392
+ "krbsrvname",
393
+ "gsslib",
394
+ "gssencmode",
395
+ "service",
396
+ "target_session_attrs",
397
+ "load_balance_hosts",
398
+ "replication",
399
+ ]);
400
+ /**
401
+ * Tokenize a postgres connection URI into raw components.
402
+ *
403
+ * Hand-rolled rather than using `new URL()` because libpq accepts shapes
404
+ * the WHATWG URL parser rejects, e.g. `postgresql://user@` (userinfo with
405
+ * no host), `postgres://:12345/` (port-only), `postgres://uri-user@/db`
406
+ * (userinfo with empty host). The upstream conformance suite in
407
+ * `src/interfaces/libpq/t/001_uri.pl` exercises these forms.
408
+ */
409
+ const tokenizeConnectionUri = (uri) => {
410
+ // Strip scheme. Only postgres:// and postgresql:// are accepted; libpq
411
+ // rejects everything else with a "missing schema" error.
412
+ let rest;
413
+ if (uri.startsWith("postgresql://")) {
414
+ rest = uri.slice("postgresql://".length);
415
+ }
416
+ else if (uri.startsWith("postgres://")) {
417
+ rest = uri.slice("postgres://".length);
418
+ }
419
+ else {
420
+ throw new Error(`unsupported scheme in URI: ${uri}`);
421
+ }
422
+ // Split off query string.
423
+ let query = "";
424
+ const qIdx = rest.indexOf("?");
425
+ if (qIdx >= 0) {
426
+ query = rest.slice(qIdx + 1);
427
+ rest = rest.slice(0, qIdx);
428
+ }
429
+ // Split off path (database).
430
+ let database;
431
+ const pIdx = rest.indexOf("/");
432
+ if (pIdx >= 0) {
433
+ const pathRaw = rest.slice(pIdx + 1);
434
+ database = pathRaw === "" ? undefined : decodePercent(pathRaw);
435
+ rest = rest.slice(0, pIdx);
436
+ }
437
+ // What's left is the authority: [userinfo@][host[:port]]
438
+ let userinfo;
439
+ const atIdx = rest.lastIndexOf("@");
440
+ if (atIdx >= 0) {
441
+ userinfo = rest.slice(0, atIdx);
442
+ rest = rest.slice(atIdx + 1);
443
+ }
444
+ let user;
445
+ let password;
446
+ if (userinfo !== undefined) {
447
+ const colon = userinfo.indexOf(":");
448
+ if (colon >= 0) {
449
+ user = decodePercent(userinfo.slice(0, colon));
450
+ password = decodePercent(userinfo.slice(colon + 1));
451
+ }
452
+ else {
453
+ user = decodePercent(userinfo);
454
+ }
455
+ }
456
+ // hostport: either [ipv6]:port, [ipv6], host:port, or host. With multi-host
457
+ // (libpq 10+), the authority may be a comma-separated list:
458
+ // `h1:5432,h2,[::1]:5433`
459
+ // We split on commas at the top level (i.e. not inside `[...]` IPv6
460
+ // brackets) and parse each segment using the single-host grammar.
461
+ const tuples = splitAuthorityTuples(rest, uri);
462
+ let host;
463
+ let port;
464
+ const hosts = [];
465
+ for (const tuple of tuples) {
466
+ const parsed = parseAuthorityTuple(tuple, uri);
467
+ if (parsed.host !== undefined) {
468
+ hosts.push({ host: parsed.host, port: parsed.port });
469
+ }
470
+ }
471
+ if (hosts.length > 0) {
472
+ host = hosts[0].host;
473
+ port = hosts[0].port;
474
+ }
475
+ const queryMap = parseQuery(query);
476
+ return {
477
+ user,
478
+ password,
479
+ host,
480
+ port,
481
+ ...(hosts.length > 1 ? { hosts } : {}),
482
+ database,
483
+ query: queryMap,
484
+ };
485
+ };
486
+ /**
487
+ * Split a multi-host authority string into one tuple per top-level comma.
488
+ * IPv6 bracket regions are atomic — commas inside `[…]` don't split.
489
+ *
490
+ * Examples:
491
+ * `h1:5432,h2,h3:5434` -> ['h1:5432','h2','h3:5434']
492
+ * `[::1]:5432,[2001:db8::1]` -> ['[::1]:5432','[2001:db8::1]']
493
+ * `h1` -> ['h1']
494
+ * `` -> [''] (single empty tuple — caller may
495
+ * treat as no-host)
496
+ */
497
+ const splitAuthorityTuples = (rest, uri) => {
498
+ if (rest === "")
499
+ return [""];
500
+ const tuples = [];
501
+ let start = 0;
502
+ let i = 0;
503
+ while (i < rest.length) {
504
+ const ch = rest[i];
505
+ if (ch === "[") {
506
+ const closeIdx = rest.indexOf("]", i);
507
+ if (closeIdx < 0) {
508
+ throw new Error(`missing matching "]" in IPv6 host address: ${uri}`);
509
+ }
510
+ i = closeIdx + 1;
511
+ continue;
512
+ }
513
+ if (ch === ",") {
514
+ tuples.push(rest.slice(start, i));
515
+ i += 1;
516
+ start = i;
517
+ continue;
518
+ }
519
+ i += 1;
520
+ }
521
+ tuples.push(rest.slice(start));
522
+ return tuples;
523
+ };
524
+ const parseAuthorityTuple = (tuple, uri) => {
525
+ if (tuple === "")
526
+ return {};
527
+ if (tuple.startsWith("[")) {
528
+ const closeIdx = tuple.indexOf("]");
529
+ if (closeIdx < 0) {
530
+ throw new Error(`missing matching "]" in IPv6 host address: ${uri}`);
531
+ }
532
+ const host = tuple.slice(1, closeIdx);
533
+ if (host === "") {
534
+ throw new Error(`IPv6 host address may not be empty: ${uri}`);
535
+ }
536
+ const after = tuple.slice(closeIdx + 1);
537
+ if (after === "")
538
+ return { host };
539
+ if (after.startsWith(":")) {
540
+ return { host, port: after.slice(1) };
541
+ }
542
+ throw new Error(`unexpected characters after IPv6 host address in URI: ${uri}`);
543
+ }
544
+ const colon = tuple.indexOf(":");
545
+ if (colon >= 0) {
546
+ return {
547
+ host: decodePercent(tuple.slice(0, colon)),
548
+ port: tuple.slice(colon + 1),
549
+ };
550
+ }
551
+ return { host: decodePercent(tuple) };
552
+ };
553
+ const parseQuery = (raw) => {
554
+ const out = new Map();
555
+ if (raw === "")
556
+ return out;
557
+ for (const segment of raw.split("&")) {
558
+ if (segment === "")
559
+ continue;
560
+ const eq = segment.indexOf("=");
561
+ if (eq < 0) {
562
+ // libpq: every query parameter must be `key=value`. Bare keys (no `=`)
563
+ // are rejected. Matches the upstream 001_uri.pl `?zzz` and
564
+ // `?value1&value2` cases.
565
+ throw new Error(`missing "=" after "${segment.trim()}" in connection info string`);
566
+ }
567
+ const keyRaw = segment.slice(0, eq);
568
+ const valueRaw = segment.slice(eq + 1);
569
+ // libpq rejects an extra `=` in either key or value; matches the
570
+ // `?key=key=value` upstream case.
571
+ if (valueRaw.includes("=")) {
572
+ throw new Error(`extra "=" in query parameter "${decodePercent(keyRaw).trim()}"`);
573
+ }
574
+ const key = decodePercent(keyRaw).trim();
575
+ const value = decodePercent(valueRaw).trim();
576
+ if (key === "")
577
+ continue;
578
+ if (!KNOWN_QUERY_KEYS.has(key)) {
579
+ throw new Error(`invalid URI query parameter: "${key}"`);
580
+ }
581
+ out.set(key, value);
582
+ }
583
+ return out;
584
+ };
585
+ /**
586
+ * Percent-decode a URI component. libpq strictly validates percent-encoding:
587
+ * - `%XX` must be two hex digits
588
+ * - bare `%` or `%X` is invalid
589
+ * - `%00` is forbidden (NUL bytes can't appear in connection params)
590
+ *
591
+ * `decodeURIComponent` throws URIError on malformed escapes — we surface that
592
+ * as a clear Error. It accepts `%00` (returns `\0`); we explicitly reject.
593
+ */
594
+ const decodePercent = (s) => {
595
+ let decoded;
596
+ try {
597
+ decoded = decodeURIComponent(s);
598
+ }
599
+ catch {
600
+ throw new Error(`invalid percent-encoded token in URI: ${s}`);
601
+ }
602
+ if (decoded.includes("\x00")) {
603
+ throw new Error(`forbidden NUL byte (%00) in URI: ${s}`);
604
+ }
605
+ return decoded;
606
+ };
607
+ export const parseConnectionUri = (uri) => {
608
+ const raw = tokenizeConnectionUri(uri);
609
+ // libpq-style: query string can override authority components.
610
+ const queryUser = raw.query.get("user");
611
+ const queryPassword = raw.query.get("password");
612
+ const queryPort = raw.query.get("port");
613
+ const queryDbname = raw.query.get("dbname");
614
+ const queryHost = raw.query.get("host");
615
+ // Multi-host: either from the authority (`h1,h2,h3:5434`) or from
616
+ // `?host=h1,h2,h3&port=5432,5433,5434`. Query-string overrides authority
617
+ // (matching libpq: query params take precedence over URI structural
618
+ // components). Both `host=` and `port=` lists must be the same length OR
619
+ // a single value (broadcast).
620
+ const hostsTuples = computeHostsTuples({
621
+ rawHost: raw.host,
622
+ rawPort: raw.port,
623
+ rawAuthorityHosts: raw.hosts,
624
+ queryHost,
625
+ queryPort,
626
+ });
627
+ // Single-host fallbacks (preserve current behaviour for the `host` / `port`
628
+ // surface — the wire layer prefers `hosts` when set).
629
+ const host = hostsTuples.length > 0 && hostsTuples[0].host !== ""
630
+ ? hostsTuples[0].host
631
+ : "localhost";
632
+ const port = hostsTuples.length > 0 ? hostsTuples[0].port : 5432;
633
+ const user = queryUser !== undefined && queryUser !== ""
634
+ ? queryUser
635
+ : raw.user !== undefined && raw.user !== ""
636
+ ? raw.user
637
+ : (process.env.USER ?? "");
638
+ const password = queryPassword ?? raw.password;
639
+ const database = queryDbname ?? raw.database ?? user;
640
+ let ssl = normalizeSslMode(raw.query.get("sslmode") ?? null);
641
+ const channelBinding = normalizeChannelBinding(raw.query.get("channel_binding") ?? null);
642
+ // GSSAPI is unsupported (no native Kerberos dep); validate+reject require.
643
+ validateGssEncMode(raw.query.get("gssencmode") ?? null);
644
+ const options = raw.query.get("options");
645
+ // Match upstream psql: default `application_name` to `'psql'` so users see
646
+ // the expected value in `pg_stat_activity`. The neonctl-specific identifier
647
+ // is still discoverable via the User-Agent the protocol layer sends.
648
+ const applicationName = raw.query.get("application_name") ?? "psql";
649
+ const replication = normalizeReplication(raw.query.get("replication") ?? null);
650
+ const targetSessionAttrs = normalizeTargetSessionAttrs(raw.query.get("target_session_attrs") ?? null);
651
+ const loadBalanceHosts = normalizeLoadBalanceHosts(raw.query.get("load_balance_hosts") ?? null);
652
+ // libpq PEM file paths. Empty string is treated as "not set" so a URI
653
+ // like `?sslcert=` doesn't surface as an attempt to load `""` from disk.
654
+ const sslcert = nonEmpty(raw.query.get("sslcert"));
655
+ const sslkey = nonEmpty(raw.query.get("sslkey"));
656
+ const sslcertmode = normalizeSslCertMode(raw.query.get("sslcertmode") ?? null);
657
+ const sslnegotiation = normalizeSslNegotiation(raw.query.get("sslnegotiation") ?? null);
658
+ const sslrootcert = nonEmpty(raw.query.get("sslrootcert"));
659
+ const sslcrl = nonEmpty(raw.query.get("sslcrl"));
660
+ const sslcrldir = nonEmpty(raw.query.get("sslcrldir"));
661
+ const sslkeylogfile = nonEmpty(raw.query.get("sslkeylogfile"));
662
+ // libpq sslsni / keepalives toggles (0/1) + keepalives_idle (seconds) +
663
+ // requirepeer (OS user, validated but not enforceable in Node).
664
+ const sslsni = parseLibpqBool(nonEmpty(raw.query.get("sslsni")));
665
+ const keepalives = parseLibpqBool(nonEmpty(raw.query.get("keepalives")));
666
+ const keepalivesIdle = parseKeepalivesIdle(nonEmpty(raw.query.get("keepalives_idle")));
667
+ const requirepeer = nonEmpty(raw.query.get("requirepeer"));
668
+ // libpq: `sslrootcert=system` raises the effective sslmode to verify-full.
669
+ // verify-full is the strongest mode, so this only ever raises it.
670
+ if (sslrootcert === "system" && ssl !== "verify-full") {
671
+ ssl = "verify-full";
672
+ }
673
+ // libpq `hostaddr`: a fixed IP that bypasses DNS while `host` still drives
674
+ // TLS SNI / cert verification. Empty string is "not set".
675
+ const hostaddr = nonEmpty(raw.query.get("hostaddr"));
676
+ const sslMinProtocolVersion = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_min_protocol_version")), "ssl_min_protocol_version");
677
+ const sslMaxProtocolVersion = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_max_protocol_version")), "ssl_max_protocol_version");
678
+ assertTlsProtocolRange(sslMinProtocolVersion, sslMaxProtocolVersion);
679
+ assertTlsMaxProtocolSupported(sslMaxProtocolVersion);
680
+ // libpq rejects `sslnegotiation=direct` paired with a weak sslmode. The URI
681
+ // surface always resolves a concrete `ssl` (defaulting to 'prefer'), so the
682
+ // check is authoritative here.
683
+ assertSslNegotiationModeCompatible(ssl, sslnegotiation);
684
+ return {
685
+ host,
686
+ port,
687
+ user,
688
+ password,
689
+ database,
690
+ ssl,
691
+ channelBinding,
692
+ applicationName,
693
+ options,
694
+ ...(sslnegotiation !== undefined ? { sslnegotiation } : {}),
695
+ ...(hostaddr !== undefined ? { hostaddr } : {}),
696
+ ...(replication !== undefined ? { replication } : {}),
697
+ ...(hostsTuples.length > 1
698
+ ? {
699
+ hosts: hostsTuples.map((t) => ({
700
+ host: t.host,
701
+ port: t.port,
702
+ })),
703
+ }
704
+ : {}),
705
+ ...(targetSessionAttrs !== undefined ? { targetSessionAttrs } : {}),
706
+ ...(loadBalanceHosts !== undefined ? { loadBalanceHosts } : {}),
707
+ ...(sslcert !== undefined ? { sslcert } : {}),
708
+ ...(sslkey !== undefined ? { sslkey } : {}),
709
+ ...(sslcertmode !== undefined ? { sslcertmode } : {}),
710
+ ...(sslrootcert !== undefined ? { sslrootcert } : {}),
711
+ ...(sslcrl !== undefined ? { sslcrl } : {}),
712
+ ...(sslcrldir !== undefined ? { sslcrldir } : {}),
713
+ ...(sslkeylogfile !== undefined ? { sslkeylogfile } : {}),
714
+ ...(sslsni !== undefined ? { sslsni } : {}),
715
+ ...(keepalives !== undefined ? { keepalives } : {}),
716
+ ...(keepalivesIdle !== undefined ? { keepalivesIdle } : {}),
717
+ ...(requirepeer !== undefined ? { requirepeer } : {}),
718
+ ...(sslMinProtocolVersion !== undefined
719
+ ? { sslMinProtocolVersion }
720
+ : {}),
721
+ ...(sslMaxProtocolVersion !== undefined
722
+ ? { sslMaxProtocolVersion }
723
+ : {}),
724
+ };
725
+ };
726
+ /**
727
+ * Parse a libpq 0/1 boolean connection parameter (`sslsni`, `keepalives`).
728
+ * libpq's `parse_bool_with_len` accepts `1`/`0`, `true`/`false`, `yes`/`no`,
729
+ * `on`/`off` (case-insensitive). Returns `undefined` for unset / empty /
730
+ * unrecognised so the caller falls back to libpq's default (enabled).
731
+ */
732
+ const parseLibpqBool = (raw) => {
733
+ if (raw === undefined || raw === "")
734
+ return undefined;
735
+ switch (raw.toLowerCase()) {
736
+ case "1":
737
+ case "true":
738
+ case "yes":
739
+ case "on":
740
+ return true;
741
+ case "0":
742
+ case "false":
743
+ case "no":
744
+ case "off":
745
+ return false;
746
+ default:
747
+ return undefined;
748
+ }
749
+ };
750
+ /**
751
+ * Parse `keepalives_idle` (seconds, non-negative integer). Returns the value
752
+ * in seconds, or `undefined` if unset / malformed (the wire layer converts to
753
+ * milliseconds for `socket.setKeepAlive`'s `initialDelay`).
754
+ */
755
+ const parseKeepalivesIdle = (raw) => {
756
+ if (raw === undefined || raw === "")
757
+ return undefined;
758
+ const n = Number.parseInt(raw, 10);
759
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
760
+ };
761
+ const nonEmpty = (v) => v === undefined || v === "" ? undefined : v;
762
+ /**
763
+ * Resolve the final {host, port}[] list for a URI:
764
+ *
765
+ * 1. Start from the authority. `?host=`/`?port=` query overrides take
766
+ * precedence (libpq semantics).
767
+ * 2. If `host=h1,h2,…` is supplied, parse the comma-list. Ports come from
768
+ * `port=p1,p2,…`; must match the host count or be a single value
769
+ * (broadcast to all hosts).
770
+ * 3. If only the authority had multi-host tuples (`postgresql://h1,h2/db`),
771
+ * use those.
772
+ * 4. Otherwise fall back to single-host.
773
+ *
774
+ * Validates every port is in 1..65535 and surfaces a clear error otherwise.
775
+ */
776
+ const computeHostsTuples = (input) => {
777
+ const { rawHost, rawPort, rawAuthorityHosts, queryHost, queryPort } = input;
778
+ // Case A: ?host=… overrides the authority host(s). Port resolution still
779
+ // prefers `?port=` (if supplied), but falls back to the authority port so
780
+ // e.g. `postgres://:12345?host=/path/to/socket` keeps `port=12345`.
781
+ if (queryHost !== undefined && queryHost !== "") {
782
+ const hosts = queryHost.split(",").map((h) => h.trim());
783
+ const portList = queryPort !== undefined && queryPort !== ""
784
+ ? queryPort.split(",").map((p) => p.trim())
785
+ : null;
786
+ if (portList !== null &&
787
+ portList.length !== 1 &&
788
+ portList.length !== hosts.length) {
789
+ throw new Error(`could not match ${String(portList.length)} port numbers to ${String(hosts.length)} hosts`);
790
+ }
791
+ return hosts.map((h, idx) => {
792
+ let portStr;
793
+ if (portList !== null) {
794
+ portStr = portList.length === 1 ? portList[0] : portList[idx];
795
+ }
796
+ else {
797
+ // Fall back to the authority port. Multi-host without an explicit
798
+ // ?port= list shares the authority port across all hosts.
799
+ portStr = rawPort;
800
+ }
801
+ return { host: h, port: parsePort(portStr) };
802
+ });
803
+ }
804
+ // Case B: authority carried a comma-list (`postgresql://h1,h2:5433/db`).
805
+ // Query-string `?port=` can still broadcast or pair with this list.
806
+ if (rawAuthorityHosts !== undefined && rawAuthorityHosts.length > 0) {
807
+ const portList = queryPort !== undefined && queryPort !== ""
808
+ ? queryPort.split(",").map((p) => p.trim())
809
+ : null;
810
+ if (portList !== null &&
811
+ portList.length !== 1 &&
812
+ portList.length !== rawAuthorityHosts.length) {
813
+ throw new Error(`could not match ${String(portList.length)} port numbers to ${String(rawAuthorityHosts.length)} hosts`);
814
+ }
815
+ return rawAuthorityHosts.map((t, idx) => ({
816
+ host: t.host,
817
+ port: parsePort(portList !== null
818
+ ? portList.length === 1
819
+ ? portList[0]
820
+ : portList[idx]
821
+ : t.port),
822
+ }));
823
+ }
824
+ // Case C: single-host. Honour `?port=` (single value) if provided.
825
+ const portStr = queryPort ?? rawPort;
826
+ const host = rawHost !== undefined && rawHost !== "" ? rawHost : "";
827
+ return [{ host, port: parsePort(portStr) }];
828
+ };
829
+ const parsePort = (raw) => {
830
+ if (raw === undefined || raw === "")
831
+ return 5432;
832
+ // `Number.parseInt` silently tolerates trailing junk (`parseInt("12345 12")`
833
+ // === 12345), which would let an internal-whitespace value like the upstream
834
+ // `port = 12345 12` URI case sneak through as port 12345. libpq rejects that
835
+ // shape with `invalid integer value "<v>" for connection option "port"`,
836
+ // pointing at the whole bogus value (the whitespace included) rather than a
837
+ // generic out-of-range message. Detect any digits-then-garbage value here so
838
+ // that exact wording fires; genuinely non-numeric (`abc`) and out-of-range
839
+ // (`99999`) values keep the shorter `invalid port:` diagnostic.
840
+ if (/^\d/.test(raw) && !/^\d+$/.test(raw)) {
841
+ throw new Error(`invalid integer value "${raw}" for connection option "port"`);
842
+ }
843
+ const p = Number.parseInt(raw, 10);
844
+ if (!Number.isFinite(p) || p <= 0 || p > 65535) {
845
+ throw new Error(`invalid port: ${raw}`);
846
+ }
847
+ return p;
848
+ };
849
+ /**
850
+ * Parse a libpq-style conninfo string into a `Partial<ConnectOptions>`.
851
+ *
852
+ * The grammar is roughly `key = value` pairs separated by whitespace, where
853
+ * values may be quoted with single quotes (libpq's `\'` is honored as an
854
+ * embedded single-quote, but we don't model the full backslash-escape
855
+ * universe — there's no test corpus that exercises it). Unknown keys are
856
+ * rejected so a typo like `replicate=database` produces a clear error
857
+ * rather than silently dropping the parameter.
858
+ *
859
+ * Recognised keys mirror the URI-side `KNOWN_QUERY_KEYS` allowlist plus
860
+ * the authority-style keys (`host`, `port`, `user`, `dbname`, `password`).
861
+ * `replication` is normalised through `normalizeReplication` for libpq
862
+ * value-set compatibility.
863
+ *
864
+ * Out of scope: percent-decoding (conninfo strings are NOT percent-encoded),
865
+ * `service` resolution from `pg_service.conf`, `passfile` resolution.
866
+ */
867
+ export const parseConninfo = (input) => {
868
+ const out = {};
869
+ let i = 0;
870
+ const n = input.length;
871
+ while (i < n) {
872
+ // Skip whitespace between pairs.
873
+ while (i < n && /\s/.test(input[i]))
874
+ i += 1;
875
+ if (i >= n)
876
+ break;
877
+ // Parse key: chars up to `=` or whitespace.
878
+ const keyStart = i;
879
+ while (i < n && input[i] !== "=" && !/\s/.test(input[i]))
880
+ i += 1;
881
+ const key = input.slice(keyStart, i).toLowerCase();
882
+ if (key === "")
883
+ break;
884
+ // Skip whitespace before `=`.
885
+ while (i < n && /\s/.test(input[i]))
886
+ i += 1;
887
+ if (i >= n || input[i] !== "=") {
888
+ throw new Error(`missing "=" after "${key}" in conninfo string`);
889
+ }
890
+ i += 1; // consume `=`
891
+ // Skip whitespace after `=`.
892
+ while (i < n && /\s/.test(input[i]))
893
+ i += 1;
894
+ // Parse value: either single-quoted (with `\'` and `\\` escapes) or
895
+ // bare up to next whitespace.
896
+ let value;
897
+ if (i < n && input[i] === "'") {
898
+ i += 1; // consume opening quote
899
+ const parts = [];
900
+ while (i < n && input[i] !== "'") {
901
+ if (input[i] === "\\" && i + 1 < n) {
902
+ parts.push(input[i + 1]);
903
+ i += 2;
904
+ }
905
+ else {
906
+ parts.push(input[i]);
907
+ i += 1;
908
+ }
909
+ }
910
+ if (i >= n) {
911
+ throw new Error(`unterminated single quote in conninfo string for key "${key}"`);
912
+ }
913
+ i += 1; // consume closing quote
914
+ value = parts.join("");
915
+ }
916
+ else {
917
+ const valStart = i;
918
+ while (i < n && !/\s/.test(input[i]))
919
+ i += 1;
920
+ value = input.slice(valStart, i);
921
+ }
922
+ applyConninfoPair(out, key, value);
923
+ }
924
+ // Materialise multi-host list. The scalar `host`/`port` already hold the
925
+ // first entry (so single-host callers see no surface change); we only
926
+ // surface `hosts` when the comma-list had ≥2 entries.
927
+ const hostList = out._hostList;
928
+ const portList = out._portList;
929
+ if (hostList !== undefined && hostList.length > 0) {
930
+ if (portList !== undefined &&
931
+ portList.length !== 1 &&
932
+ portList.length !== hostList.length) {
933
+ throw new Error(`could not match ${String(portList.length)} port numbers to ${String(hostList.length)} hosts`);
934
+ }
935
+ if (hostList.length > 1) {
936
+ out.hosts = hostList.map((h, idx) => ({
937
+ host: h,
938
+ port: portList === undefined
939
+ ? (out.port ?? 5432)
940
+ : portList.length === 1
941
+ ? parsePort(portList[0])
942
+ : parsePort(portList[idx]),
943
+ }));
944
+ }
945
+ }
946
+ // Drop the private staging fields before returning to the caller.
947
+ // `_service` is left in place — the layered connect resolver in
948
+ // `core/startup.ts` doesn't see this struct; only `runPsql` extracts
949
+ // and forwards the service name. The caller deletes the slot after
950
+ // reading it.
951
+ delete out._hostList;
952
+ delete out._portList;
953
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
954
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
955
+ return out;
956
+ };
957
+ const applyConninfoPair = (out, key, value) => {
958
+ switch (key) {
959
+ case "host": {
960
+ // Multi-host: `host=h1,h2,h3`. Store the list aside; the post-pass
961
+ // (finalizeConninfo) materialises it into `hosts` + matches up against
962
+ // any `port=p1,p2,p3` list.
963
+ if (value.includes(",")) {
964
+ out._hostList = value.split(",").map((h) => h.trim());
965
+ out.host = out._hostList[0];
966
+ }
967
+ else {
968
+ out.host = value;
969
+ out._hostList = undefined;
970
+ }
971
+ return;
972
+ }
973
+ case "port": {
974
+ if (value.includes(",")) {
975
+ out._portList = value.split(",").map((p) => p.trim());
976
+ // First port still goes into the scalar slot for back-compat.
977
+ out.port = parsePort(out._portList[0]);
978
+ }
979
+ else {
980
+ out.port = parsePort(value);
981
+ out._portList = undefined;
982
+ }
983
+ return;
984
+ }
985
+ case "user":
986
+ out.user = value;
987
+ return;
988
+ case "password":
989
+ out.password = value;
990
+ return;
991
+ case "dbname":
992
+ out.database = value;
993
+ return;
994
+ case "application_name":
995
+ out.applicationName = value;
996
+ return;
997
+ case "sslmode":
998
+ out.ssl = normalizeSslMode(value);
999
+ return;
1000
+ case "channel_binding": {
1001
+ const cb = normalizeChannelBinding(value);
1002
+ if (cb !== undefined)
1003
+ out.channelBinding = cb;
1004
+ return;
1005
+ }
1006
+ case "require_auth": {
1007
+ const ra = normalizeRequireAuth(value);
1008
+ if (ra !== undefined)
1009
+ out.requireAuth = ra;
1010
+ return;
1011
+ }
1012
+ case "connect_timeout": {
1013
+ const t = Number.parseInt(value, 10);
1014
+ if (Number.isFinite(t) && t >= 0) {
1015
+ out.connectTimeoutMs = t * 1000;
1016
+ }
1017
+ return;
1018
+ }
1019
+ case "client_encoding":
1020
+ out.clientEncoding = value;
1021
+ return;
1022
+ case "options":
1023
+ out.options = value;
1024
+ return;
1025
+ case "replication": {
1026
+ const rep = normalizeReplication(value);
1027
+ if (rep !== undefined)
1028
+ out.replication = rep;
1029
+ return;
1030
+ }
1031
+ case "target_session_attrs": {
1032
+ const tsa = normalizeTargetSessionAttrs(value);
1033
+ if (tsa !== undefined)
1034
+ out.targetSessionAttrs = tsa;
1035
+ return;
1036
+ }
1037
+ case "load_balance_hosts": {
1038
+ const lbh = normalizeLoadBalanceHosts(value);
1039
+ if (lbh !== undefined)
1040
+ out.loadBalanceHosts = lbh;
1041
+ return;
1042
+ }
1043
+ case "gssencmode":
1044
+ // Unsupported (no GSSAPI); accept disable/prefer, reject require.
1045
+ validateGssEncMode(value);
1046
+ return;
1047
+ case "sslcert":
1048
+ if (value !== "")
1049
+ out.sslcert = value;
1050
+ return;
1051
+ case "sslkey":
1052
+ if (value !== "")
1053
+ out.sslkey = value;
1054
+ return;
1055
+ case "sslcertmode": {
1056
+ const cm = normalizeSslCertMode(value);
1057
+ if (cm !== undefined)
1058
+ out.sslcertmode = cm;
1059
+ return;
1060
+ }
1061
+ case "sslnegotiation": {
1062
+ const sn = normalizeSslNegotiation(value);
1063
+ if (sn !== undefined)
1064
+ out.sslnegotiation = sn;
1065
+ return;
1066
+ }
1067
+ case "sslrootcert":
1068
+ if (value !== "")
1069
+ out.sslrootcert = value;
1070
+ return;
1071
+ case "sslcrl":
1072
+ if (value !== "")
1073
+ out.sslcrl = value;
1074
+ return;
1075
+ case "sslcrldir":
1076
+ if (value !== "")
1077
+ out.sslcrldir = value;
1078
+ return;
1079
+ case "sslkeylogfile":
1080
+ if (value !== "")
1081
+ out.sslkeylogfile = value;
1082
+ return;
1083
+ case "hostaddr":
1084
+ if (value !== "")
1085
+ out.hostaddr = value;
1086
+ return;
1087
+ case "ssl_min_protocol_version": {
1088
+ const v = normalizeTlsProtocolVersion(value === "" ? undefined : value, "ssl_min_protocol_version");
1089
+ if (v !== undefined)
1090
+ out.sslMinProtocolVersion = v;
1091
+ return;
1092
+ }
1093
+ case "ssl_max_protocol_version": {
1094
+ const v = normalizeTlsProtocolVersion(value === "" ? undefined : value, "ssl_max_protocol_version");
1095
+ if (v !== undefined)
1096
+ out.sslMaxProtocolVersion = v;
1097
+ return;
1098
+ }
1099
+ case "sslsni": {
1100
+ const b = parseLibpqBool(value);
1101
+ if (b !== undefined)
1102
+ out.sslsni = b;
1103
+ return;
1104
+ }
1105
+ case "keepalives": {
1106
+ const b = parseLibpqBool(value);
1107
+ if (b !== undefined)
1108
+ out.keepalives = b;
1109
+ return;
1110
+ }
1111
+ case "keepalives_idle": {
1112
+ const n = parseKeepalivesIdle(value);
1113
+ if (n !== undefined)
1114
+ out.keepalivesIdle = n;
1115
+ return;
1116
+ }
1117
+ case "requirepeer":
1118
+ if (value !== "")
1119
+ out.requirepeer = value;
1120
+ return;
1121
+ // Recognised libpq keys that we don't model — accept silently so we
1122
+ // don't reject legitimate connection strings. keepalives_interval /
1123
+ // keepalives_count have no Node net API equivalent (setKeepAlive only
1124
+ // exposes enable + initial delay) — recognised but cannot be applied.
1125
+ case "passfile":
1126
+ case "sslcompression":
1127
+ case "krbsrvname":
1128
+ case "gsslib":
1129
+ case "fallback_application_name":
1130
+ case "keepalives_interval":
1131
+ case "keepalives_count":
1132
+ return;
1133
+ case "service": {
1134
+ // Service name is NOT a ConnectOptions field — it's resolved by
1135
+ // the layered connect resolver in `core/startup.ts`. Stash it on
1136
+ // a private staging slot so the caller (`runPsql`) can extract it
1137
+ // alongside the URI-side `?service=…` parser.
1138
+ out._service =
1139
+ value;
1140
+ return;
1141
+ }
1142
+ default:
1143
+ throw new Error(`invalid conninfo key: "${key}"`);
1144
+ }
1145
+ };
1146
+ /**
1147
+ * Heuristic: does the `-d` value look like a connection URI or a conninfo
1148
+ * string (vs. a bare database name)? Mirrors libpq's
1149
+ * `recognized_connection_string()` test.
1150
+ */
1151
+ export const looksLikeConnectionString = (s) => {
1152
+ if (s.startsWith("postgresql://") || s.startsWith("postgres://"))
1153
+ return true;
1154
+ // A bare key=value pair (or several) — conninfo. We require the `=` to
1155
+ // appear before any whitespace so values like "weird name" (a bareword
1156
+ // database name with a space) don't get misclassified.
1157
+ const eq = s.indexOf("=");
1158
+ if (eq < 0)
1159
+ return false;
1160
+ const head = s.slice(0, eq);
1161
+ return !/\s/.test(head);
1162
+ };
1163
+ const normalizeSslMode = (raw) => {
1164
+ // Only null/unset defaults to 'prefer'. An explicit but unrecognized value
1165
+ // (e.g. a `verify-ful` typo) MUST be rejected — silently falling back to
1166
+ // 'prefer' would proceed with no cert-chain / hostname verification, a TLS
1167
+ // downgrade. Mirrors libpq's `invalid sslmode value: "..."` and every
1168
+ // sibling validator (normalizeChannelBinding, normalizeSslCertMode, …).
1169
+ if (raw === null)
1170
+ return "prefer";
1171
+ const value = raw.toLowerCase();
1172
+ switch (value) {
1173
+ case "disable":
1174
+ case "allow":
1175
+ case "prefer":
1176
+ case "require":
1177
+ case "verify-ca":
1178
+ case "verify-full":
1179
+ return value;
1180
+ default:
1181
+ throw new Error(`invalid sslmode value: "${raw}"`);
1182
+ }
1183
+ };
1184
+ /**
1185
+ * libpq's accepted TLS protocol-version names, in ascending order. The
1186
+ * index doubles as the comparison key for the `min > max` check. Matching
1187
+ * is case-insensitive on input (libpq lowercases before comparing) but we
1188
+ * keep the canonical mixed-case spelling Node's `tls` module expects for
1189
+ * `minVersion` / `maxVersion`.
1190
+ */
1191
+ const TLS_PROTOCOL_VERSIONS = ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"];
1192
+ /**
1193
+ * Validate and canonicalise a `ssl_{min,max}_protocol_version` value. Returns
1194
+ * the canonical spelling (`TLSv1.2` etc.) or `undefined` for empty / unset.
1195
+ * Throws libpq's `invalid <key> value: "<raw>"` wording on a malformed value.
1196
+ */
1197
+ const normalizeTlsProtocolVersion = (raw, key) => {
1198
+ if (raw === undefined || raw === "")
1199
+ return undefined;
1200
+ const match = TLS_PROTOCOL_VERSIONS.find((v) => v.toLowerCase() === raw.toLowerCase());
1201
+ if (match === undefined) {
1202
+ throw new Error(`invalid ${key} value: "${raw}"`);
1203
+ }
1204
+ return match;
1205
+ };
1206
+ /**
1207
+ * Reject a `ssl_max_protocol_version` ceiling below TLSv1.2. TLS 1.0/1.1 are
1208
+ * disabled in Node's bundled OpenSSL, so capping the ceiling there leaves no
1209
+ * negotiable protocol and the handshake otherwise fails with an opaque
1210
+ * `ERR_SSL_NO_PROTOCOLS_AVAILABLE` — surface an actionable message at parse
1211
+ * time instead. Only the MAX is gated: a low *min*
1212
+ * (`ssl_min_protocol_version=TLSv1.1`) is harmless because Node still
1213
+ * negotiates the highest mutually-supported version (1.2/1.3), exactly as
1214
+ * libpq does on a modern OpenSSL. Called AFTER {@link assertTlsProtocolRange}
1215
+ * so an inverted range (min > max) reports the range error first, matching
1216
+ * libpq's ordering.
1217
+ */
1218
+ const assertTlsMaxProtocolSupported = (max) => {
1219
+ if (max === "TLSv1" || max === "TLSv1.1") {
1220
+ throw new Error(`ssl_max_protocol_version "${max}" is not supported by this ` +
1221
+ `runtime's TLS library — TLS 1.0/1.1 are disabled in Node's OpenSSL; ` +
1222
+ `the minimum negotiable version is TLSv1.2`);
1223
+ }
1224
+ };
1225
+ /**
1226
+ * Reject a `ssl_min_protocol_version` that is higher than
1227
+ * `ssl_max_protocol_version`, matching libpq's
1228
+ * `ssl_min_protocol_version must be <= ssl_max_protocol_version` diagnostic.
1229
+ * Both arguments must already be canonicalised by
1230
+ * {@link normalizeTlsProtocolVersion}.
1231
+ */
1232
+ const assertTlsProtocolRange = (min, max) => {
1233
+ if (min === undefined || max === undefined)
1234
+ return;
1235
+ if (TLS_PROTOCOL_VERSIONS.indexOf(min) > TLS_PROTOCOL_VERSIONS.indexOf(max)) {
1236
+ throw new Error(`ssl_min_protocol_version must be <= ssl_max_protocol_version`);
1237
+ }
1238
+ };
1239
+ const normalizeChannelBinding = (raw) => {
1240
+ if (raw === null || raw === "")
1241
+ return undefined;
1242
+ const value = raw.toLowerCase();
1243
+ switch (value) {
1244
+ case "disable":
1245
+ case "prefer":
1246
+ case "require":
1247
+ return value;
1248
+ default:
1249
+ // Mirror libpq's `invalid channel_binding value: "<raw>"`
1250
+ // diagnostic (upstream test `002_scram.pl`). Empty / unset
1251
+ // returns `undefined` above so the wire-layer default applies.
1252
+ throw new Error(`invalid channel_binding value: "${raw}"`);
1253
+ }
1254
+ };
1255
+ /**
1256
+ * Validate libpq's `gssencmode` (GSSAPI transport encryption).
1257
+ *
1258
+ * This client has NO GSSAPI support: GSS-API `gss_wrap`/`gss_unwrap` would
1259
+ * require a native Kerberos addon (e.g. the `kerberos` npm), which the
1260
+ * embedded psql deliberately avoids (pure-TS, zero native bindings — the
1261
+ * same reason the line editor is hand-rolled). `node-postgres` doesn't
1262
+ * support it either. So:
1263
+ * - `disable` / `prefer` — accepted and ignored: neither needs GSS
1264
+ * (`prefer` means "try GSS, else fall back", and falling back to the
1265
+ * non-GSS path is exactly what we always do).
1266
+ * - `require` — rejected with a clear diagnostic; we cannot satisfy it.
1267
+ * - anything else — `invalid gssencmode value`.
1268
+ * We recognise the parameter (rather than rejecting it as an unknown key)
1269
+ * so the many tools that always append `gssencmode=...` to a URI keep
1270
+ * working against Neon.
1271
+ */
1272
+ const validateGssEncMode = (raw) => {
1273
+ if (raw === null || raw === "")
1274
+ return;
1275
+ const value = raw.toLowerCase();
1276
+ if (value === "disable" || value === "prefer")
1277
+ return;
1278
+ if (value === "require") {
1279
+ throw new Error("gssencmode=require is not supported: this client has no GSSAPI support");
1280
+ }
1281
+ throw new Error(`invalid gssencmode value: "${raw}"`);
1282
+ };
1283
+ /**
1284
+ * Parse libpq's `sslcertmode` value (`disable` / `allow` / `require`).
1285
+ * Empty / unset returns `undefined` so the wire-layer default (`allow`)
1286
+ * applies. A malformed value throws libpq's
1287
+ * `invalid sslcertmode value: "<raw>"` diagnostic.
1288
+ */
1289
+ const normalizeSslCertMode = (raw) => {
1290
+ if (raw === null || raw === "")
1291
+ return undefined;
1292
+ const value = raw.toLowerCase();
1293
+ switch (value) {
1294
+ case "disable":
1295
+ case "allow":
1296
+ case "require":
1297
+ return value;
1298
+ default:
1299
+ throw new Error(`invalid sslcertmode value: "${raw}"`);
1300
+ }
1301
+ };
1302
+ /**
1303
+ * Parse libpq's `sslnegotiation` value (`postgres` / `direct`). Empty / unset
1304
+ * returns `undefined` so the wire-layer default (`postgres`, the classic
1305
+ * SSLRequest flow) applies. A malformed value throws libpq's
1306
+ * `invalid sslnegotiation value: "<raw>"` diagnostic.
1307
+ */
1308
+ const normalizeSslNegotiation = (raw) => {
1309
+ if (raw === null || raw === "")
1310
+ return undefined;
1311
+ const value = raw.toLowerCase();
1312
+ switch (value) {
1313
+ case "postgres":
1314
+ case "direct":
1315
+ return value;
1316
+ default:
1317
+ throw new Error(`invalid sslnegotiation value: "${raw}"`);
1318
+ }
1319
+ };
1320
+ /**
1321
+ * libpq constraint: `sslnegotiation=direct` may only be used with an encrypted
1322
+ * sslmode (`require` / `verify-ca` / `verify-full`). Direct SSL starts the TLS
1323
+ * handshake immediately with no plaintext fallback, so a "weak" mode that could
1324
+ * end up unencrypted (`disable` / `allow` / `prefer`) is rejected with libpq's
1325
+ * exact `pqConnectOptions2` wording. No-op unless `sslnegotiation` is `direct`.
1326
+ */
1327
+ const assertSslNegotiationModeCompatible = (ssl, sslnegotiation) => {
1328
+ if (sslnegotiation !== "direct")
1329
+ return;
1330
+ if (ssl === "require" || ssl === "verify-ca" || ssl === "verify-full") {
1331
+ return;
1332
+ }
1333
+ throw new Error(`weak sslmode "${ssl}" may not be used with sslnegotiation=direct`);
1334
+ };
1335
+ const VALID_REQUIRE_AUTH_METHODS = new Set([
1336
+ "password",
1337
+ "md5",
1338
+ "gss",
1339
+ "sspi",
1340
+ "scram-sha-256",
1341
+ "creds",
1342
+ "none",
1343
+ ]);
1344
+ /**
1345
+ * Parse libpq's `require_auth` value: a comma-separated list of method
1346
+ * names where each entry may be prefixed with `!` to negate. Mixing
1347
+ * positive and negative entries is forbidden (libpq matches this).
1348
+ *
1349
+ * Returns `undefined` for empty input so the wire-layer default applies.
1350
+ * Throws on invalid syntax with libpq-parity wording, surfaced via the
1351
+ * outer `psql: error: ...` channel.
1352
+ */
1353
+ const normalizeRequireAuth = (raw) => {
1354
+ if (raw === null || raw === "")
1355
+ return undefined;
1356
+ const tokens = raw
1357
+ .split(",")
1358
+ .map((s) => s.trim())
1359
+ .filter((s) => s.length > 0);
1360
+ if (tokens.length === 0)
1361
+ return undefined;
1362
+ const methods = new Set();
1363
+ let polarity = null;
1364
+ for (const token of tokens) {
1365
+ const isNeg = token.startsWith("!");
1366
+ const name = (isNeg ? token.slice(1) : token).toLowerCase();
1367
+ if (!VALID_REQUIRE_AUTH_METHODS.has(name)) {
1368
+ throw new Error(`invalid require_auth method: "${token}"`);
1369
+ }
1370
+ if (polarity === null) {
1371
+ polarity = isNeg;
1372
+ }
1373
+ else if (polarity !== isNeg) {
1374
+ // libpq wording: "negative require_auth method ... cannot be mixed
1375
+ // with non-negative methods". We use a slightly shorter form here.
1376
+ throw new Error("require_auth methods cannot mix positive and negative entries");
1377
+ }
1378
+ methods.add(name);
1379
+ }
1380
+ return { methods, negated: polarity ?? false };
1381
+ };
1382
+ /**
1383
+ * Accept the libpq-spec set for `target_session_attrs`. Aliases `read-write`/
1384
+ * `primary` and `read-only`/`standby` are kept distinct because the wire
1385
+ * layer treats the canonical four values identically — we only normalise
1386
+ * unknown / empty inputs to `undefined` so the wire-layer default ('any')
1387
+ * applies. Throws on unrecognised values, matching libpq behaviour.
1388
+ */
1389
+ const normalizeTargetSessionAttrs = (raw) => {
1390
+ if (raw === null || raw === "")
1391
+ return undefined;
1392
+ const value = raw.toLowerCase();
1393
+ switch (value) {
1394
+ case "any":
1395
+ case "read-write":
1396
+ case "read-only":
1397
+ case "primary":
1398
+ case "standby":
1399
+ case "prefer-standby":
1400
+ return value;
1401
+ default:
1402
+ throw new Error(`invalid value for "target_session_attrs": "${raw}"`);
1403
+ }
1404
+ };
1405
+ /**
1406
+ * Accept the libpq-spec set for `load_balance_hosts`:
1407
+ * - `disable` (default) — preserve list order
1408
+ * - `random` — shuffle before iteration
1409
+ *
1410
+ * Unknown values throw; empty / unset returns `undefined` so the wire layer
1411
+ * default ('disable') applies.
1412
+ */
1413
+ const normalizeLoadBalanceHosts = (raw) => {
1414
+ if (raw === null || raw === "")
1415
+ return undefined;
1416
+ const value = raw.toLowerCase();
1417
+ if (value === "disable" || value === "random")
1418
+ return value;
1419
+ throw new Error(`invalid value for "load_balance_hosts": "${raw}"`);
1420
+ };
1421
+ /**
1422
+ * libpq accepts a wide set of "truthy" values for `replication`:
1423
+ * - `true` / `on` / `yes` / `1` → physical replication (mapped to `true`)
1424
+ * - `false` / `off` / `no` / `0` → not a walsender (no replication mode)
1425
+ * - `database` → logical replication on that DB
1426
+ *
1427
+ * Returns `undefined` when no value was supplied or when it explicitly
1428
+ * disables replication; throws for unrecognised input (matching libpq's
1429
+ * `"invalid <…> value"` semantics so users see a clear error rather than
1430
+ * silently sending an unexpected startup-message parameter).
1431
+ */
1432
+ const normalizeReplication = (raw) => {
1433
+ if (raw === null || raw === "")
1434
+ return undefined;
1435
+ const value = raw.toLowerCase();
1436
+ if (value === "database")
1437
+ return "database";
1438
+ if (value === "true" ||
1439
+ value === "on" ||
1440
+ value === "yes" ||
1441
+ value === "1") {
1442
+ return "true";
1443
+ }
1444
+ if (value === "false" ||
1445
+ value === "off" ||
1446
+ value === "no" ||
1447
+ value === "0") {
1448
+ return undefined;
1449
+ }
1450
+ throw new Error(`invalid value for "replication": "${raw}"`);
1451
+ };
1452
+ // ---------------------------------------------------------------------------
1453
+ // Layered connection-parameter resolution.
1454
+ //
1455
+ // Vanilla psql consults several sources in priority order when filling in
1456
+ // connection parameters. Order (highest → lowest):
1457
+ //
1458
+ // 1. Explicit URI / conninfo (what the user passed on the command line)
1459
+ // 2. Argv flags (`-h`, `-p`, etc.)
1460
+ // 3. PG* env vars
1461
+ // 4. ~/.pgpass (password only; matched against host/port/db/user)
1462
+ // 5. pg_service.conf (when PGSERVICE / ?service= is set)
1463
+ // 6. libpq compiled-in defaults (localhost / 5432 / USER / database=user)
1464
+ //
1465
+ // The historical `parseConnectionUri` bakes (1) and (6) into a single
1466
+ // `ConnectOptions`, which makes layering impossible. `parseConnectionUriPartial`
1467
+ // gives the same parser surface but returns ONLY the fields the URI
1468
+ // explicitly set — leaving env / pgpass / service / defaults to fill the
1469
+ // gaps via `mergeConnectOptions`.
1470
+ // ---------------------------------------------------------------------------
1471
+ /**
1472
+ * Service name extracted from a connection URI's `?service=` query
1473
+ * parameter, if any. Surfaced alongside `parseConnectionUriPartial` so the
1474
+ * caller can route the lookup into `applyStartupArgs` without re-parsing
1475
+ * the URI.
1476
+ */
1477
+ export const parseConnectionUriService = (uri) => {
1478
+ const raw = tokenizeConnectionUri(uri);
1479
+ const value = raw.query.get("service");
1480
+ return value === undefined || value === "" ? undefined : value;
1481
+ };
1482
+ /**
1483
+ * Parse a URI into a `Partial<ConnectOptions>` containing only the fields
1484
+ * the URI explicitly supplied. Returned shape:
1485
+ *
1486
+ * - missing fields are absent (no `undefined` placeholders)
1487
+ * - `host`/`port` are populated only when the URI authority or `?host=`
1488
+ * specified them
1489
+ * - `user`/`password`/`database`/`ssl`/`channelBinding`/... follow the
1490
+ * same rule — present iff explicitly set
1491
+ *
1492
+ * This is the building block for the layered merge in `applyStartupArgs`.
1493
+ * The full-defaults variant `parseConnectionUri` remains the right choice
1494
+ * for callers that want a complete `ConnectOptions` (e.g. the existing
1495
+ * `-d URI` path); it's kept untouched for back-compat.
1496
+ */
1497
+ export const parseConnectionUriPartial = (uri) => {
1498
+ const raw = tokenizeConnectionUri(uri);
1499
+ const queryUser = raw.query.get("user");
1500
+ const queryPassword = raw.query.get("password");
1501
+ const queryPort = raw.query.get("port");
1502
+ const queryDbname = raw.query.get("dbname");
1503
+ const queryHost = raw.query.get("host");
1504
+ // Multi-host: same resolution as the full parser, but we treat its absence
1505
+ // as "URI didn't say anything about hosts" rather than synthesising a
1506
+ // localhost default.
1507
+ const hostsTuples = computeHostsTuples({
1508
+ rawHost: raw.host,
1509
+ rawPort: raw.port,
1510
+ rawAuthorityHosts: raw.hosts,
1511
+ queryHost,
1512
+ queryPort,
1513
+ });
1514
+ // Did the URI actually mention a port anywhere? `parsePort()` defaults
1515
+ // empty input to 5432, so `hostsTuples[i].port` is ALWAYS a number even
1516
+ // for a URI like `postgres:///?service=foo` (no port specified). We
1517
+ // need to distinguish "URI explicitly said 5432" from "URI said nothing
1518
+ // about a port" so the service-file's port wins when we layer this
1519
+ // partial above the service layer. Mirrors libpq's behaviour for
1520
+ // `006_service.pl`'s `postgres:///?service=…` cases.
1521
+ const portInUri = (raw.port !== undefined && raw.port !== "") ||
1522
+ (queryPort !== undefined && queryPort !== "") ||
1523
+ (raw.hosts?.some((t) => t.port !== undefined && t.port !== "") ??
1524
+ false);
1525
+ const out = {};
1526
+ if (hostsTuples.length > 0) {
1527
+ // First tuple drives the single-host surface; the full multi-host list
1528
+ // is included only when the URI specified more than one. An empty-host
1529
+ // tuple (e.g. `postgres://:12345/`) means "explicit port, no host" —
1530
+ // we record the port but leave host to the next layer.
1531
+ if (hostsTuples[0].host !== "")
1532
+ out.host = hostsTuples[0].host;
1533
+ if (portInUri && hostsTuples[0].port !== 0)
1534
+ out.port = hostsTuples[0].port;
1535
+ if (hostsTuples.length > 1) {
1536
+ out.hosts = hostsTuples.map((t) => ({
1537
+ host: t.host,
1538
+ port: t.port,
1539
+ }));
1540
+ }
1541
+ }
1542
+ const userExplicit = queryUser !== undefined && queryUser !== ""
1543
+ ? queryUser
1544
+ : raw.user !== undefined && raw.user !== ""
1545
+ ? raw.user
1546
+ : undefined;
1547
+ if (userExplicit !== undefined)
1548
+ out.user = userExplicit;
1549
+ const password = queryPassword ?? raw.password;
1550
+ if (password !== undefined)
1551
+ out.password = password;
1552
+ const database = queryDbname ?? raw.database;
1553
+ if (database !== undefined)
1554
+ out.database = database;
1555
+ const sslRaw = raw.query.get("sslmode");
1556
+ if (sslRaw !== undefined && sslRaw !== "") {
1557
+ out.ssl = normalizeSslMode(sslRaw);
1558
+ }
1559
+ const cb = normalizeChannelBinding(raw.query.get("channel_binding") ?? null);
1560
+ if (cb !== undefined)
1561
+ out.channelBinding = cb;
1562
+ const ra = normalizeRequireAuth(raw.query.get("require_auth") ?? null);
1563
+ if (ra !== undefined)
1564
+ out.requireAuth = ra;
1565
+ const options = raw.query.get("options");
1566
+ if (options !== undefined && options !== "")
1567
+ out.options = options;
1568
+ const appName = raw.query.get("application_name");
1569
+ if (appName !== undefined && appName !== "")
1570
+ out.applicationName = appName;
1571
+ const replication = normalizeReplication(raw.query.get("replication") ?? null);
1572
+ if (replication !== undefined)
1573
+ out.replication = replication;
1574
+ const targetSessionAttrs = normalizeTargetSessionAttrs(raw.query.get("target_session_attrs") ?? null);
1575
+ if (targetSessionAttrs !== undefined) {
1576
+ out.targetSessionAttrs = targetSessionAttrs;
1577
+ }
1578
+ const loadBalanceHosts = normalizeLoadBalanceHosts(raw.query.get("load_balance_hosts") ?? null);
1579
+ if (loadBalanceHosts !== undefined)
1580
+ out.loadBalanceHosts = loadBalanceHosts;
1581
+ const sslcert = nonEmpty(raw.query.get("sslcert"));
1582
+ if (sslcert !== undefined)
1583
+ out.sslcert = sslcert;
1584
+ const sslkey = nonEmpty(raw.query.get("sslkey"));
1585
+ if (sslkey !== undefined)
1586
+ out.sslkey = sslkey;
1587
+ const sslcertmode = normalizeSslCertMode(raw.query.get("sslcertmode") ?? null);
1588
+ if (sslcertmode !== undefined)
1589
+ out.sslcertmode = sslcertmode;
1590
+ const sslnegotiation = normalizeSslNegotiation(raw.query.get("sslnegotiation") ?? null);
1591
+ if (sslnegotiation !== undefined)
1592
+ out.sslnegotiation = sslnegotiation;
1593
+ const sslrootcert = nonEmpty(raw.query.get("sslrootcert"));
1594
+ if (sslrootcert !== undefined)
1595
+ out.sslrootcert = sslrootcert;
1596
+ const sslcrl = nonEmpty(raw.query.get("sslcrl"));
1597
+ if (sslcrl !== undefined)
1598
+ out.sslcrl = sslcrl;
1599
+ const sslcrldir = nonEmpty(raw.query.get("sslcrldir"));
1600
+ if (sslcrldir !== undefined)
1601
+ out.sslcrldir = sslcrldir;
1602
+ const sslkeylogfile = nonEmpty(raw.query.get("sslkeylogfile"));
1603
+ if (sslkeylogfile !== undefined)
1604
+ out.sslkeylogfile = sslkeylogfile;
1605
+ const sslsni = parseLibpqBool(nonEmpty(raw.query.get("sslsni")));
1606
+ if (sslsni !== undefined)
1607
+ out.sslsni = sslsni;
1608
+ const keepalives = parseLibpqBool(nonEmpty(raw.query.get("keepalives")));
1609
+ if (keepalives !== undefined)
1610
+ out.keepalives = keepalives;
1611
+ const keepalivesIdle = parseKeepalivesIdle(nonEmpty(raw.query.get("keepalives_idle")));
1612
+ if (keepalivesIdle !== undefined)
1613
+ out.keepalivesIdle = keepalivesIdle;
1614
+ const requirepeer = nonEmpty(raw.query.get("requirepeer"));
1615
+ if (requirepeer !== undefined)
1616
+ out.requirepeer = requirepeer;
1617
+ const hostaddr = nonEmpty(raw.query.get("hostaddr"));
1618
+ if (hostaddr !== undefined)
1619
+ out.hostaddr = hostaddr;
1620
+ const sslMin = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_min_protocol_version")), "ssl_min_protocol_version");
1621
+ if (sslMin !== undefined)
1622
+ out.sslMinProtocolVersion = sslMin;
1623
+ const sslMax = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_max_protocol_version")), "ssl_max_protocol_version");
1624
+ if (sslMax !== undefined)
1625
+ out.sslMaxProtocolVersion = sslMax;
1626
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
1627
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
1628
+ const connectTimeoutSec = raw.query.get("connect_timeout");
1629
+ if (connectTimeoutSec !== undefined && connectTimeoutSec !== "") {
1630
+ const t = Number.parseInt(connectTimeoutSec, 10);
1631
+ if (Number.isFinite(t) && t >= 0)
1632
+ out.connectTimeoutMs = t * 1000;
1633
+ }
1634
+ const clientEncoding = raw.query.get("client_encoding");
1635
+ if (clientEncoding !== undefined && clientEncoding !== "") {
1636
+ out.clientEncoding = clientEncoding;
1637
+ }
1638
+ return out;
1639
+ };
1640
+ // Field map for PG* env vars. Order in the table is documentation; resolution
1641
+ // only depends on whether the var is set.
1642
+ const PG_ENV_FIELD_MAP = {
1643
+ PGHOST: "host",
1644
+ PGHOSTADDR: "hostaddr",
1645
+ PGPORT: "port",
1646
+ PGUSER: "user",
1647
+ PGDATABASE: "database",
1648
+ PGPASSWORD: "password",
1649
+ PGAPPNAME: "applicationName",
1650
+ PGOPTIONS: "options",
1651
+ PGCLIENTENCODING: "clientEncoding",
1652
+ PGSSLMODE: "ssl",
1653
+ PGSSLROOTCERT: "sslrootcert",
1654
+ PGSSLCERT: "sslcert",
1655
+ PGSSLKEY: "sslkey",
1656
+ PGSSLCERTMODE: "sslcertmode",
1657
+ PGSSLNEGOTIATION: "sslnegotiation",
1658
+ PGSSLCRL: "sslcrl",
1659
+ PGSSLCRLDIR: "sslcrldir",
1660
+ PGSSLKEYLOGFILE: "sslkeylogfile",
1661
+ PGCHANNELBINDING: "channelBinding",
1662
+ };
1663
+ /**
1664
+ * Resolve the PG* env vars into a `Partial<ConnectOptions>`. Only set keys
1665
+ * end up in the result; unset / empty env vars are skipped so the caller
1666
+ * can layer this between URI overrides and pgpass / service / libpq
1667
+ * defaults without clobbering anything.
1668
+ *
1669
+ * Validation: any malformed value (e.g. `PGPORT=abc`) is silently dropped.
1670
+ * libpq behaves the same — the connection then fails later with a clearer
1671
+ * "could not parse" message, but the env-var lookup itself does not throw.
1672
+ *
1673
+ * Notes:
1674
+ * - `PGHOSTADDR` maps to {@link ConnectOptions.hostaddr}: the wire layer
1675
+ * dials this fixed IP while `PGHOST` still drives TLS SNI / cert
1676
+ * verification.
1677
+ * - `PGCONNECT_TIMEOUT` is in seconds; we convert to milliseconds.
1678
+ * - `PGCHANNELBINDING` accepts disable/prefer/require.
1679
+ * - `PGSERVICE` is consumed by the caller (it drives the
1680
+ * pg_service.conf lookup) and is NOT a direct ConnectOptions field.
1681
+ * - `PGSERVICEFILE`, `PGSYSCONFDIR`, `PGPASSFILE` are likewise consumed
1682
+ * by the loaders, not surfaced here.
1683
+ */
1684
+ export const envConnectionDefaults = (env) => {
1685
+ const out = {};
1686
+ const get = (k) => {
1687
+ const v = env[k];
1688
+ return v !== undefined && v !== "" ? v : undefined;
1689
+ };
1690
+ for (const [envName, field] of Object.entries(PG_ENV_FIELD_MAP)) {
1691
+ const value = get(envName);
1692
+ if (value === undefined)
1693
+ continue;
1694
+ applyEnvValue(out, field, value);
1695
+ }
1696
+ const timeoutRaw = get("PGCONNECT_TIMEOUT");
1697
+ if (timeoutRaw !== undefined) {
1698
+ const t = Number.parseInt(timeoutRaw, 10);
1699
+ if (Number.isFinite(t) && t >= 0)
1700
+ out.connectTimeoutMs = t * 1000;
1701
+ }
1702
+ // GSSAPI is unsupported; PGGSSENCMODE=require is rejected, disable/prefer
1703
+ // accepted-and-ignored. Same contract as the URI/conninfo `gssencmode`.
1704
+ validateGssEncMode(get("PGGSSENCMODE") ?? null);
1705
+ return out;
1706
+ };
1707
+ const applyEnvValue = (out, field, value) => {
1708
+ switch (field) {
1709
+ case "host":
1710
+ out.host = value;
1711
+ return;
1712
+ case "port": {
1713
+ const p = Number.parseInt(value, 10);
1714
+ if (Number.isFinite(p) && p > 0 && p <= 65535)
1715
+ out.port = p;
1716
+ return;
1717
+ }
1718
+ case "user":
1719
+ out.user = value;
1720
+ return;
1721
+ case "database":
1722
+ out.database = value;
1723
+ return;
1724
+ case "password":
1725
+ out.password = value;
1726
+ return;
1727
+ case "applicationName":
1728
+ out.applicationName = value;
1729
+ return;
1730
+ case "options":
1731
+ out.options = value;
1732
+ return;
1733
+ case "clientEncoding":
1734
+ out.clientEncoding = value;
1735
+ return;
1736
+ case "ssl":
1737
+ out.ssl = normalizeSslMode(value);
1738
+ return;
1739
+ case "sslrootcert":
1740
+ out.sslrootcert = value;
1741
+ return;
1742
+ case "sslcert":
1743
+ out.sslcert = value;
1744
+ return;
1745
+ case "sslkey":
1746
+ out.sslkey = value;
1747
+ return;
1748
+ case "sslcertmode": {
1749
+ const cm = normalizeSslCertMode(value);
1750
+ if (cm !== undefined)
1751
+ out.sslcertmode = cm;
1752
+ return;
1753
+ }
1754
+ case "sslnegotiation": {
1755
+ const sn = normalizeSslNegotiation(value);
1756
+ if (sn !== undefined)
1757
+ out.sslnegotiation = sn;
1758
+ return;
1759
+ }
1760
+ case "sslcrl":
1761
+ out.sslcrl = value;
1762
+ return;
1763
+ case "sslcrldir":
1764
+ out.sslcrldir = value;
1765
+ return;
1766
+ case "sslkeylogfile":
1767
+ out.sslkeylogfile = value;
1768
+ return;
1769
+ case "hostaddr":
1770
+ out.hostaddr = value;
1771
+ return;
1772
+ case "channelBinding": {
1773
+ const cb = normalizeChannelBinding(value);
1774
+ if (cb !== undefined)
1775
+ out.channelBinding = cb;
1776
+ return;
1777
+ }
1778
+ default:
1779
+ // Unhandled field — silently drop. Tightening this would require
1780
+ // narrowing the field-map type; not worth the complexity.
1781
+ return;
1782
+ }
1783
+ };
1784
+ /**
1785
+ * libpq compiled-in defaults. The lowest-priority layer in the merge chain.
1786
+ *
1787
+ * - host: 'localhost'
1788
+ * - port: 5432
1789
+ * - user: $USER ?? '' (the wire layer surfaces a clear error if the user
1790
+ * is still empty at connect time)
1791
+ * - database: deferred — libpq defaults dbname to the user; we wire that
1792
+ * in `mergeConnectOptions` after layering so a `PGUSER` env can flow
1793
+ * into `database` when the user didn't specify one.
1794
+ * - ssl: 'prefer'
1795
+ * - applicationName: 'psql' — matches upstream so `pg_stat_activity` shows
1796
+ * the value users expect.
1797
+ * - sslcert / sslkey: libpq auto-loads the default client cert/key at
1798
+ * `~/.postgresql/postgresql.crt` / `.key` when neither is configured AND
1799
+ * the file exists. We seed these as the lowest-priority defaults via
1800
+ * {@link defaultClientCertDefaults}; any explicit URI / env / conninfo
1801
+ * value overrides them. A non-existent default file is simply not set
1802
+ * (no error), matching libpq — only an explicit path that's missing
1803
+ * surfaces an error (at TLS-load time, in the wire layer).
1804
+ */
1805
+ export const libpqConnectionDefaults = (env) => ({
1806
+ host: "localhost",
1807
+ port: 5432,
1808
+ user: env.USER ?? "",
1809
+ database: "",
1810
+ ssl: "prefer",
1811
+ applicationName: "psql",
1812
+ ...defaultClientCertDefaults(env),
1813
+ });
1814
+ /**
1815
+ * libpq default client-certificate discovery. When the user has NOT set
1816
+ * `sslcert` / `sslkey` (explicit paths and `PGSSLCERT` / `PGSSLKEY` are
1817
+ * higher-priority layers), libpq falls back to `~/.postgresql/postgresql.crt`
1818
+ * and `~/.postgresql/postgresql.key` — but only if those files actually
1819
+ * exist. We mirror that here so a present default cert satisfies e.g.
1820
+ * `sslcertmode=require`.
1821
+ *
1822
+ * The home directory is taken from `env.HOME` (falling back to
1823
+ * `os.homedir()`), the same convention as the pgpass / pgservice loaders;
1824
+ * passing a synthetic `HOME` keeps this hermetic in tests.
1825
+ *
1826
+ * Exported for unit testing.
1827
+ */
1828
+ export const defaultClientCertDefaults = (env) => {
1829
+ const home = env.HOME ?? os.homedir();
1830
+ if (home === undefined || home === "")
1831
+ return {};
1832
+ const out = {};
1833
+ const certPath = path.join(home, ".postgresql", "postgresql.crt");
1834
+ if (existsSync(certPath))
1835
+ out.sslcert = certPath;
1836
+ const keyPath = path.join(home, ".postgresql", "postgresql.key");
1837
+ if (existsSync(keyPath))
1838
+ out.sslkey = keyPath;
1839
+ return out;
1840
+ };
1841
+ /**
1842
+ * Translate a `pg_service.conf` entry into a `Partial<ConnectOptions>`.
1843
+ * Unknown keys are silently dropped — the service file format admits
1844
+ * arbitrary keys but only the libpq-spec subset maps to ConnectOptions.
1845
+ *
1846
+ * Numeric / enum validation mirrors `parseConninfo` so an out-of-range
1847
+ * port or bogus sslmode in the service file fails the same way (silently
1848
+ * dropped here, since libpq itself only warns on invalid service values).
1849
+ */
1850
+ export const serviceEntryToConnectOptions = (entry) => {
1851
+ const out = {};
1852
+ for (const [k, v] of Object.entries(entry)) {
1853
+ const key = k.toLowerCase();
1854
+ switch (key) {
1855
+ case "host":
1856
+ if (v !== "")
1857
+ out.host = v;
1858
+ break;
1859
+ case "port": {
1860
+ const p = Number.parseInt(v, 10);
1861
+ if (Number.isFinite(p) && p > 0 && p <= 65535)
1862
+ out.port = p;
1863
+ break;
1864
+ }
1865
+ case "user":
1866
+ if (v !== "")
1867
+ out.user = v;
1868
+ break;
1869
+ case "dbname":
1870
+ if (v !== "")
1871
+ out.database = v;
1872
+ break;
1873
+ case "password":
1874
+ out.password = v;
1875
+ break;
1876
+ case "application_name":
1877
+ if (v !== "")
1878
+ out.applicationName = v;
1879
+ break;
1880
+ case "sslmode":
1881
+ if (v !== "")
1882
+ out.ssl = normalizeSslMode(v);
1883
+ break;
1884
+ case "channel_binding": {
1885
+ const cb = normalizeChannelBinding(v);
1886
+ if (cb !== undefined)
1887
+ out.channelBinding = cb;
1888
+ break;
1889
+ }
1890
+ case "require_auth": {
1891
+ const ra = normalizeRequireAuth(v);
1892
+ if (ra !== undefined)
1893
+ out.requireAuth = ra;
1894
+ break;
1895
+ }
1896
+ case "options":
1897
+ if (v !== "")
1898
+ out.options = v;
1899
+ break;
1900
+ case "client_encoding":
1901
+ if (v !== "")
1902
+ out.clientEncoding = v;
1903
+ break;
1904
+ case "sslcert":
1905
+ if (v !== "")
1906
+ out.sslcert = v;
1907
+ break;
1908
+ case "sslkey":
1909
+ if (v !== "")
1910
+ out.sslkey = v;
1911
+ break;
1912
+ case "sslcertmode": {
1913
+ const cm = normalizeSslCertMode(v);
1914
+ if (cm !== undefined)
1915
+ out.sslcertmode = cm;
1916
+ break;
1917
+ }
1918
+ case "sslnegotiation": {
1919
+ const sn = normalizeSslNegotiation(v);
1920
+ if (sn !== undefined)
1921
+ out.sslnegotiation = sn;
1922
+ break;
1923
+ }
1924
+ case "sslrootcert":
1925
+ if (v !== "")
1926
+ out.sslrootcert = v;
1927
+ break;
1928
+ case "sslcrl":
1929
+ if (v !== "")
1930
+ out.sslcrl = v;
1931
+ break;
1932
+ case "sslcrldir":
1933
+ if (v !== "")
1934
+ out.sslcrldir = v;
1935
+ break;
1936
+ case "sslkeylogfile":
1937
+ if (v !== "")
1938
+ out.sslkeylogfile = v;
1939
+ break;
1940
+ case "sslsni": {
1941
+ const b = parseLibpqBool(v);
1942
+ if (b !== undefined)
1943
+ out.sslsni = b;
1944
+ break;
1945
+ }
1946
+ case "keepalives": {
1947
+ const b = parseLibpqBool(v);
1948
+ if (b !== undefined)
1949
+ out.keepalives = b;
1950
+ break;
1951
+ }
1952
+ case "keepalives_idle": {
1953
+ const n = parseKeepalivesIdle(v);
1954
+ if (n !== undefined)
1955
+ out.keepalivesIdle = n;
1956
+ break;
1957
+ }
1958
+ case "requirepeer":
1959
+ if (v !== "")
1960
+ out.requirepeer = v;
1961
+ break;
1962
+ case "hostaddr":
1963
+ if (v !== "")
1964
+ out.hostaddr = v;
1965
+ break;
1966
+ case "ssl_min_protocol_version": {
1967
+ const pv = normalizeTlsProtocolVersion(v === "" ? undefined : v, "ssl_min_protocol_version");
1968
+ if (pv !== undefined)
1969
+ out.sslMinProtocolVersion = pv;
1970
+ break;
1971
+ }
1972
+ case "ssl_max_protocol_version": {
1973
+ const pv = normalizeTlsProtocolVersion(v === "" ? undefined : v, "ssl_max_protocol_version");
1974
+ if (pv !== undefined)
1975
+ out.sslMaxProtocolVersion = pv;
1976
+ break;
1977
+ }
1978
+ case "connect_timeout": {
1979
+ const t = Number.parseInt(v, 10);
1980
+ if (Number.isFinite(t) && t >= 0)
1981
+ out.connectTimeoutMs = t * 1000;
1982
+ break;
1983
+ }
1984
+ // Recognised but not mapped — service files may contain `passfile`,
1985
+ // `krbsrvname`, etc. We drop silently rather than complain.
1986
+ default:
1987
+ break;
1988
+ }
1989
+ }
1990
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
1991
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
1992
+ return out;
1993
+ };
1994
+ /**
1995
+ * Merge layered partial ConnectOptions into a complete ConnectOptions.
1996
+ *
1997
+ * Layers are listed in PRIORITY order (highest first). For each output
1998
+ * field, the first layer that supplies a value wins. The implementation
1999
+ * walks the layers in reverse so the spread-into semantics match.
2000
+ *
2001
+ * `database` has a libpq-specific fallback: if every layer omits it, the
2002
+ * default is the resolved `user` (so `psql -U alice` connects to a
2003
+ * database named `alice`). We apply this AFTER all layers have run.
2004
+ */
2005
+ export const mergeConnectOptions = (layers, defaults) => {
2006
+ let out = { ...defaults };
2007
+ // Apply layers from LOWEST → HIGHEST so higher-priority layers overwrite.
2008
+ for (let i = layers.length - 1; i >= 0; i--) {
2009
+ const layer = layers[i];
2010
+ out = { ...out, ...layer };
2011
+ }
2012
+ // libpq: database defaults to the resolved user when no layer set it.
2013
+ // The default `database: ''` from `libpqConnectionDefaults` is the
2014
+ // sentinel for "nothing supplied".
2015
+ if (out.database === "") {
2016
+ out.database = out.user;
2017
+ }
2018
+ // libpq: `sslrootcert=system` raises the effective sslmode to verify-full
2019
+ // (it makes no sense to trust the public CA store without verifying the
2020
+ // chain AND the hostname). verify-full is the strongest mode, so this can
2021
+ // only ever raise — never downgrade — an explicitly requested mode.
2022
+ if (out.sslrootcert === "system" && out.ssl !== "verify-full") {
2023
+ out.ssl = "verify-full";
2024
+ }
2025
+ // libpq validates `sslnegotiation=direct` against the FINAL sslmode (after
2026
+ // any `sslrootcert=system` raise and cross-layer merge), rejecting a weak
2027
+ // mode that could end up plaintext. Authoritative check across all layers.
2028
+ assertSslNegotiationModeCompatible(out.ssl, out.sslnegotiation);
2029
+ return out;
2030
+ };