neon 3.0.0 → 3.1.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 (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +354 -517
  5. package/dist/_shared/env-core/reuse-secrets.js +159 -203
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +573 -690
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +639 -719
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -355
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -68
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +130 -189
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +7 -5
@@ -1,2030 +1,1517 @@
1
- import { existsSync, promises as fs } from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
1
  import { createCondStack } from "./command/cmd_cond.js";
2
+ import { syncConnectionVars } from "./core/syncVars.js";
3
+ import { PgConnection } from "./wire/connection.js";
5
4
  import { defaultRegistry } from "./command/dispatch.js";
6
- import { EXIT_BADCONN, EXIT_FAILURE, EXIT_SUCCESS, EXIT_USER, runMainLoop, } from "./core/mainloop.js";
5
+ import { runMainLoop } from "./core/mainloop.js";
7
6
  import { applyEnvOverrides, defaultSettings } from "./core/settings.js";
7
+ import { loadPgPass } from "./io/pgpass.js";
8
8
  import { applyStartupArgs, parseStartupArgs } from "./core/startup.js";
9
- import { syncConnectionVars } from "./core/syncVars.js";
10
9
  import { createVarStore } from "./core/variables.js";
11
- import { loadPgPass } from "./io/pgpass.js";
12
10
  import { loadPgServices } from "./io/pgservice.js";
13
11
  import { executeInputString, loadPsqlrc } from "./io/psqlrc.js";
14
- import { PgConnection } from "./wire/connection.js";
12
+ import { existsSync, promises } from "node:fs";
13
+ import * as path from "node:path";
14
+ import * as os from "node:os";
15
+ //#region src/psql/index.ts
15
16
  /**
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
- }
17
+ * Embedded TypeScript psql entrypoint.
18
+ *
19
+ * Argv shape mirrors the legacy native-psql call site:
20
+ * argv[0] = connection URI (postgresql://user:pw@host:port/db?sslmode=...)
21
+ * OR an empty string `''` when the caller provides the
22
+ * connection target via libpq flags (-h/-p/-U/-d) and/or PG*
23
+ * env. The downstream layered resolver picks those up just like
24
+ * upstream psql does.
25
+ * argv[1..] = forwarded psql args (parsed by `parseStartupArgs`).
26
+ */
27
+ const runPsql = async (argv, stdio = {}) => {
28
+ const stdin = stdio.stdin ?? process.stdin;
29
+ const stdout = stdio.stdout ?? process.stdout;
30
+ const stderr = stdio.stderr ?? process.stderr;
31
+ const connectionUri = argv[0] ?? "";
32
+ let uriPartial = {};
33
+ let uriService;
34
+ if (connectionUri !== "" && looksLikeConnectionString(connectionUri)) try {
35
+ if (connectionUri.startsWith("postgres://") || connectionUri.startsWith("postgresql://")) {
36
+ uriPartial = parseConnectionUriPartial(connectionUri);
37
+ uriService = parseConnectionUriService(connectionUri);
38
+ } else {
39
+ const parsed = parseConninfo(connectionUri);
40
+ if (typeof parsed._service === "string" && parsed._service.length > 0) uriService = parsed._service;
41
+ delete parsed._service;
42
+ uriPartial = parsed;
43
+ }
44
+ } catch (err) {
45
+ stderr.write(`psql: error: ${err.message}\n`);
46
+ return 2;
47
+ }
48
+ const parsed = parseStartupArgs(argv.slice(1));
49
+ if ("kind" in parsed) {
50
+ if (parsed.kind === "help" || parsed.kind === "version") {
51
+ stdout.write(parsed.message);
52
+ if (!parsed.message.endsWith("\n")) stdout.write("\n");
53
+ return 0;
54
+ }
55
+ stderr.write(`psql: error: ${parsed.message}\n`);
56
+ return 1;
57
+ }
58
+ const vars = createVarStore();
59
+ const settings = defaultSettings(vars);
60
+ applyEnvOverrides(settings, process.env);
61
+ settings.notty = !stdin.isTTY;
62
+ const pgpassEntries = await loadPgPass(void 0, {
63
+ env: process.env,
64
+ stderr
65
+ });
66
+ let services;
67
+ try {
68
+ services = await loadPgServices();
69
+ } catch (err) {
70
+ stderr.write(`psql: error: ${err.message}\n`);
71
+ return 2;
72
+ }
73
+ let resolved;
74
+ try {
75
+ resolved = applyStartupArgs(parsed, settings, void 0, {
76
+ env: process.env,
77
+ uriPartial,
78
+ serviceName: uriService,
79
+ pgpassEntries,
80
+ services
81
+ });
82
+ } catch (err) {
83
+ stderr.write(`psql: error: ${err.message}\n`);
84
+ return 2;
85
+ }
86
+ const { connect: connectOpts, preActions } = resolved;
87
+ let connection;
88
+ try {
89
+ connection = await PgConnection.connect(connectOpts);
90
+ } catch (err) {
91
+ const e = err;
92
+ stderr.write(`psql: error: connection to server failed: ${e.message ?? String(err)}\n`);
93
+ return 2;
94
+ }
95
+ settings.db = connection;
96
+ syncConnectionVars(settings.vars, connection);
97
+ const ctx = {
98
+ settings,
99
+ registry: defaultRegistry(),
100
+ cond: createCondStack(),
101
+ stdin,
102
+ stdout,
103
+ stderr
104
+ };
105
+ try {
106
+ if (!settings.quiet && !settings.notty && preActions.length === 0) writeStartupBanner(connection, stdout);
107
+ await loadPsqlrc(ctx, {
108
+ skip: parsed.noPsqlrc,
109
+ env: process.env
110
+ });
111
+ if (preActions.length > 0) return await runPreActions(ctx, preActions, parsed.singleTransaction);
112
+ return await runMainLoop(ctx);
113
+ } finally {
114
+ try {
115
+ await connection.close();
116
+ } catch {}
117
+ }
176
118
  };
177
119
  /**
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
- */
120
+ * Execute the ordered list of `-c` / `-f` actions and return the upstream
121
+ * psql exit code.
122
+ *
123
+ * Upstream `process_psqlrc_and_targets()` (in `startup.c`) plus the
124
+ * dispatcher in `MainLoop()` cooperate to give each switch one of three
125
+ * outcomes:
126
+ *
127
+ * - `-c "SQL or \backslash"`:
128
+ * * a client-side failure (e.g. bad `\copy`) marks the SWITCH itself
129
+ * as failed → the overall exit code is non-zero, even when
130
+ * ON_ERROR_STOP is off and the transaction commits.
131
+ * * a server-side failure follows the same rule.
132
+ *
133
+ * - `-f file`:
134
+ * * `process_file()` returns a success status by default, so a
135
+ * failing statement inside the file does NOT bubble up to the
136
+ * outer exit code (without ON_ERROR_STOP). Only an I/O failure
137
+ * opening the file flips the switch status.
138
+ *
139
+ * - `--single-transaction`:
140
+ * * before the FIRST action, issue `BEGIN`. After the LAST action,
141
+ * issue `COMMIT` (success) or `ROLLBACK` (when ON_ERROR_STOP fired
142
+ * and we stopped early).
143
+ * * Without ON_ERROR_STOP, the transaction commits even when some
144
+ * individual statements failed — the failing statements only
145
+ * influence the exit code (see the `-c` / `-f` distinction above).
146
+ */
205
147
  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;
148
+ const { settings, stderr } = ctx;
149
+ let status = 0;
150
+ let beganTransaction = false;
151
+ let earlyStopOnError = false;
152
+ let connectionLost = false;
153
+ if (singleTransaction && settings.db) try {
154
+ await settings.db.execSimple("BEGIN");
155
+ beganTransaction = true;
156
+ } catch (err) {
157
+ const msg = err instanceof Error ? err.message : String(err);
158
+ stderr.write(`psql: error: could not begin transaction: ${msg}\n`);
159
+ return 1;
160
+ }
161
+ for (const action of preActions) {
162
+ if (connectionLost) break;
163
+ if (action.kind === "command") {
164
+ let outcome;
165
+ try {
166
+ outcome = await executeInputString(action.sql, ctx, { print: true });
167
+ } catch (err) {
168
+ const msg = err instanceof Error ? err.message : String(err);
169
+ stderr.write(`psql: ERROR: ${msg}\n`);
170
+ status = 1;
171
+ if (settings.onErrorStop) {
172
+ earlyStopOnError = true;
173
+ break;
174
+ }
175
+ continue;
176
+ }
177
+ if (outcome.connectionLost) {
178
+ connectionLost = true;
179
+ status = 2;
180
+ break;
181
+ }
182
+ if (outcome.hadError || outcome.stoppedOnError) status = 1;
183
+ if (outcome.stoppedOnError) {
184
+ earlyStopOnError = true;
185
+ break;
186
+ }
187
+ } else {
188
+ let contents;
189
+ try {
190
+ contents = await promises.readFile(action.path, "utf8");
191
+ } catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ stderr.write(`psql: error: ${msg}\n`);
194
+ status = 1;
195
+ if (settings.onErrorStop) {
196
+ earlyStopOnError = true;
197
+ break;
198
+ }
199
+ continue;
200
+ }
201
+ let outcome;
202
+ try {
203
+ outcome = await executeInputString(contents, ctx, { print: true });
204
+ } catch (err) {
205
+ const msg = err instanceof Error ? err.message : String(err);
206
+ stderr.write(`psql: ERROR: ${msg}\n`);
207
+ status = 3;
208
+ if (settings.onErrorStop) {
209
+ earlyStopOnError = true;
210
+ break;
211
+ }
212
+ continue;
213
+ }
214
+ if (outcome.connectionLost) {
215
+ connectionLost = true;
216
+ status = 2;
217
+ break;
218
+ }
219
+ if (outcome.stoppedOnError) {
220
+ status = 3;
221
+ earlyStopOnError = true;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ if (beganTransaction && settings.db && !connectionLost) {
227
+ const closing = earlyStopOnError ? "ROLLBACK" : "COMMIT";
228
+ try {
229
+ await settings.db.execSimple(closing);
230
+ } catch (err) {
231
+ const msg = err instanceof Error ? err.message : String(err);
232
+ stderr.write(`psql: error: ${closing} failed: ${msg}\n`);
233
+ if (status === 0) status = 1;
234
+ }
235
+ }
236
+ return status;
334
237
  };
335
238
  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 (neon, 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');
239
+ const serverVersion = connection.parameterStatus("server_version") ?? "unknown";
240
+ out.write(`psql-ts (neon, server ${serverVersion})\n`);
241
+ const tls = connection.getTlsInfo();
242
+ if (tls) {
243
+ const parts = [
244
+ `protocol: ${tls.protocol}`,
245
+ `cipher: ${tls.cipher}`,
246
+ `compression: ${tls.compression}`
247
+ ];
248
+ if (tls.alpn) parts.push(`ALPN: ${tls.alpn}`);
249
+ out.write(`SSL connection (${parts.join(", ")})\n`);
250
+ }
251
+ out.write("Type \"help\" for help.\n\n");
353
252
  };
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",
253
+ const KNOWN_QUERY_KEYS = /* @__PURE__ */ new Set([
254
+ "host",
255
+ "hostaddr",
256
+ "port",
257
+ "dbname",
258
+ "user",
259
+ "password",
260
+ "passfile",
261
+ "channel_binding",
262
+ "require_auth",
263
+ "connect_timeout",
264
+ "client_encoding",
265
+ "options",
266
+ "application_name",
267
+ "fallback_application_name",
268
+ "keepalives",
269
+ "keepalives_idle",
270
+ "keepalives_interval",
271
+ "keepalives_count",
272
+ "sslmode",
273
+ "sslnegotiation",
274
+ "sslcompression",
275
+ "sslcert",
276
+ "sslkey",
277
+ "sslcertmode",
278
+ "sslrootcert",
279
+ "sslcrl",
280
+ "sslcrldir",
281
+ "sslkeylogfile",
282
+ "sslsni",
283
+ "requirepeer",
284
+ "ssl_min_protocol_version",
285
+ "ssl_max_protocol_version",
286
+ "krbsrvname",
287
+ "gsslib",
288
+ "gssencmode",
289
+ "service",
290
+ "target_session_attrs",
291
+ "load_balance_hosts",
292
+ "replication"
399
293
  ]);
400
294
  /**
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
- */
295
+ * Tokenize a postgres connection URI into raw components.
296
+ *
297
+ * Hand-rolled rather than using `new URL()` because libpq accepts shapes
298
+ * the WHATWG URL parser rejects, e.g. `postgresql://user@` (userinfo with
299
+ * no host), `postgres://:12345/` (port-only), `postgres://uri-user@/db`
300
+ * (userinfo with empty host). The upstream conformance suite in
301
+ * `src/interfaces/libpq/t/001_uri.pl` exercises these forms.
302
+ */
409
303
  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
- };
304
+ let rest;
305
+ if (uri.startsWith("postgresql://")) rest = uri.slice(13);
306
+ else if (uri.startsWith("postgres://")) rest = uri.slice(11);
307
+ else throw new Error(`unsupported scheme in URI: ${uri}`);
308
+ let query = "";
309
+ const qIdx = rest.indexOf("?");
310
+ if (qIdx >= 0) {
311
+ query = rest.slice(qIdx + 1);
312
+ rest = rest.slice(0, qIdx);
313
+ }
314
+ let database;
315
+ const pIdx = rest.indexOf("/");
316
+ if (pIdx >= 0) {
317
+ const pathRaw = rest.slice(pIdx + 1);
318
+ database = pathRaw === "" ? void 0 : decodePercent(pathRaw);
319
+ rest = rest.slice(0, pIdx);
320
+ }
321
+ let userinfo;
322
+ const atIdx = rest.lastIndexOf("@");
323
+ if (atIdx >= 0) {
324
+ userinfo = rest.slice(0, atIdx);
325
+ rest = rest.slice(atIdx + 1);
326
+ }
327
+ let user;
328
+ let password;
329
+ if (userinfo !== void 0) {
330
+ const colon = userinfo.indexOf(":");
331
+ if (colon >= 0) {
332
+ user = decodePercent(userinfo.slice(0, colon));
333
+ password = decodePercent(userinfo.slice(colon + 1));
334
+ } else user = decodePercent(userinfo);
335
+ }
336
+ const tuples = splitAuthorityTuples(rest, uri);
337
+ let host;
338
+ let port;
339
+ const hosts = [];
340
+ for (const tuple of tuples) {
341
+ const parsed = parseAuthorityTuple(tuple, uri);
342
+ if (parsed.host !== void 0) hosts.push({
343
+ host: parsed.host,
344
+ port: parsed.port
345
+ });
346
+ }
347
+ if (hosts.length > 0) {
348
+ host = hosts[0].host;
349
+ port = hosts[0].port;
350
+ }
351
+ const queryMap = parseQuery(query);
352
+ return {
353
+ user,
354
+ password,
355
+ host,
356
+ port,
357
+ ...hosts.length > 1 ? { hosts } : {},
358
+ database,
359
+ query: queryMap
360
+ };
485
361
  };
