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