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