486
362
  /**
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
- */
363
+ * Split a multi-host authority string into one tuple per top-level comma.
364
+ * IPv6 bracket regions are atomic — commas inside `[…]` don't split.
365
+ *
366
+ * Examples:
367
+ * `h1:5432,h2,h3:5434` -> ['h1:5432','h2','h3:5434']
368
+ * `[::1]:5432,[2001:db8::1]` -> ['[::1]:5432','[2001:db8::1]']
369
+ * `h1` -> ['h1']
370
+ * `` -> [''] (single empty tuple — caller may
371
+ * treat as no-host)
372
+ */
497
373
  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;
374
+ if (rest === "") return [""];
375
+ const tuples = [];
376
+ let start = 0;
377
+ let i = 0;
378
+ while (i < rest.length) {
379
+ const ch = rest[i];
380
+ if (ch === "[") {
381
+ const closeIdx = rest.indexOf("]", i);
382
+ if (closeIdx < 0) throw new Error(`missing matching "]" in IPv6 host address: ${uri}`);
383
+ i = closeIdx + 1;
384
+ continue;
385
+ }
386
+ if (ch === ",") {
387
+ tuples.push(rest.slice(start, i));
388
+ i += 1;
389
+ start = i;
390
+ continue;
391
+ }
392
+ i += 1;
393
+ }
394
+ tuples.push(rest.slice(start));
395
+ return tuples;
523
396
  };
524
397
  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) };
398
+ if (tuple === "") return {};
399
+ if (tuple.startsWith("[")) {
400
+ const closeIdx = tuple.indexOf("]");
401
+ if (closeIdx < 0) throw new Error(`missing matching "]" in IPv6 host address: ${uri}`);
402
+ const host = tuple.slice(1, closeIdx);
403
+ if (host === "") throw new Error(`IPv6 host address may not be empty: ${uri}`);
404
+ const after = tuple.slice(closeIdx + 1);
405
+ if (after === "") return { host };
406
+ if (after.startsWith(":")) return {
407
+ host,
408
+ port: after.slice(1)
409
+ };
410
+ throw new Error(`unexpected characters after IPv6 host address in URI: ${uri}`);
411
+ }
412
+ const colon = tuple.indexOf(":");
413
+ if (colon >= 0) return {
414
+ host: decodePercent(tuple.slice(0, colon)),
415
+ port: tuple.slice(colon + 1)
416
+ };
417
+ return { host: decodePercent(tuple) };
552
418
  };
553
419
  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;
420
+ const out = /* @__PURE__ */ new Map();
421
+ if (raw === "") return out;
422
+ for (const segment of raw.split("&")) {
423
+ if (segment === "") continue;
424
+ const eq = segment.indexOf("=");
425
+ if (eq < 0) throw new Error(`missing "=" after "${segment.trim()}" in connection info string`);
426
+ const keyRaw = segment.slice(0, eq);
427
+ const valueRaw = segment.slice(eq + 1);
428
+ if (valueRaw.includes("=")) throw new Error(`extra "=" in query parameter "${decodePercent(keyRaw).trim()}"`);
429
+ const key = decodePercent(keyRaw).trim();
430
+ const value = decodePercent(valueRaw).trim();
431
+ if (key === "") continue;
432
+ if (!KNOWN_QUERY_KEYS.has(key)) throw new Error(`invalid URI query parameter: "${key}"`);
433
+ out.set(key, value);
434
+ }
435
+ return out;
584
436
  };
585
437
  /**
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
- */
438
+ * Percent-decode a URI component. libpq strictly validates percent-encoding:
439
+ * - `%XX` must be two hex digits
440
+ * - bare `%` or `%X` is invalid
441
+ * - `%00` is forbidden (NUL bytes can't appear in connection params)
442
+ *
443
+ * `decodeURIComponent` throws URIError on malformed escapes — we surface that
444
+ * as a clear Error. It accepts `%00` (returns `\0`); we explicitly reject.
445
+ */
594
446
  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;
447
+ let decoded;
448
+ try {
449
+ decoded = decodeURIComponent(s);
450
+ } catch {
451
+ throw new Error(`invalid percent-encoded token in URI: ${s}`);
452
+ }
453
+ if (decoded.includes("\0")) throw new Error(`forbidden NUL byte (%00) in URI: ${s}`);
454
+ return decoded;
606
455
  };
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
- };
456
+ const parseConnectionUri = (uri) => {
457
+ const raw = tokenizeConnectionUri(uri);
458
+ const queryUser = raw.query.get("user");
459
+ const queryPassword = raw.query.get("password");
460
+ const queryPort = raw.query.get("port");
461
+ const queryDbname = raw.query.get("dbname");
462
+ const queryHost = raw.query.get("host");
463
+ const hostsTuples = computeHostsTuples({
464
+ rawHost: raw.host,
465
+ rawPort: raw.port,
466
+ rawAuthorityHosts: raw.hosts,
467
+ queryHost,
468
+ queryPort
469
+ });
470
+ const host = hostsTuples.length > 0 && hostsTuples[0].host !== "" ? hostsTuples[0].host : "localhost";
471
+ const port = hostsTuples.length > 0 ? hostsTuples[0].port : 5432;
472
+ const user = queryUser !== void 0 && queryUser !== "" ? queryUser : raw.user !== void 0 && raw.user !== "" ? raw.user : process.env.USER ?? "";
473
+ const password = queryPassword ?? raw.password;
474
+ const database = queryDbname ?? raw.database ?? user;
475
+ let ssl = normalizeSslMode(raw.query.get("sslmode") ?? null);
476
+ const channelBinding = normalizeChannelBinding(raw.query.get("channel_binding") ?? null);
477
+ validateGssEncMode(raw.query.get("gssencmode") ?? null);
478
+ const options = raw.query.get("options");
479
+ const applicationName = raw.query.get("application_name") ?? "psql";
480
+ const replication = normalizeReplication(raw.query.get("replication") ?? null);
481
+ const targetSessionAttrs = normalizeTargetSessionAttrs(raw.query.get("target_session_attrs") ?? null);
482
+ const loadBalanceHosts = normalizeLoadBalanceHosts(raw.query.get("load_balance_hosts") ?? null);
483
+ const sslcert = nonEmpty(raw.query.get("sslcert"));
484
+ const sslkey = nonEmpty(raw.query.get("sslkey"));
485
+ const sslcertmode = normalizeSslCertMode(raw.query.get("sslcertmode") ?? null);
486
+ const sslnegotiation = normalizeSslNegotiation(raw.query.get("sslnegotiation") ?? null);
487
+ const sslrootcert = nonEmpty(raw.query.get("sslrootcert"));
488
+ const sslcrl = nonEmpty(raw.query.get("sslcrl"));
489
+ const sslcrldir = nonEmpty(raw.query.get("sslcrldir"));
490
+ const sslkeylogfile = nonEmpty(raw.query.get("sslkeylogfile"));
491
+ const sslsni = parseLibpqBool(nonEmpty(raw.query.get("sslsni")));
492
+ const keepalives = parseLibpqBool(nonEmpty(raw.query.get("keepalives")));
493
+ const keepalivesIdle = parseKeepalivesIdle(nonEmpty(raw.query.get("keepalives_idle")));
494
+ const requirepeer = nonEmpty(raw.query.get("requirepeer"));
495
+ if (sslrootcert === "system" && ssl !== "verify-full") ssl = "verify-full";
496
+ const hostaddr = nonEmpty(raw.query.get("hostaddr"));
497
+ const sslMinProtocolVersion = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_min_protocol_version")), "ssl_min_protocol_version");
498
+ const sslMaxProtocolVersion = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_max_protocol_version")), "ssl_max_protocol_version");
499
+ assertTlsProtocolRange(sslMinProtocolVersion, sslMaxProtocolVersion);
500
+ assertTlsMaxProtocolSupported(sslMaxProtocolVersion);
501
+ assertSslNegotiationModeCompatible(ssl, sslnegotiation);
502
+ return {
503
+ host,
504
+ port,
505
+ user,
506
+ password,
507
+ database,
508
+ ssl,
509
+ channelBinding,
510
+ applicationName,
511
+ options,
512
+ ...sslnegotiation !== void 0 ? { sslnegotiation } : {},
513
+ ...hostaddr !== void 0 ? { hostaddr } : {},
514
+ ...replication !== void 0 ? { replication } : {},
515
+ ...hostsTuples.length > 1 ? { hosts: hostsTuples.map((t) => ({
516
+ host: t.host,
517
+ port: t.port
518
+ })) } : {},
519
+ ...targetSessionAttrs !== void 0 ? { targetSessionAttrs } : {},
520
+ ...loadBalanceHosts !== void 0 ? { loadBalanceHosts } : {},
521
+ ...sslcert !== void 0 ? { sslcert } : {},
522
+ ...sslkey !== void 0 ? { sslkey } : {},
523
+ ...sslcertmode !== void 0 ? { sslcertmode } : {},
524
+ ...sslrootcert !== void 0 ? { sslrootcert } : {},
525
+ ...sslcrl !== void 0 ? { sslcrl } : {},
526
+ ...sslcrldir !== void 0 ? { sslcrldir } : {},
527
+ ...sslkeylogfile !== void 0 ? { sslkeylogfile } : {},
528
+ ...sslsni !== void 0 ? { sslsni } : {},
529
+ ...keepalives !== void 0 ? { keepalives } : {},
530
+ ...keepalivesIdle !== void 0 ? { keepalivesIdle } : {},
531
+ ...requirepeer !== void 0 ? { requirepeer } : {},
532
+ ...sslMinProtocolVersion !== void 0 ? { sslMinProtocolVersion } : {},
533
+ ...sslMaxProtocolVersion !== void 0 ? { sslMaxProtocolVersion } : {}
534
+ };
725
535
  };
726
536
  /**
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
- */
537
+ * Parse a libpq 0/1 boolean connection parameter (`sslsni`, `keepalives`).
538
+ * libpq's `parse_bool_with_len` accepts `1`/`0`, `true`/`false`, `yes`/`no`,
539
+ * `on`/`off` (case-insensitive). Returns `undefined` for unset / empty /
540
+ * unrecognised so the caller falls back to libpq's default (enabled).
541
+ */
732
542
  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
- }
543
+ if (raw === void 0 || raw === "") return void 0;
544
+ switch (raw.toLowerCase()) {
545
+ case "1":
546
+ case "true":
547
+ case "yes":
548
+ case "on": return true;
549
+ case "0":
550
+ case "false":
551
+ case "no":
552
+ case "off": return false;
553
+ default: return;
554
+ }
749
555
  };
750
556
  /**
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
- */
557
+ * Parse `keepalives_idle` (seconds, non-negative integer). Returns the value
558
+ * in seconds, or `undefined` if unset / malformed (the wire layer converts to
559
+ * milliseconds for `socket.setKeepAlive`'s `initialDelay`).
560
+ */
755
561
  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;
562
+ if (raw === void 0 || raw === "") return void 0;
563
+ const n = Number.parseInt(raw, 10);
564
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
760
565
  };
761
- const nonEmpty = (v) => v === undefined || v === "" ? undefined : v;
566
+ const nonEmpty = (v) => v === void 0 || v === "" ? void 0 : v;
762
567
  /**
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
- */
568
+ * Resolve the final {host, port}[] list for a URI:
569
+ *
570
+ * 1. Start from the authority. `?host=`/`?port=` query overrides take
571
+ * precedence (libpq semantics).
572
+ * 2. If `host=h1,h2,…` is supplied, parse the comma-list. Ports come from
573
+ * `port=p1,p2,…`; must match the host count or be a single value
574
+ * (broadcast to all hosts).
575
+ * 3. If only the authority had multi-host tuples (`postgresql://h1,h2/db`),
576
+ * use those.
577
+ * 4. Otherwise fall back to single-host.
578
+ *
579
+ * Validates every port is in 1..65535 and surfaces a clear error otherwise.
580
+ */
776
581
  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) }];
582
+ const { rawHost, rawPort, rawAuthorityHosts, queryHost, queryPort } = input;
583
+ if (queryHost !== void 0 && queryHost !== "") {
584
+ const hosts = queryHost.split(",").map((h) => h.trim());
585
+ const portList = queryPort !== void 0 && queryPort !== "" ? queryPort.split(",").map((p) => p.trim()) : null;
586
+ if (portList !== null && portList.length !== 1 && portList.length !== hosts.length) throw new Error(`could not match ${String(portList.length)} port numbers to ${String(hosts.length)} hosts`);
587
+ return hosts.map((h, idx) => {
588
+ let portStr;
589
+ if (portList !== null) portStr = portList.length === 1 ? portList[0] : portList[idx];
590
+ else portStr = rawPort;
591
+ return {
592
+ host: h,
593
+ port: parsePort(portStr)
594
+ };
595
+ });
596
+ }
597
+ if (rawAuthorityHosts !== void 0 && rawAuthorityHosts.length > 0) {
598
+ const portList = queryPort !== void 0 && queryPort !== "" ? queryPort.split(",").map((p) => p.trim()) : null;
599
+ if (portList !== null && portList.length !== 1 && portList.length !== rawAuthorityHosts.length) throw new Error(`could not match ${String(portList.length)} port numbers to ${String(rawAuthorityHosts.length)} hosts`);
600
+ return rawAuthorityHosts.map((t, idx) => ({
601
+ host: t.host,
602
+ port: parsePort(portList !== null ? portList.length === 1 ? portList[0] : portList[idx] : t.port)
603
+ }));
604
+ }
605
+ return [{
606
+ host: rawHost !== void 0 && rawHost !== "" ? rawHost : "",
607
+ port: parsePort(queryPort ?? rawPort)
608
+ }];
828
609
  };
829
610
  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;
611
+ if (raw === void 0 || raw === "") return 5432;
612
+ if (/^\d/.test(raw) && !/^\d+$/.test(raw)) throw new Error(`invalid integer value "${raw}" for connection option "port"`);
613
+ const p = Number.parseInt(raw, 10);
614
+ if (!Number.isFinite(p) || p <= 0 || p > 65535) throw new Error(`invalid port: ${raw}`);
615
+ return p;
848
616
  };
849
617
  /**
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;
618
+ * Parse a libpq-style conninfo string into a `Partial<ConnectOptions>`.
619
+ *
620
+ * The grammar is roughly `key = value` pairs separated by whitespace, where
621
+ * values may be quoted with single quotes (libpq's `\'` is honored as an
622
+ * embedded single-quote, but we don't model the full backslash-escape
623
+ * universe — there's no test corpus that exercises it). Unknown keys are
624
+ * rejected so a typo like `replicate=database` produces a clear error
625
+ * rather than silently dropping the parameter.
626
+ *
627
+ * Recognised keys mirror the URI-side `KNOWN_QUERY_KEYS` allowlist plus
628
+ * the authority-style keys (`host`, `port`, `user`, `dbname`, `password`).
629
+ * `replication` is normalised through `normalizeReplication` for libpq
630
+ * value-set compatibility.
631
+ *
632
+ * Out of scope: percent-decoding (conninfo strings are NOT percent-encoded),
633
+ * `service` resolution from `pg_service.conf`, `passfile` resolution.
634
+ */
635
+ const parseConninfo = (input) => {
636
+ const out = {};
637
+ let i = 0;
638
+ const n = input.length;
639
+ while (i < n) {
640
+ while (i < n && /\s/.test(input[i])) i += 1;
641
+ if (i >= n) break;
642
+ const keyStart = i;
643
+ while (i < n && input[i] !== "=" && !/\s/.test(input[i])) i += 1;
644
+ const key = input.slice(keyStart, i).toLowerCase();
645
+ if (key === "") break;
646
+ while (i < n && /\s/.test(input[i])) i += 1;
647
+ if (i >= n || input[i] !== "=") throw new Error(`missing "=" after "${key}" in conninfo string`);
648
+ i += 1;
649
+ while (i < n && /\s/.test(input[i])) i += 1;
650
+ let value;
651
+ if (i < n && input[i] === "'") {
652
+ i += 1;
653
+ const parts = [];
654
+ while (i < n && input[i] !== "'") if (input[i] === "\\" && i + 1 < n) {
655
+ parts.push(input[i + 1]);
656
+ i += 2;
657
+ } else {
658
+ parts.push(input[i]);
659
+ i += 1;
660
+ }
661
+ if (i >= n) throw new Error(`unterminated single quote in conninfo string for key "${key}"`);
662
+ i += 1;
663
+ value = parts.join("");
664
+ } else {
665
+ const valStart = i;
666
+ while (i < n && !/\s/.test(input[i])) i += 1;
667
+ value = input.slice(valStart, i);
668
+ }
669
+ applyConninfoPair(out, key, value);
670
+ }
671
+ const hostList = out._hostList;
672
+ const portList = out._portList;
673
+ if (hostList !== void 0 && hostList.length > 0) {
674
+ if (portList !== void 0 && portList.length !== 1 && portList.length !== hostList.length) throw new Error(`could not match ${String(portList.length)} port numbers to ${String(hostList.length)} hosts`);
675
+ if (hostList.length > 1) out.hosts = hostList.map((h, idx) => ({
676
+ host: h,
677
+ port: portList === void 0 ? out.port ?? 5432 : portList.length === 1 ? parsePort(portList[0]) : parsePort(portList[idx])
678
+ }));
679
+ }
680
+ delete out._hostList;
681
+ delete out._portList;
682
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
683
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
684
+ return out;
956
685
  };
957
686
  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
- }
687
+ switch (key) {
688
+ case "host":
689
+ if (value.includes(",")) {
690
+ out._hostList = value.split(",").map((h) => h.trim());
691
+ out.host = out._hostList[0];
692
+ } else {
693
+ out.host = value;
694
+ out._hostList = void 0;
695
+ }
696
+ return;
697
+ case "port":
698
+ if (value.includes(",")) {
699
+ out._portList = value.split(",").map((p) => p.trim());
700
+ out.port = parsePort(out._portList[0]);
701
+ } else {
702
+ out.port = parsePort(value);
703
+ out._portList = void 0;
704
+ }
705
+ return;
706
+ case "user":
707
+ out.user = value;
708
+ return;
709
+ case "password":
710
+ out.password = value;
711
+ return;
712
+ case "dbname":
713
+ out.database = value;
714
+ return;
715
+ case "application_name":
716
+ out.applicationName = value;
717
+ return;
718
+ case "sslmode":
719
+ out.ssl = normalizeSslMode(value);
720
+ return;
721
+ case "channel_binding": {
722
+ const cb = normalizeChannelBinding(value);
723
+ if (cb !== void 0) out.channelBinding = cb;
724
+ return;
725
+ }
726
+ case "require_auth": {
727
+ const ra = normalizeRequireAuth(value);
728
+ if (ra !== void 0) out.requireAuth = ra;
729
+ return;
730
+ }
731
+ case "connect_timeout": {
732
+ const t = Number.parseInt(value, 10);
733
+ if (Number.isFinite(t) && t >= 0) out.connectTimeoutMs = t * 1e3;
734
+ return;
735
+ }
736
+ case "client_encoding":
737
+ out.clientEncoding = value;
738
+ return;
739
+ case "options":
740
+ out.options = value;
741
+ return;
742
+ case "replication": {
743
+ const rep = normalizeReplication(value);
744
+ if (rep !== void 0) out.replication = rep;
745
+ return;
746
+ }
747
+ case "target_session_attrs": {
748
+ const tsa = normalizeTargetSessionAttrs(value);
749
+ if (tsa !== void 0) out.targetSessionAttrs = tsa;
750
+ return;
751
+ }
752
+ case "load_balance_hosts": {
753
+ const lbh = normalizeLoadBalanceHosts(value);
754
+ if (lbh !== void 0) out.loadBalanceHosts = lbh;
755
+ return;
756
+ }
757
+ case "gssencmode":
758
+ validateGssEncMode(value);
759
+ return;
760
+ case "sslcert":
761
+ if (value !== "") out.sslcert = value;
762
+ return;
763
+ case "sslkey":
764
+ if (value !== "") out.sslkey = value;
765
+ return;
766
+ case "sslcertmode": {
767
+ const cm = normalizeSslCertMode(value);
768
+ if (cm !== void 0) out.sslcertmode = cm;
769
+ return;
770
+ }
771
+ case "sslnegotiation": {
772
+ const sn = normalizeSslNegotiation(value);
773
+ if (sn !== void 0) out.sslnegotiation = sn;
774
+ return;
775
+ }
776
+ case "sslrootcert":
777
+ if (value !== "") out.sslrootcert = value;
778
+ return;
779
+ case "sslcrl":
780
+ if (value !== "") out.sslcrl = value;
781
+ return;
782
+ case "sslcrldir":
783
+ if (value !== "") out.sslcrldir = value;
784
+ return;
785
+ case "sslkeylogfile":
786
+ if (value !== "") out.sslkeylogfile = value;
787
+ return;
788
+ case "hostaddr":
789
+ if (value !== "") out.hostaddr = value;
790
+ return;
791
+ case "ssl_min_protocol_version": {
792
+ const v = normalizeTlsProtocolVersion(value === "" ? void 0 : value, "ssl_min_protocol_version");
793
+ if (v !== void 0) out.sslMinProtocolVersion = v;
794
+ return;
795
+ }
796
+ case "ssl_max_protocol_version": {
797
+ const v = normalizeTlsProtocolVersion(value === "" ? void 0 : value, "ssl_max_protocol_version");
798
+ if (v !== void 0) out.sslMaxProtocolVersion = v;
799
+ return;
800
+ }
801
+ case "sslsni": {
802
+ const b = parseLibpqBool(value);
803
+ if (b !== void 0) out.sslsni = b;
804
+ return;
805
+ }
806
+ case "keepalives": {
807
+ const b = parseLibpqBool(value);
808
+ if (b !== void 0) out.keepalives = b;
809
+ return;
810
+ }
811
+ case "keepalives_idle": {
812
+ const n = parseKeepalivesIdle(value);
813
+ if (n !== void 0) out.keepalivesIdle = n;
814
+ return;
815
+ }
816
+ case "requirepeer":
817
+ if (value !== "") out.requirepeer = value;
818
+ return;
819
+ case "passfile":
820
+ case "sslcompression":
821
+ case "krbsrvname":
822
+ case "gsslib":
823
+ case "fallback_application_name":
824
+ case "keepalives_interval":
825
+ case "keepalives_count": return;
826
+ case "service":
827
+ out._service = value;
828
+ return;
829
+ default: throw new Error(`invalid conninfo key: "${key}"`);
830
+ }
1145
831
  };
1146
832
  /**
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);
833
+ * Heuristic: does the `-d` value look like a connection URI or a conninfo
834
+ * string (vs. a bare database name)? Mirrors libpq's
835
+ * `recognized_connection_string()` test.
836
+ */
837
+ const looksLikeConnectionString = (s) => {
838
+ if (s.startsWith("postgresql://") || s.startsWith("postgres://")) return true;
839
+ const eq = s.indexOf("=");
840
+ if (eq < 0) return false;
841
+ const head = s.slice(0, eq);
842
+ return !/\s/.test(head);
1162
843
  };
1163
844
  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
- }
845
+ if (raw === null) return "prefer";
846
+ const value = raw.toLowerCase();
847
+ switch (value) {
848
+ case "disable":
849
+ case "allow":
850
+ case "prefer":
851
+ case "require":
852
+ case "verify-ca":
853
+ case "verify-full": return value;
854
+ default: throw new Error(`invalid sslmode value: "${raw}"`);
855
+ }
1183
856
  };
1184
857
  /**
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"];
858
+ * libpq's accepted TLS protocol-version names, in ascending order. The
859
+ * index doubles as the comparison key for the `min > max` check. Matching
860
+ * is case-insensitive on input (libpq lowercases before comparing) but we
861
+ * keep the canonical mixed-case spelling Node's `tls` module expects for
862
+ * `minVersion` / `maxVersion`.
863
+ */
864
+ const TLS_PROTOCOL_VERSIONS = [
865
+ "TLSv1",
866
+ "TLSv1.1",
867
+ "TLSv1.2",
868
+ "TLSv1.3"
869
+ ];
1192
870
  /**
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
- */
871
+ * Validate and canonicalise a `ssl_{min,max}_protocol_version` value. Returns
872
+ * the canonical spelling (`TLSv1.2` etc.) or `undefined` for empty / unset.
873
+ * Throws libpq's `invalid <key> value: "<raw>"` wording on a malformed value.
874
+ */
1197
875
  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;
876
+ if (raw === void 0 || raw === "") return void 0;
877
+ const match = TLS_PROTOCOL_VERSIONS.find((v) => v.toLowerCase() === raw.toLowerCase());
878
+ if (match === void 0) throw new Error(`invalid ${key} value: "${raw}"`);
879
+ return match;
1205
880
  };
1206
881
  /**
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
- */
882
+ * Reject a `ssl_max_protocol_version` ceiling below TLSv1.2. TLS 1.0/1.1 are
883
+ * disabled in Node's bundled OpenSSL, so capping the ceiling there leaves no
884
+ * negotiable protocol and the handshake otherwise fails with an opaque
885
+ * `ERR_SSL_NO_PROTOCOLS_AVAILABLE` — surface an actionable message at parse
886
+ * time instead. Only the MAX is gated: a low *min*
887
+ * (`ssl_min_protocol_version=TLSv1.1`) is harmless because Node still
888
+ * negotiates the highest mutually-supported version (1.2/1.3), exactly as
889
+ * libpq does on a modern OpenSSL. Called AFTER {@link assertTlsProtocolRange}
890
+ * so an inverted range (min > max) reports the range error first, matching
891
+ * libpq's ordering.
892
+ */
1218
893
  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
- }
894
+ if (max === "TLSv1" || max === "TLSv1.1") throw new Error(`ssl_max_protocol_version "${max}" is not supported by this runtime's TLS library — TLS 1.0/1.1 are disabled in Node's OpenSSL; the minimum negotiable version is TLSv1.2`);
1224
895
  };
1225
896
  /**
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
- */
897
+ * Reject a `ssl_min_protocol_version` that is higher than
898
+ * `ssl_max_protocol_version`, matching libpq's
899
+ * `ssl_min_protocol_version must be <= ssl_max_protocol_version` diagnostic.
900
+ * Both arguments must already be canonicalised by
901
+ * {@link normalizeTlsProtocolVersion}.
902
+ */
1232
903
  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
- }
904
+ if (min === void 0 || max === void 0) return;
905
+ if (TLS_PROTOCOL_VERSIONS.indexOf(min) > TLS_PROTOCOL_VERSIONS.indexOf(max)) throw new Error(`ssl_min_protocol_version must be <= ssl_max_protocol_version`);
1238
906
  };
1239
907
  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
- }
908
+ if (raw === null || raw === "") return void 0;
909
+ const value = raw.toLowerCase();
910
+ switch (value) {
911
+ case "disable":
912
+ case "prefer":
913
+ case "require": return value;
914
+ default: throw new Error(`invalid channel_binding value: "${raw}"`);
915
+ }
1254
916
  };
1255
917
  /**
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
- */
918
+ * Validate libpq's `gssencmode` (GSSAPI transport encryption).
919
+ *
920
+ * This client has NO GSSAPI support: GSS-API `gss_wrap`/`gss_unwrap` would
921
+ * require a native Kerberos addon (e.g. the `kerberos` npm), which the
922
+ * embedded psql deliberately avoids (pure-TS, zero native bindings — the
923
+ * same reason the line editor is hand-rolled). `node-postgres` doesn't
924
+ * support it either. So:
925
+ * - `disable` / `prefer` — accepted and ignored: neither needs GSS
926
+ * (`prefer` means "try GSS, else fall back", and falling back to the
927
+ * non-GSS path is exactly what we always do).
928
+ * - `require` — rejected with a clear diagnostic; we cannot satisfy it.
929
+ * - anything else — `invalid gssencmode value`.
930
+ * We recognise the parameter (rather than rejecting it as an unknown key)
931
+ * so the many tools that always append `gssencmode=...` to a URI keep
932
+ * working against Neon.
933
+ */
1272
934
  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}"`);
935
+ if (raw === null || raw === "") return;
936
+ const value = raw.toLowerCase();
937
+ if (value === "disable" || value === "prefer") return;
938
+ if (value === "require") throw new Error("gssencmode=require is not supported: this client has no GSSAPI support");
939
+ throw new Error(`invalid gssencmode value: "${raw}"`);
1282
940
  };
1283
941
  /**
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
- */
942
+ * Parse libpq's `sslcertmode` value (`disable` / `allow` / `require`).
943
+ * Empty / unset returns `undefined` so the wire-layer default (`allow`)
944
+ * applies. A malformed value throws libpq's
945
+ * `invalid sslcertmode value: "<raw>"` diagnostic.
946
+ */
1289
947
  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
- }
948
+ if (raw === null || raw === "") return void 0;
949
+ const value = raw.toLowerCase();
950
+ switch (value) {
951
+ case "disable":
952
+ case "allow":
953
+ case "require": return value;
954
+ default: throw new Error(`invalid sslcertmode value: "${raw}"`);
955
+ }
1301
956
  };
1302
957
  /**
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
- */
958
+ * Parse libpq's `sslnegotiation` value (`postgres` / `direct`). Empty / unset
959
+ * returns `undefined` so the wire-layer default (`postgres`, the classic
960
+ * SSLRequest flow) applies. A malformed value throws libpq's
961
+ * `invalid sslnegotiation value: "<raw>"` diagnostic.
962
+ */
1308
963
  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
- }
964
+ if (raw === null || raw === "") return void 0;
965
+ const value = raw.toLowerCase();
966
+ switch (value) {
967
+ case "postgres":
968
+ case "direct": return value;
969
+ default: throw new Error(`invalid sslnegotiation value: "${raw}"`);
970
+ }
1319
971
  };
1320
972
  /**
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
- */
973
+ * libpq constraint: `sslnegotiation=direct` may only be used with an encrypted
974
+ * sslmode (`require` / `verify-ca` / `verify-full`). Direct SSL starts the TLS
975
+ * handshake immediately with no plaintext fallback, so a "weak" mode that could
976
+ * end up unencrypted (`disable` / `allow` / `prefer`) is rejected with libpq's
977
+ * exact `pqConnectOptions2` wording. No-op unless `sslnegotiation` is `direct`.
978
+ */
1327
979
  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`);
980
+ if (sslnegotiation !== "direct") return;
981
+ if (ssl === "require" || ssl === "verify-ca" || ssl === "verify-full") return;
982
+ throw new Error(`weak sslmode "${ssl}" may not be used with sslnegotiation=direct`);
1334
983
  };
1335
- const VALID_REQUIRE_AUTH_METHODS = new Set([
1336
- "password",
1337
- "md5",
1338
- "gss",
1339
- "sspi",
1340
- "scram-sha-256",
1341
- "creds",
1342
- "none",
984
+ const VALID_REQUIRE_AUTH_METHODS = /* @__PURE__ */ new Set([
985
+ "password",
986
+ "md5",
987
+ "gss",
988
+ "sspi",
989
+ "scram-sha-256",
990
+ "creds",
991
+ "none"
1343
992
  ]);
1344
993
  /**
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
- */
994
+ * Parse libpq's `require_auth` value: a comma-separated list of method
995
+ * names where each entry may be prefixed with `!` to negate. Mixing
996
+ * positive and negative entries is forbidden (libpq matches this).
997
+ *
998
+ * Returns `undefined` for empty input so the wire-layer default applies.
999
+ * Throws on invalid syntax with libpq-parity wording, surfaced via the
1000
+ * outer `psql: error: ...` channel.
1001
+ */
1353
1002
  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 };
1003
+ if (raw === null || raw === "") return void 0;
1004
+ const tokens = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1005
+ if (tokens.length === 0) return void 0;
1006
+ const methods = /* @__PURE__ */ new Set();
1007
+ let polarity = null;
1008
+ for (const token of tokens) {
1009
+ const isNeg = token.startsWith("!");
1010
+ const name = (isNeg ? token.slice(1) : token).toLowerCase();
1011
+ if (!VALID_REQUIRE_AUTH_METHODS.has(name)) throw new Error(`invalid require_auth method: "${token}"`);
1012
+ if (polarity === null) polarity = isNeg;
1013
+ else if (polarity !== isNeg) throw new Error("require_auth methods cannot mix positive and negative entries");
1014
+ methods.add(name);
1015
+ }
1016
+ return {
1017
+ methods,
1018
+ negated: polarity ?? false
1019
+ };
1381
1020
  };
1382
1021
  /**
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
- */
1022
+ * Accept the libpq-spec set for `target_session_attrs`. Aliases `read-write`/
1023
+ * `primary` and `read-only`/`standby` are kept distinct because the wire
1024
+ * layer treats the canonical four values identically — we only normalise
1025
+ * unknown / empty inputs to `undefined` so the wire-layer default ('any')
1026
+ * applies. Throws on unrecognised values, matching libpq behaviour.
1027
+ */
1389
1028
  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
- }
1029
+ if (raw === null || raw === "") return void 0;
1030
+ const value = raw.toLowerCase();
1031
+ switch (value) {
1032
+ case "any":
1033
+ case "read-write":
1034
+ case "read-only":
1035
+ case "primary":
1036
+ case "standby":
1037
+ case "prefer-standby": return value;
1038
+ default: throw new Error(`invalid value for "target_session_attrs": "${raw}"`);
1039
+ }
1404
1040
  };
1405
1041
  /**
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
- */
1042
+ * Accept the libpq-spec set for `load_balance_hosts`:
1043
+ * - `disable` (default) — preserve list order
1044
+ * - `random` — shuffle before iteration
1045
+ *
1046
+ * Unknown values throw; empty / unset returns `undefined` so the wire layer
1047
+ * default ('disable') applies.
1048
+ */
1413
1049
  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}"`);
1050
+ if (raw === null || raw === "") return void 0;
1051
+ const value = raw.toLowerCase();
1052
+ if (value === "disable" || value === "random") return value;
1053
+ throw new Error(`invalid value for "load_balance_hosts": "${raw}"`);
1420
1054
  };
1421
1055
  /**
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
- */
1056
+ * libpq accepts a wide set of "truthy" values for `replication`:
1057
+ * - `true` / `on` / `yes` / `1` → physical replication (mapped to `true`)
1058
+ * - `false` / `off` / `no` / `0` → not a walsender (no replication mode)
1059
+ * - `database` → logical replication on that DB
1060
+ *
1061
+ * Returns `undefined` when no value was supplied or when it explicitly
1062
+ * disables replication; throws for unrecognised input (matching libpq's
1063
+ * `"invalid <…> value"` semantics so users see a clear error rather than
1064
+ * silently sending an unexpected startup-message parameter).
1065
+ */
1432
1066
  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}"`);
1067
+ if (raw === null || raw === "") return void 0;
1068
+ const value = raw.toLowerCase();
1069
+ if (value === "database") return "database";
1070
+ if (value === "true" || value === "on" || value === "yes" || value === "1") return "true";
1071
+ if (value === "false" || value === "off" || value === "no" || value === "0") return;
1072
+ throw new Error(`invalid value for "replication": "${raw}"`);
1451
1073
  };
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
1074
  /**
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;
1075
+ * Service name extracted from a connection URI's `?service=` query
1076
+ * parameter, if any. Surfaced alongside `parseConnectionUriPartial` so the
1077
+ * caller can route the lookup into `applyStartupArgs` without re-parsing
1078
+ * the URI.
1079
+ */
1080
+ const parseConnectionUriService = (uri) => {
1081
+ const value = tokenizeConnectionUri(uri).query.get("service");
1082
+ return value === void 0 || value === "" ? void 0 : value;
1481
1083
  };
1482
1084
  /**
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;
1085
+ * Parse a URI into a `Partial<ConnectOptions>` containing only the fields
1086
+ * the URI explicitly supplied. Returned shape:
1087
+ *
1088
+ * - missing fields are absent (no `undefined` placeholders)
1089
+ * - `host`/`port` are populated only when the URI authority or `?host=`
1090
+ * specified them
1091
+ * - `user`/`password`/`database`/`ssl`/`channelBinding`/... follow the
1092
+ * same rule — present iff explicitly set
1093
+ *
1094
+ * This is the building block for the layered merge in `applyStartupArgs`.
1095
+ * The full-defaults variant `parseConnectionUri` remains the right choice
1096
+ * for callers that want a complete `ConnectOptions` (e.g. the existing
1097
+ * `-d URI` path); it's kept untouched for back-compat.
1098
+ */
1099
+ const parseConnectionUriPartial = (uri) => {
1100
+ const raw = tokenizeConnectionUri(uri);
1101
+ const queryUser = raw.query.get("user");
1102
+ const queryPassword = raw.query.get("password");
1103
+ const queryPort = raw.query.get("port");
1104
+ const queryDbname = raw.query.get("dbname");
1105
+ const queryHost = raw.query.get("host");
1106
+ const hostsTuples = computeHostsTuples({
1107
+ rawHost: raw.host,
1108
+ rawPort: raw.port,
1109
+ rawAuthorityHosts: raw.hosts,
1110
+ queryHost,
1111
+ queryPort
1112
+ });
1113
+ const portInUri = raw.port !== void 0 && raw.port !== "" || queryPort !== void 0 && queryPort !== "" || (raw.hosts?.some((t) => t.port !== void 0 && t.port !== "") ?? false);
1114
+ const out = {};
1115
+ if (hostsTuples.length > 0) {
1116
+ if (hostsTuples[0].host !== "") out.host = hostsTuples[0].host;
1117
+ if (portInUri && hostsTuples[0].port !== 0) out.port = hostsTuples[0].port;
1118
+ if (hostsTuples.length > 1) out.hosts = hostsTuples.map((t) => ({
1119
+ host: t.host,
1120
+ port: t.port
1121
+ }));
1122
+ }
1123
+ const userExplicit = queryUser !== void 0 && queryUser !== "" ? queryUser : raw.user !== void 0 && raw.user !== "" ? raw.user : void 0;
1124
+ if (userExplicit !== void 0) out.user = userExplicit;
1125
+ const password = queryPassword ?? raw.password;
1126
+ if (password !== void 0) out.password = password;
1127
+ const database = queryDbname ?? raw.database;
1128
+ if (database !== void 0) out.database = database;
1129
+ const sslRaw = raw.query.get("sslmode");
1130
+ if (sslRaw !== void 0 && sslRaw !== "") out.ssl = normalizeSslMode(sslRaw);
1131
+ const cb = normalizeChannelBinding(raw.query.get("channel_binding") ?? null);
1132
+ if (cb !== void 0) out.channelBinding = cb;
1133
+ const ra = normalizeRequireAuth(raw.query.get("require_auth") ?? null);
1134
+ if (ra !== void 0) out.requireAuth = ra;
1135
+ const options = raw.query.get("options");
1136
+ if (options !== void 0 && options !== "") out.options = options;
1137
+ const appName = raw.query.get("application_name");
1138
+ if (appName !== void 0 && appName !== "") out.applicationName = appName;
1139
+ const replication = normalizeReplication(raw.query.get("replication") ?? null);
1140
+ if (replication !== void 0) out.replication = replication;
1141
+ const targetSessionAttrs = normalizeTargetSessionAttrs(raw.query.get("target_session_attrs") ?? null);
1142
+ if (targetSessionAttrs !== void 0) out.targetSessionAttrs = targetSessionAttrs;
1143
+ const loadBalanceHosts = normalizeLoadBalanceHosts(raw.query.get("load_balance_hosts") ?? null);
1144
+ if (loadBalanceHosts !== void 0) out.loadBalanceHosts = loadBalanceHosts;
1145
+ const sslcert = nonEmpty(raw.query.get("sslcert"));
1146
+ if (sslcert !== void 0) out.sslcert = sslcert;
1147
+ const sslkey = nonEmpty(raw.query.get("sslkey"));
1148
+ if (sslkey !== void 0) out.sslkey = sslkey;
1149
+ const sslcertmode = normalizeSslCertMode(raw.query.get("sslcertmode") ?? null);
1150
+ if (sslcertmode !== void 0) out.sslcertmode = sslcertmode;
1151
+ const sslnegotiation = normalizeSslNegotiation(raw.query.get("sslnegotiation") ?? null);
1152
+ if (sslnegotiation !== void 0) out.sslnegotiation = sslnegotiation;
1153
+ const sslrootcert = nonEmpty(raw.query.get("sslrootcert"));
1154
+ if (sslrootcert !== void 0) out.sslrootcert = sslrootcert;
1155
+ const sslcrl = nonEmpty(raw.query.get("sslcrl"));
1156
+ if (sslcrl !== void 0) out.sslcrl = sslcrl;
1157
+ const sslcrldir = nonEmpty(raw.query.get("sslcrldir"));
1158
+ if (sslcrldir !== void 0) out.sslcrldir = sslcrldir;
1159
+ const sslkeylogfile = nonEmpty(raw.query.get("sslkeylogfile"));
1160
+ if (sslkeylogfile !== void 0) out.sslkeylogfile = sslkeylogfile;
1161
+ const sslsni = parseLibpqBool(nonEmpty(raw.query.get("sslsni")));
1162
+ if (sslsni !== void 0) out.sslsni = sslsni;
1163
+ const keepalives = parseLibpqBool(nonEmpty(raw.query.get("keepalives")));
1164
+ if (keepalives !== void 0) out.keepalives = keepalives;
1165
+ const keepalivesIdle = parseKeepalivesIdle(nonEmpty(raw.query.get("keepalives_idle")));
1166
+ if (keepalivesIdle !== void 0) out.keepalivesIdle = keepalivesIdle;
1167
+ const requirepeer = nonEmpty(raw.query.get("requirepeer"));
1168
+ if (requirepeer !== void 0) out.requirepeer = requirepeer;
1169
+ const hostaddr = nonEmpty(raw.query.get("hostaddr"));
1170
+ if (hostaddr !== void 0) out.hostaddr = hostaddr;
1171
+ const sslMin = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_min_protocol_version")), "ssl_min_protocol_version");
1172
+ if (sslMin !== void 0) out.sslMinProtocolVersion = sslMin;
1173
+ const sslMax = normalizeTlsProtocolVersion(nonEmpty(raw.query.get("ssl_max_protocol_version")), "ssl_max_protocol_version");
1174
+ if (sslMax !== void 0) out.sslMaxProtocolVersion = sslMax;
1175
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
1176
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
1177
+ const connectTimeoutSec = raw.query.get("connect_timeout");
1178
+ if (connectTimeoutSec !== void 0 && connectTimeoutSec !== "") {
1179
+ const t = Number.parseInt(connectTimeoutSec, 10);
1180
+ if (Number.isFinite(t) && t >= 0) out.connectTimeoutMs = t * 1e3;
1181
+ }
1182
+ const clientEncoding = raw.query.get("client_encoding");
1183
+ if (clientEncoding !== void 0 && clientEncoding !== "") out.clientEncoding = clientEncoding;
1184
+ return out;
1639
1185
  };
1640
- // Field map for PG* env vars. Order in the table is documentation; resolution
1641
- // only depends on whether the var is set.
1642
1186
  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",
1187
+ PGHOST: "host",
1188
+ PGHOSTADDR: "hostaddr",
1189
+ PGPORT: "port",
1190
+ PGUSER: "user",
1191
+ PGDATABASE: "database",
1192
+ PGPASSWORD: "password",
1193
+ PGAPPNAME: "applicationName",
1194
+ PGOPTIONS: "options",
1195
+ PGCLIENTENCODING: "clientEncoding",
1196
+ PGSSLMODE: "ssl",
1197
+ PGSSLROOTCERT: "sslrootcert",
1198
+ PGSSLCERT: "sslcert",
1199
+ PGSSLKEY: "sslkey",
1200
+ PGSSLCERTMODE: "sslcertmode",
1201
+ PGSSLNEGOTIATION: "sslnegotiation",
1202
+ PGSSLCRL: "sslcrl",
1203
+ PGSSLCRLDIR: "sslcrldir",
1204
+ PGSSLKEYLOGFILE: "sslkeylogfile",
1205
+ PGCHANNELBINDING: "channelBinding"
1662
1206
  };
1663
1207
  /**
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;
1208
+ * Resolve the PG* env vars into a `Partial<ConnectOptions>`. Only set keys
1209
+ * end up in the result; unset / empty env vars are skipped so the caller
1210
+ * can layer this between URI overrides and pgpass / service / libpq
1211
+ * defaults without clobbering anything.
1212
+ *
1213
+ * Validation: any malformed value (e.g. `PGPORT=abc`) is silently dropped.
1214
+ * libpq behaves the same — the connection then fails later with a clearer
1215
+ * "could not parse" message, but the env-var lookup itself does not throw.
1216
+ *
1217
+ * Notes:
1218
+ * - `PGHOSTADDR` maps to {@link ConnectOptions.hostaddr}: the wire layer
1219
+ * dials this fixed IP while `PGHOST` still drives TLS SNI / cert
1220
+ * verification.
1221
+ * - `PGCONNECT_TIMEOUT` is in seconds; we convert to milliseconds.
1222
+ * - `PGCHANNELBINDING` accepts disable/prefer/require.
1223
+ * - `PGSERVICE` is consumed by the caller (it drives the
1224
+ * pg_service.conf lookup) and is NOT a direct ConnectOptions field.
1225
+ * - `PGSERVICEFILE`, `PGSYSCONFDIR`, `PGPASSFILE` are likewise consumed
1226
+ * by the loaders, not surfaced here.
1227
+ */
1228
+ const envConnectionDefaults = (env) => {
1229
+ const out = {};
1230
+ const get = (k) => {
1231
+ const v = env[k];
1232
+ return v !== void 0 && v !== "" ? v : void 0;
1233
+ };
1234
+ for (const [envName, field] of Object.entries(PG_ENV_FIELD_MAP)) {
1235
+ const value = get(envName);
1236
+ if (value === void 0) continue;
1237
+ applyEnvValue(out, field, value);
1238
+ }
1239
+ const timeoutRaw = get("PGCONNECT_TIMEOUT");
1240
+ if (timeoutRaw !== void 0) {
1241
+ const t = Number.parseInt(timeoutRaw, 10);
1242
+ if (Number.isFinite(t) && t >= 0) out.connectTimeoutMs = t * 1e3;
1243
+ }
1244
+ validateGssEncMode(get("PGGSSENCMODE") ?? null);
1245
+ return out;
1706
1246
  };
1707
1247
  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
- }
1248
+ switch (field) {
1249
+ case "host":
1250
+ out.host = value;
1251
+ return;
1252
+ case "port": {
1253
+ const p = Number.parseInt(value, 10);
1254
+ if (Number.isFinite(p) && p > 0 && p <= 65535) out.port = p;
1255
+ return;
1256
+ }
1257
+ case "user":
1258
+ out.user = value;
1259
+ return;
1260
+ case "database":
1261
+ out.database = value;
1262
+ return;
1263
+ case "password":
1264
+ out.password = value;
1265
+ return;
1266
+ case "applicationName":
1267
+ out.applicationName = value;
1268
+ return;
1269
+ case "options":
1270
+ out.options = value;
1271
+ return;
1272
+ case "clientEncoding":
1273
+ out.clientEncoding = value;
1274
+ return;
1275
+ case "ssl":
1276
+ out.ssl = normalizeSslMode(value);
1277
+ return;
1278
+ case "sslrootcert":
1279
+ out.sslrootcert = value;
1280
+ return;
1281
+ case "sslcert":
1282
+ out.sslcert = value;
1283
+ return;
1284
+ case "sslkey":
1285
+ out.sslkey = value;
1286
+ return;
1287
+ case "sslcertmode": {
1288
+ const cm = normalizeSslCertMode(value);
1289
+ if (cm !== void 0) out.sslcertmode = cm;
1290
+ return;
1291
+ }
1292
+ case "sslnegotiation": {
1293
+ const sn = normalizeSslNegotiation(value);
1294
+ if (sn !== void 0) out.sslnegotiation = sn;
1295
+ return;
1296
+ }
1297
+ case "sslcrl":
1298
+ out.sslcrl = value;
1299
+ return;
1300
+ case "sslcrldir":
1301
+ out.sslcrldir = value;
1302
+ return;
1303
+ case "sslkeylogfile":
1304
+ out.sslkeylogfile = value;
1305
+ return;
1306
+ case "hostaddr":
1307
+ out.hostaddr = value;
1308
+ return;
1309
+ case "channelBinding": {
1310
+ const cb = normalizeChannelBinding(value);
1311
+ if (cb !== void 0) out.channelBinding = cb;
1312
+ return;
1313
+ }
1314
+ default: return;
1315
+ }
1783
1316
  };
1784
1317
  /**
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),
1318
+ * libpq compiled-in defaults. The lowest-priority layer in the merge chain.
1319
+ *
1320
+ * - host: 'localhost'
1321
+ * - port: 5432
1322
+ * - user: $USER ?? '' (the wire layer surfaces a clear error if the user
1323
+ * is still empty at connect time)
1324
+ * - database: deferred — libpq defaults dbname to the user; we wire that
1325
+ * in `mergeConnectOptions` after layering so a `PGUSER` env can flow
1326
+ * into `database` when the user didn't specify one.
1327
+ * - ssl: 'prefer'
1328
+ * - applicationName: 'psql' — matches upstream so `pg_stat_activity` shows
1329
+ * the value users expect.
1330
+ * - sslcert / sslkey: libpq auto-loads the default client cert/key at
1331
+ * `~/.postgresql/postgresql.crt` / `.key` when neither is configured AND
1332
+ * the file exists. We seed these as the lowest-priority defaults via
1333
+ * {@link defaultClientCertDefaults}; any explicit URI / env / conninfo
1334
+ * value overrides them. A non-existent default file is simply not set
1335
+ * (no error), matching libpq — only an explicit path that's missing
1336
+ * surfaces an error (at TLS-load time, in the wire layer).
1337
+ */
1338
+ const libpqConnectionDefaults = (env) => ({
1339
+ host: "localhost",
1340
+ port: 5432,
1341
+ user: env.USER ?? "",
1342
+ database: "",
1343
+ ssl: "prefer",
1344
+ applicationName: "psql",
1345
+ ...defaultClientCertDefaults(env)
1813
1346
  });
1814
1347
  /**
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;
1348
+ * libpq default client-certificate discovery. When the user has NOT set
1349
+ * `sslcert` / `sslkey` (explicit paths and `PGSSLCERT` / `PGSSLKEY` are
1350
+ * higher-priority layers), libpq falls back to `~/.postgresql/postgresql.crt`
1351
+ * and `~/.postgresql/postgresql.key` — but only if those files actually
1352
+ * exist. We mirror that here so a present default cert satisfies e.g.
1353
+ * `sslcertmode=require`.
1354
+ *
1355
+ * The home directory is taken from `env.HOME` (falling back to
1356
+ * `os.homedir()`), the same convention as the pgpass / pgservice loaders;
1357
+ * passing a synthetic `HOME` keeps this hermetic in tests.
1358
+ *
1359
+ * Exported for unit testing.
1360
+ */
1361
+ const defaultClientCertDefaults = (env) => {
1362
+ const home = env.HOME ?? os.homedir();
1363
+ if (home === void 0 || home === "") return {};
1364
+ const out = {};
1365
+ const certPath = path.join(home, ".postgresql", "postgresql.crt");
1366
+ if (existsSync(certPath)) out.sslcert = certPath;
1367
+ const keyPath = path.join(home, ".postgresql", "postgresql.key");
1368
+ if (existsSync(keyPath)) out.sslkey = keyPath;
1369
+ return out;
1840
1370
  };
1841
1371
  /**
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;
1372
+ * Translate a `pg_service.conf` entry into a `Partial<ConnectOptions>`.
1373
+ * Unknown keys are silently dropped — the service file format admits
1374
+ * arbitrary keys but only the libpq-spec subset maps to ConnectOptions.
1375
+ *
1376
+ * Numeric / enum validation mirrors `parseConninfo` so an out-of-range
1377
+ * port or bogus sslmode in the service file fails the same way (silently
1378
+ * dropped here, since libpq itself only warns on invalid service values).
1379
+ */
1380
+ const serviceEntryToConnectOptions = (entry) => {
1381
+ const out = {};
1382
+ for (const [k, v] of Object.entries(entry)) switch (k.toLowerCase()) {
1383
+ case "host":
1384
+ if (v !== "") out.host = v;
1385
+ break;
1386
+ case "port": {
1387
+ const p = Number.parseInt(v, 10);
1388
+ if (Number.isFinite(p) && p > 0 && p <= 65535) out.port = p;
1389
+ break;
1390
+ }
1391
+ case "user":
1392
+ if (v !== "") out.user = v;
1393
+ break;
1394
+ case "dbname":
1395
+ if (v !== "") out.database = v;
1396
+ break;
1397
+ case "password":
1398
+ out.password = v;
1399
+ break;
1400
+ case "application_name":
1401
+ if (v !== "") out.applicationName = v;
1402
+ break;
1403
+ case "sslmode":
1404
+ if (v !== "") out.ssl = normalizeSslMode(v);
1405
+ break;
1406
+ case "channel_binding": {
1407
+ const cb = normalizeChannelBinding(v);
1408
+ if (cb !== void 0) out.channelBinding = cb;
1409
+ break;
1410
+ }
1411
+ case "require_auth": {
1412
+ const ra = normalizeRequireAuth(v);
1413
+ if (ra !== void 0) out.requireAuth = ra;
1414
+ break;
1415
+ }
1416
+ case "options":
1417
+ if (v !== "") out.options = v;
1418
+ break;
1419
+ case "client_encoding":
1420
+ if (v !== "") out.clientEncoding = v;
1421
+ break;
1422
+ case "sslcert":
1423
+ if (v !== "") out.sslcert = v;
1424
+ break;
1425
+ case "sslkey":
1426
+ if (v !== "") out.sslkey = v;
1427
+ break;
1428
+ case "sslcertmode": {
1429
+ const cm = normalizeSslCertMode(v);
1430
+ if (cm !== void 0) out.sslcertmode = cm;
1431
+ break;
1432
+ }
1433
+ case "sslnegotiation": {
1434
+ const sn = normalizeSslNegotiation(v);
1435
+ if (sn !== void 0) out.sslnegotiation = sn;
1436
+ break;
1437
+ }
1438
+ case "sslrootcert":
1439
+ if (v !== "") out.sslrootcert = v;
1440
+ break;
1441
+ case "sslcrl":
1442
+ if (v !== "") out.sslcrl = v;
1443
+ break;
1444
+ case "sslcrldir":
1445
+ if (v !== "") out.sslcrldir = v;
1446
+ break;
1447
+ case "sslkeylogfile":
1448
+ if (v !== "") out.sslkeylogfile = v;
1449
+ break;
1450
+ case "sslsni": {
1451
+ const b = parseLibpqBool(v);
1452
+ if (b !== void 0) out.sslsni = b;
1453
+ break;
1454
+ }
1455
+ case "keepalives": {
1456
+ const b = parseLibpqBool(v);
1457
+ if (b !== void 0) out.keepalives = b;
1458
+ break;
1459
+ }
1460
+ case "keepalives_idle": {
1461
+ const n = parseKeepalivesIdle(v);
1462
+ if (n !== void 0) out.keepalivesIdle = n;
1463
+ break;
1464
+ }
1465
+ case "requirepeer":
1466
+ if (v !== "") out.requirepeer = v;
1467
+ break;
1468
+ case "hostaddr":
1469
+ if (v !== "") out.hostaddr = v;
1470
+ break;
1471
+ case "ssl_min_protocol_version": {
1472
+ const pv = normalizeTlsProtocolVersion(v === "" ? void 0 : v, "ssl_min_protocol_version");
1473
+ if (pv !== void 0) out.sslMinProtocolVersion = pv;
1474
+ break;
1475
+ }
1476
+ case "ssl_max_protocol_version": {
1477
+ const pv = normalizeTlsProtocolVersion(v === "" ? void 0 : v, "ssl_max_protocol_version");
1478
+ if (pv !== void 0) out.sslMaxProtocolVersion = pv;
1479
+ break;
1480
+ }
1481
+ case "connect_timeout": {
1482
+ const t = Number.parseInt(v, 10);
1483
+ if (Number.isFinite(t) && t >= 0) out.connectTimeoutMs = t * 1e3;
1484
+ break;
1485
+ }
1486
+ }
1487
+ assertTlsProtocolRange(out.sslMinProtocolVersion, out.sslMaxProtocolVersion);
1488
+ assertTlsMaxProtocolSupported(out.sslMaxProtocolVersion);
1489
+ return out;
1993
1490
  };
1994
1491
  /**
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;
1492
+ * Merge layered partial ConnectOptions into a complete ConnectOptions.
1493
+ *
1494
+ * Layers are listed in PRIORITY order (highest first). For each output
1495
+ * field, the first layer that supplies a value wins. The implementation
1496
+ * walks the layers in reverse so the spread-into semantics match.
1497
+ *
1498
+ * `database` has a libpq-specific fallback: if every layer omits it, the
1499
+ * default is the resolved `user` (so `psql -U alice` connects to a
1500
+ * database named `alice`). We apply this AFTER all layers have run.
1501
+ */
1502
+ const mergeConnectOptions = (layers, defaults) => {
1503
+ let out = { ...defaults };
1504
+ for (let i = layers.length - 1; i >= 0; i--) {
1505
+ const layer = layers[i];
1506
+ out = {
1507
+ ...out,
1508
+ ...layer
1509
+ };
1510
+ }
1511
+ if (out.database === "") out.database = out.user;
1512
+ if (out.sslrootcert === "system" && out.ssl !== "verify-full") out.ssl = "verify-full";
1513
+ assertSslNegotiationModeCompatible(out.ssl, out.sslnegotiation);
1514
+ return out;
2030
1515
  };
1516
+ //#endregion
1517
+ export { defaultClientCertDefaults, envConnectionDefaults, libpqConnectionDefaults, looksLikeConnectionString, mergeConnectOptions, parseConnectionUri, parseConnectionUriPartial, parseConnectionUriService, parseConninfo, runPsql, serviceEntryToConnectOptions };