neonctl 2.22.2 → 2.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/esbuild.js +147 -0
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection backslash commands.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of the corresponding `exec_command_*` functions in
|
|
5
|
+
* upstream PostgreSQL's `src/bin/psql/command.c`:
|
|
6
|
+
*
|
|
7
|
+
* - `\c` / `\connect` → exec_command_connect / do_connect
|
|
8
|
+
* - `\conninfo` → exec_command_conninfo
|
|
9
|
+
* - `\encoding` → exec_command_encoding
|
|
10
|
+
* - `\password` → exec_command_password
|
|
11
|
+
*
|
|
12
|
+
* What this module owns:
|
|
13
|
+
*
|
|
14
|
+
* - Parsing the three `\c` argument shapes — positional (`db user host
|
|
15
|
+
* port`, with `-` meaning "keep current"), URI (`postgresql://…`), and
|
|
16
|
+
* conninfo key=value pairs (`dbname=foo host=bar`).
|
|
17
|
+
* - Building a fresh {@link ConnectOptions} by merging the override with
|
|
18
|
+
* the previous connection's opts and dispatching `PgConnection.connect`
|
|
19
|
+
* via an injectable seam so tests can stub the wire layer.
|
|
20
|
+
* - `\conninfo` — renders the PostgreSQL 18 "Connection Information"
|
|
21
|
+
* two-column table (Parameter / Value), mirroring upstream
|
|
22
|
+
* `exec_command_conninfo` in `src/bin/psql/command.c`. PG 18 rewrote
|
|
23
|
+
* `\conninfo` from the old one-line "You are connected to …" message
|
|
24
|
+
* into this table. Every value comes from the live connection's
|
|
25
|
+
* accessors (`getConnectionInfo()`, `getTlsInfo()`,
|
|
26
|
+
* `parameterStatus()`) — we issue NO SQL (the old form's
|
|
27
|
+
* `inet_server_addr()` returned a bogus internal IP behind a proxy like
|
|
28
|
+
* Neon). The ResultSet is handed to the active `\pset format` printer
|
|
29
|
+
* so `\conninfo` honours `-A`, `-H`, etc. just like a query result.
|
|
30
|
+
* - `\encoding` — like cmd_format.ts but additionally issues
|
|
31
|
+
* `SET client_encoding TO …` on the live connection so the backend
|
|
32
|
+
* ParameterStatus stays in sync with `settings.popt.topt.encoding`.
|
|
33
|
+
* - `\password` — prompts for a password (twice), encodes it as a
|
|
34
|
+
* SCRAM-SHA-256 verifier (RFC 5803 / PG's `ALTER USER … PASSWORD`
|
|
35
|
+
* format), and issues the ALTER USER on the live connection. The
|
|
36
|
+
* encoder lives in {@link scramSha256Verifier}; tests call it directly
|
|
37
|
+
* with a fixed salt so we don't have to mock crypto.randomBytes.
|
|
38
|
+
* Refuses to prompt when `settings.notty` is true (matches the spirit
|
|
39
|
+
* of upstream — we don't yet wire `/dev/tty`, so notty would otherwise
|
|
40
|
+
* fight the mainloop's stdin reader).
|
|
41
|
+
*
|
|
42
|
+
* What this module does NOT own:
|
|
43
|
+
*
|
|
44
|
+
* - Cataloguing the current database/user. These are populated by the
|
|
45
|
+
* startup WP into psql vars DBNAME/USER (and kept in sync by `\c`), so
|
|
46
|
+
* `\conninfo` reads them out of `settings.vars`, falling back to the
|
|
47
|
+
* connect-opts surfaced on the live connection. Host/port/hostaddr and
|
|
48
|
+
* the SSL facts come straight from the connection accessors.
|
|
49
|
+
*
|
|
50
|
+
* Password retention: `PgConnection` exposes the password captured at
|
|
51
|
+
* connect time via a read-only `password` getter (mirroring libpq's
|
|
52
|
+
* retention on the `PGconn`). `\c` reads it via a structural cast so we
|
|
53
|
+
* don't have to widen the frozen {@link Connection} interface, and feeds
|
|
54
|
+
* it to {@link mergeConnectOpts} so a reconnect to a different database
|
|
55
|
+
* works without re-prompting. A new password supplied in the conninfo /
|
|
56
|
+
* URI override always wins.
|
|
57
|
+
*/
|
|
58
|
+
import { Buffer } from 'node:buffer';
|
|
59
|
+
import { createHash, createHmac, pbkdf2Sync, randomBytes as nodeRandomBytes, } from 'node:crypto';
|
|
60
|
+
import { readLine as readInputLine } from '../io/input.js';
|
|
61
|
+
import { syncConnectionVars } from '../core/syncVars.js';
|
|
62
|
+
import { PgConnection } from '../wire/connection.js';
|
|
63
|
+
import { alignedPrinter } from '../print/aligned.js';
|
|
64
|
+
import { asciidocPrinter } from '../print/asciidoc.js';
|
|
65
|
+
import { csvPrinter } from '../print/csv.js';
|
|
66
|
+
import { htmlPrinter } from '../print/html.js';
|
|
67
|
+
import { jsonPrinter } from '../print/json.js';
|
|
68
|
+
import { latexLongtablePrinter, latexPrinter } from '../print/latex.js';
|
|
69
|
+
import { troffMsPrinter } from '../print/troff.js';
|
|
70
|
+
import { unalignedPrinter } from '../print/unaligned.js';
|
|
71
|
+
import { writeErr, writeOut } from './shared.js';
|
|
72
|
+
const defaultDeps = {
|
|
73
|
+
connect: (opts) => PgConnection.connect(opts),
|
|
74
|
+
readLine: defaultReadLine,
|
|
75
|
+
randomBytes: nodeRandomBytes,
|
|
76
|
+
};
|
|
77
|
+
let currentDeps = defaultDeps;
|
|
78
|
+
/** Override the module's runtime deps. Returns a restore function. */
|
|
79
|
+
export const setCmdConnectDeps = (overrides) => {
|
|
80
|
+
const prev = currentDeps;
|
|
81
|
+
currentDeps = { ...prev, ...overrides };
|
|
82
|
+
return () => {
|
|
83
|
+
currentDeps = prev;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const readConnectionPassword = (conn) => {
|
|
87
|
+
if (!conn)
|
|
88
|
+
return null;
|
|
89
|
+
const raw = conn.password;
|
|
90
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : null;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Read the live connection's EFFECTIVE {@link ConnectOptions} (the full set
|
|
94
|
+
* it was dialled with — sslmode, sslrootcert/cert/key/crl, sslnegotiation,
|
|
95
|
+
* channelBinding, requireAuth, hostaddr, …). The {@link Connection} interface
|
|
96
|
+
* deliberately hides these, but {@link PgConnection} keeps them on a (TS-)
|
|
97
|
+
* private `opts` field; we read it structurally, the same pattern as
|
|
98
|
+
* {@link readConnectionPassword}. Used to SEED a `\c` reconnect so it doesn't
|
|
99
|
+
* silently drop TLS/cert options or downgrade sslmode.
|
|
100
|
+
* Returns `null` for a mock/absent connection.
|
|
101
|
+
*/
|
|
102
|
+
const readConnectionOptions = (conn) => {
|
|
103
|
+
if (!conn)
|
|
104
|
+
return null;
|
|
105
|
+
const raw = conn.opts;
|
|
106
|
+
return raw !== undefined && raw !== null && typeof raw === 'object'
|
|
107
|
+
? raw
|
|
108
|
+
: null;
|
|
109
|
+
};
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// \c / \connect
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
const KEEP = '-';
|
|
114
|
+
/**
|
|
115
|
+
* Parse the argument tail of `\c` into a partial override of
|
|
116
|
+
* {@link ConnectOptions}. Three forms:
|
|
117
|
+
*
|
|
118
|
+
* 1. **URI**: starts with `postgresql://` or `postgres://`.
|
|
119
|
+
* 2. **conninfo**: contains `=` in the first token — treated as a space
|
|
120
|
+
* separated `key=value` sequence (libpq's syntax; we don't implement
|
|
121
|
+
* quoted values because psql itself only forwards what the user typed
|
|
122
|
+
* to libpq's `PQconninfoParse`).
|
|
123
|
+
* 3. **positional**: `[db [user [host [port]]]]`. Each can be `-` to keep
|
|
124
|
+
* the current value.
|
|
125
|
+
*
|
|
126
|
+
* Returns an object with only the keys the user supplied. The caller is
|
|
127
|
+
* responsible for merging with the previous opts.
|
|
128
|
+
*/
|
|
129
|
+
export const parseConnectArgs = (rawArgs) => {
|
|
130
|
+
const trimmed = rawArgs.trim();
|
|
131
|
+
if (trimmed.length === 0)
|
|
132
|
+
return {};
|
|
133
|
+
// URI form.
|
|
134
|
+
if (trimmed.startsWith('postgresql://') ||
|
|
135
|
+
trimmed.startsWith('postgres://')) {
|
|
136
|
+
return parseUri(trimmed);
|
|
137
|
+
}
|
|
138
|
+
// conninfo form — at least one token contains `=` (and that `=` is not
|
|
139
|
+
// inside a quoted SQL value).
|
|
140
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed) ||
|
|
141
|
+
/\s[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
|
|
142
|
+
return parseConninfo(trimmed);
|
|
143
|
+
}
|
|
144
|
+
// Positional. Tokenise by whitespace; `-` is a sentinel.
|
|
145
|
+
const tokens = trimmed.split(/\s+/);
|
|
146
|
+
const out = {};
|
|
147
|
+
if (tokens.length >= 1 && tokens[0] !== KEEP)
|
|
148
|
+
out.database = tokens[0];
|
|
149
|
+
if (tokens.length >= 2 && tokens[1] !== KEEP)
|
|
150
|
+
out.user = tokens[1];
|
|
151
|
+
if (tokens.length >= 3 && tokens[2] !== KEEP)
|
|
152
|
+
out.host = tokens[2];
|
|
153
|
+
if (tokens.length >= 4 && tokens[3] !== KEEP) {
|
|
154
|
+
const port = parseInt(tokens[3], 10);
|
|
155
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
156
|
+
return { error: `invalid port number "${tokens[3]}"` };
|
|
157
|
+
}
|
|
158
|
+
out.port = port;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Apply a single libpq-style connection keyword to `out`, validating values
|
|
164
|
+
* where psql does. `key` must already be lowercased. Returns `{ error }` on a
|
|
165
|
+
* bad value, or `null` on success. Shared by the `key=value` conninfo parser
|
|
166
|
+
* and the `\c <uri>` query-string parser so both honour the same keywords.
|
|
167
|
+
*/
|
|
168
|
+
const applyConnInfoKey = (out, key, value) => {
|
|
169
|
+
switch (key) {
|
|
170
|
+
case 'host':
|
|
171
|
+
out.host = value;
|
|
172
|
+
break;
|
|
173
|
+
case 'hostaddr':
|
|
174
|
+
// Distinct from `host`: hostaddr is the literal IP to dial, while the
|
|
175
|
+
// cert/SNI is still verified against `host`.
|
|
176
|
+
out.hostaddr = value;
|
|
177
|
+
break;
|
|
178
|
+
case 'port': {
|
|
179
|
+
const port = parseInt(value, 10);
|
|
180
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
181
|
+
return { error: `invalid port "${value}"` };
|
|
182
|
+
}
|
|
183
|
+
out.port = port;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'user':
|
|
187
|
+
out.user = value;
|
|
188
|
+
break;
|
|
189
|
+
case 'password':
|
|
190
|
+
out.password = value;
|
|
191
|
+
break;
|
|
192
|
+
case 'dbname':
|
|
193
|
+
case 'database':
|
|
194
|
+
out.database = value;
|
|
195
|
+
break;
|
|
196
|
+
case 'application_name':
|
|
197
|
+
out.applicationName = value;
|
|
198
|
+
break;
|
|
199
|
+
case 'sslmode': {
|
|
200
|
+
const allowed = [
|
|
201
|
+
'disable',
|
|
202
|
+
'allow',
|
|
203
|
+
'prefer',
|
|
204
|
+
'require',
|
|
205
|
+
'verify-ca',
|
|
206
|
+
'verify-full',
|
|
207
|
+
];
|
|
208
|
+
if (!allowed.includes(value)) {
|
|
209
|
+
return { error: `invalid sslmode "${value}"` };
|
|
210
|
+
}
|
|
211
|
+
out.ssl = value;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case 'channel_binding': {
|
|
215
|
+
const allowed = [
|
|
216
|
+
'disable',
|
|
217
|
+
'prefer',
|
|
218
|
+
'require',
|
|
219
|
+
];
|
|
220
|
+
if (!allowed.includes(value)) {
|
|
221
|
+
return { error: `invalid channel_binding "${value}"` };
|
|
222
|
+
}
|
|
223
|
+
out.channelBinding = value;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case 'client_encoding':
|
|
227
|
+
out.clientEncoding = value;
|
|
228
|
+
break;
|
|
229
|
+
case 'connect_timeout': {
|
|
230
|
+
const n = parseInt(value, 10);
|
|
231
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
232
|
+
return { error: `invalid connect_timeout "${value}"` };
|
|
233
|
+
}
|
|
234
|
+
out.connectTimeoutMs = n * 1000;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'options':
|
|
238
|
+
out.options = value;
|
|
239
|
+
break;
|
|
240
|
+
default:
|
|
241
|
+
// Unknown keys are silently ignored — matches libpq's permissiveness
|
|
242
|
+
// for forward-compat with future PG releases. We could warn here,
|
|
243
|
+
// but psql itself doesn't.
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
};
|
|
248
|
+
const parseUri = (raw) => {
|
|
249
|
+
let url;
|
|
250
|
+
try {
|
|
251
|
+
// Node's URL parser accepts `postgresql://` schemes.
|
|
252
|
+
url = new URL(raw);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256
|
+
return { error: `invalid URI: ${msg}` };
|
|
257
|
+
}
|
|
258
|
+
const out = {};
|
|
259
|
+
// decodeURIComponent throws URIError on malformed `%` escapes (e.g. `%zz`);
|
|
260
|
+
// wrap every decode so a bad URI surfaces as a clean error rather than an
|
|
261
|
+
// unhandled exception that aborts the REPL.
|
|
262
|
+
try {
|
|
263
|
+
if (url.hostname.length > 0)
|
|
264
|
+
out.host = decodeURIComponent(url.hostname);
|
|
265
|
+
if (url.port.length > 0) {
|
|
266
|
+
const port = parseInt(url.port, 10);
|
|
267
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
268
|
+
return { error: `invalid port in URI` };
|
|
269
|
+
}
|
|
270
|
+
out.port = port;
|
|
271
|
+
}
|
|
272
|
+
if (url.username.length > 0)
|
|
273
|
+
out.user = decodeURIComponent(url.username);
|
|
274
|
+
if (url.password.length > 0) {
|
|
275
|
+
out.password = decodeURIComponent(url.password);
|
|
276
|
+
}
|
|
277
|
+
const pathname = url.pathname.replace(/^\//, '');
|
|
278
|
+
if (pathname.length > 0)
|
|
279
|
+
out.database = decodeURIComponent(pathname);
|
|
280
|
+
// Map the query string (?sslmode=require&connect_timeout=10&…) onto the
|
|
281
|
+
// same connection keywords libpq accepts in a URI.
|
|
282
|
+
for (const [rawKey, rawVal] of url.searchParams) {
|
|
283
|
+
const res = applyConnInfoKey(out, rawKey.toLowerCase(), rawVal);
|
|
284
|
+
if (res !== null)
|
|
285
|
+
return res;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
290
|
+
return { error: `invalid URI: ${msg}` };
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
const parseConninfo = (raw) => {
|
|
295
|
+
const out = {};
|
|
296
|
+
// Simple key=value tokenizer — splits on whitespace not inside single
|
|
297
|
+
// quotes. libpq supports backslash-escapes inside quotes; we accept them
|
|
298
|
+
// verbatim for now (none of the keys we care about typically need them).
|
|
299
|
+
const pairs = [];
|
|
300
|
+
let current = '';
|
|
301
|
+
let inQuote = false;
|
|
302
|
+
for (let i = 0; i < raw.length; i++) {
|
|
303
|
+
const c = raw[i];
|
|
304
|
+
if (inQuote) {
|
|
305
|
+
if (c === '\\' && i + 1 < raw.length) {
|
|
306
|
+
current += raw[i + 1];
|
|
307
|
+
i++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (c === "'") {
|
|
311
|
+
inQuote = false;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
current += c;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (c === "'") {
|
|
318
|
+
inQuote = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (/\s/.test(c)) {
|
|
322
|
+
if (current.length > 0) {
|
|
323
|
+
pairs.push(current);
|
|
324
|
+
current = '';
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
current += c;
|
|
329
|
+
}
|
|
330
|
+
if (current.length > 0)
|
|
331
|
+
pairs.push(current);
|
|
332
|
+
for (const pair of pairs) {
|
|
333
|
+
const eq = pair.indexOf('=');
|
|
334
|
+
if (eq < 0) {
|
|
335
|
+
return { error: `missing "=" after "${pair}" in connection info` };
|
|
336
|
+
}
|
|
337
|
+
const key = pair.slice(0, eq).toLowerCase();
|
|
338
|
+
const value = pair.slice(eq + 1);
|
|
339
|
+
const res = applyConnInfoKey(out, key, value);
|
|
340
|
+
if (res !== null)
|
|
341
|
+
return res;
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Build a full {@link ConnectOptions} for the new connection by merging
|
|
347
|
+
* `override` over the "previous" connection state. The previous values are
|
|
348
|
+
* sourced from `settings.vars` (HOST/PORT/USER/DBNAME/PASSWORD) which the
|
|
349
|
+
* startup WP populates; for fields not represented there (sslmode, etc.)
|
|
350
|
+
* we use safe defaults.
|
|
351
|
+
*
|
|
352
|
+
* For passwords specifically the precedence is:
|
|
353
|
+
* 1. `override.password` — anything the user typed in this `\c` invocation
|
|
354
|
+
* (URI password or `password=` conninfo key) wins.
|
|
355
|
+
* 2. `previousPassword` — the password captured on the live
|
|
356
|
+
* {@link PgConnection}, mirroring libpq's behaviour of retaining the
|
|
357
|
+
* credential on the `PGconn` so reconnects work transparently.
|
|
358
|
+
* 3. `PASSWORD` psql var — set by `-W` / `PGPASSWORD` at startup.
|
|
359
|
+
*
|
|
360
|
+
* Returns `null` if the merge can't produce a usable opts (e.g. no database
|
|
361
|
+
* and no current connection).
|
|
362
|
+
*/
|
|
363
|
+
export const mergeConnectOpts = (settings, override, prior = null) => {
|
|
364
|
+
const vars = settings.vars;
|
|
365
|
+
// Overlay only the keys the user actually supplied (drop `undefined`s so a
|
|
366
|
+
// spread doesn't clobber a seeded value with `undefined`).
|
|
367
|
+
const ov = {};
|
|
368
|
+
for (const [k, v] of Object.entries(override)) {
|
|
369
|
+
if (v !== undefined)
|
|
370
|
+
ov[k] = v;
|
|
371
|
+
}
|
|
372
|
+
// libpq's do_connect clones the prior connection's full conninfo and
|
|
373
|
+
// overrides only user-specified keys — so a reconnect keeps sslmode,
|
|
374
|
+
// sslrootcert/cert/key/crl, sslnegotiation, channelBinding, requireAuth,
|
|
375
|
+
// hostaddr, etc. Seed from the live connection's effective options; fall
|
|
376
|
+
// back to {} when there is none.
|
|
377
|
+
const seed = prior ? { ...prior } : {};
|
|
378
|
+
// libpq clears keep_password when user, host, OR port changes, so a stored
|
|
379
|
+
// credential isn't transmitted to a different principal/server. Compare the
|
|
380
|
+
// override against the prior live target.
|
|
381
|
+
const hostChanged = ov.host !== undefined && ov.host !== prior?.host;
|
|
382
|
+
const targetChanged = hostChanged ||
|
|
383
|
+
(ov.port !== undefined && ov.port !== prior?.port) ||
|
|
384
|
+
(ov.user !== undefined && ov.user !== prior?.user);
|
|
385
|
+
if (targetChanged)
|
|
386
|
+
delete seed.password;
|
|
387
|
+
// A new host invalidates the prior hostaddr (it was the old host's IP).
|
|
388
|
+
if (hostChanged)
|
|
389
|
+
delete seed.hostaddr;
|
|
390
|
+
const merged = { ...seed, ...ov };
|
|
391
|
+
// Fill the required fields from vars / env / defaults when neither the
|
|
392
|
+
// override nor the prior connection supplied them.
|
|
393
|
+
const host = merged.host ?? vars.get('HOST') ?? 'localhost';
|
|
394
|
+
const portStr = vars.get('PORT');
|
|
395
|
+
const port = merged.port ?? (portStr !== undefined ? parseInt(portStr, 10) : 5432);
|
|
396
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
397
|
+
return { error: `invalid port number` };
|
|
398
|
+
}
|
|
399
|
+
const user = merged.user ??
|
|
400
|
+
vars.get('USER') ??
|
|
401
|
+
process.env.USER ??
|
|
402
|
+
process.env.USERNAME ??
|
|
403
|
+
'';
|
|
404
|
+
if (user.length === 0) {
|
|
405
|
+
return { error: 'no user name specified' };
|
|
406
|
+
}
|
|
407
|
+
const database = merged.database ?? vars.get('DBNAME') ?? user;
|
|
408
|
+
const password = merged.password ?? vars.get('PASSWORD');
|
|
409
|
+
const ssl = merged.ssl ?? 'prefer';
|
|
410
|
+
return {
|
|
411
|
+
...merged,
|
|
412
|
+
host,
|
|
413
|
+
port,
|
|
414
|
+
user,
|
|
415
|
+
password: password ?? undefined,
|
|
416
|
+
database,
|
|
417
|
+
ssl,
|
|
418
|
+
applicationName: merged.applicationName ?? vars.get('APPLICATION_NAME'),
|
|
419
|
+
clientEncoding: merged.clientEncoding ?? settings.popt.topt.encoding,
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
/** `\c` / `\connect` — reconnect, possibly to a different database/user/host. */
|
|
423
|
+
export const cmdConnect = {
|
|
424
|
+
name: 'c',
|
|
425
|
+
aliases: ['connect'],
|
|
426
|
+
argMode: 'whole-line',
|
|
427
|
+
helpKey: 'c',
|
|
428
|
+
run: async (ctx) => {
|
|
429
|
+
const rawArgs = ctx.restOfLine();
|
|
430
|
+
if (rawArgs.trim().length === 0) {
|
|
431
|
+
// No args → print conninfo, matching upstream `do_connect` short
|
|
432
|
+
// circuit.
|
|
433
|
+
return runConninfo(ctx);
|
|
434
|
+
}
|
|
435
|
+
const parsed = parseConnectArgs(rawArgs);
|
|
436
|
+
if ('error' in parsed) {
|
|
437
|
+
writeErr(`\\${ctx.cmdName}: ${parsed.error}\n`);
|
|
438
|
+
return { status: 'error' };
|
|
439
|
+
}
|
|
440
|
+
// Seed the reconnect from the live connection's EFFECTIVE options so TLS /
|
|
441
|
+
// cert / auth settings survive and the prior password is only
|
|
442
|
+
// reused when the principal/server is unchanged. Both are read
|
|
443
|
+
// structurally from PgConnection (the frozen Connection interface hides
|
|
444
|
+
// them). readConnectionPassword stays as the fallback for mocks/drivers
|
|
445
|
+
// that expose `password` but not `opts`.
|
|
446
|
+
const priorOpts = readConnectionOptions(ctx.settings.db);
|
|
447
|
+
const priorPw = readConnectionPassword(ctx.settings.db);
|
|
448
|
+
const prior = priorOpts ?? (priorPw !== null ? { password: priorPw } : null);
|
|
449
|
+
const newOpts = mergeConnectOpts(ctx.settings, parsed, prior);
|
|
450
|
+
if ('error' in newOpts) {
|
|
451
|
+
writeErr(`\\${ctx.cmdName}: ${newOpts.error}\n`);
|
|
452
|
+
return { status: 'error' };
|
|
453
|
+
}
|
|
454
|
+
let next;
|
|
455
|
+
try {
|
|
456
|
+
next = await currentDeps.connect(newOpts);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
// psql keeps the old connection on failure.
|
|
460
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
461
|
+
writeErr(`\\${ctx.cmdName}: connection failed: ${msg}\n`);
|
|
462
|
+
return { status: 'error' };
|
|
463
|
+
}
|
|
464
|
+
const old = ctx.settings.db;
|
|
465
|
+
ctx.settings.db = next;
|
|
466
|
+
// Mirror upstream do_connect → SyncVariables(): refresh the
|
|
467
|
+
// connection-driven psql vars (DBNAME/USER/HOST/PORT/ENCODING/
|
|
468
|
+
// SERVER_VERSION_*) from the new live connection so subsequent
|
|
469
|
+
// `:DBNAME`/`:USER`/etc. interpolations reflect the reconnect target.
|
|
470
|
+
syncConnectionVars(ctx.settings.vars, next);
|
|
471
|
+
if (old && !old.isClosed()) {
|
|
472
|
+
try {
|
|
473
|
+
await old.close();
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Old connection may already be in a weird state; we've moved on.
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
writeOut(`You are now connected to database "${newOpts.database}" as user "${newOpts.user}".\n`);
|
|
480
|
+
return { status: 'ok' };
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// \conninfo
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
/**
|
|
487
|
+
* Pick the printer for the active output format. Mirrors the private
|
|
488
|
+
* `pickPrinter` in `core/common.ts` / `pickActivePrinter` in `cmd_io.ts`
|
|
489
|
+
* — replicated here (those are module-private) to avoid an import cycle,
|
|
490
|
+
* the same established pattern this codebase already uses for the two
|
|
491
|
+
* other copies.
|
|
492
|
+
*/
|
|
493
|
+
const pickActivePrinter = (settings) => {
|
|
494
|
+
switch (settings.popt.topt.format) {
|
|
495
|
+
case 'aligned':
|
|
496
|
+
case 'wrapped':
|
|
497
|
+
return alignedPrinter;
|
|
498
|
+
case 'unaligned':
|
|
499
|
+
return unalignedPrinter;
|
|
500
|
+
case 'csv':
|
|
501
|
+
return csvPrinter;
|
|
502
|
+
case 'json':
|
|
503
|
+
return jsonPrinter;
|
|
504
|
+
case 'html':
|
|
505
|
+
return htmlPrinter;
|
|
506
|
+
case 'asciidoc':
|
|
507
|
+
return asciidocPrinter;
|
|
508
|
+
case 'latex':
|
|
509
|
+
return latexPrinter;
|
|
510
|
+
case 'latex-longtable':
|
|
511
|
+
return latexLongtablePrinter;
|
|
512
|
+
case 'troff-ms':
|
|
513
|
+
return troffMsPrinter;
|
|
514
|
+
default:
|
|
515
|
+
return alignedPrinter;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
/** A text-typed field descriptor for the synthetic conninfo ResultSet. */
|
|
519
|
+
const textField = (name) => ({
|
|
520
|
+
name,
|
|
521
|
+
tableID: 0,
|
|
522
|
+
columnID: 0,
|
|
523
|
+
// OID 25 = `text`: left-aligned, like both columns of upstream's output.
|
|
524
|
+
dataTypeID: 25,
|
|
525
|
+
dataTypeSize: -1,
|
|
526
|
+
dataTypeModifier: -1,
|
|
527
|
+
format: 0,
|
|
528
|
+
});
|
|
529
|
+
/**
|
|
530
|
+
* Build the (Parameter, Value) rows for the PG18 connection-information
|
|
531
|
+
* table. Mirrors the row order and gating of upstream
|
|
532
|
+
* `exec_command_conninfo` (`src/bin/psql/command.c`).
|
|
533
|
+
*/
|
|
534
|
+
const buildConninfoRows = (ctx, db) => {
|
|
535
|
+
const info = db.getConnectionInfo?.() ?? null;
|
|
536
|
+
const tls = db.getTlsInfo?.() ?? null;
|
|
537
|
+
// Database / user come from the psql vars the startup WP populates (and
|
|
538
|
+
// that `\c` keeps in sync); fall back to the connect-opts surfaced on the
|
|
539
|
+
// connection when a var is missing.
|
|
540
|
+
const database = ctx.settings.vars.get('DBNAME') ??
|
|
541
|
+
db.database ??
|
|
542
|
+
'';
|
|
543
|
+
const user = ctx.settings.vars.get('USER') ??
|
|
544
|
+
db.user ??
|
|
545
|
+
'';
|
|
546
|
+
const host = info?.host ?? ctx.settings.vars.get('HOST') ?? '';
|
|
547
|
+
const hostaddr = info?.hostaddr ?? null;
|
|
548
|
+
const port = info?.port ?? Number(ctx.settings.vars.get('PORT') ?? Number.NaN);
|
|
549
|
+
const portStr = Number.isFinite(port) ? String(port) : '';
|
|
550
|
+
const rows = [];
|
|
551
|
+
rows.push(['Database', database]);
|
|
552
|
+
rows.push(['Client User', user]);
|
|
553
|
+
// Host rows. A Unix-domain socket path (starts with '/') prints a
|
|
554
|
+
// "Socket Directory" (or "Host Address" when a hostaddr was fixed);
|
|
555
|
+
// otherwise "Host", plus a separate "Host Address" only when a distinct
|
|
556
|
+
// hostaddr is present.
|
|
557
|
+
if (host.startsWith('/')) {
|
|
558
|
+
if (hostaddr !== null && hostaddr.length > 0) {
|
|
559
|
+
rows.push(['Host Address', hostaddr]);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
rows.push(['Socket Directory', host]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
rows.push(['Host', host]);
|
|
567
|
+
if (hostaddr !== null && hostaddr.length > 0 && hostaddr !== host) {
|
|
568
|
+
rows.push(['Host Address', hostaddr]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
rows.push(['Server Port', portStr]);
|
|
572
|
+
rows.push(['Options', info?.options ?? '']);
|
|
573
|
+
rows.push(['Protocol Version', '3.0']);
|
|
574
|
+
rows.push(['Password Used', info?.passwordUsed ? 'true' : 'false']);
|
|
575
|
+
rows.push(['GSSAPI Authenticated', info?.gssapiUsed ? 'true' : 'false']);
|
|
576
|
+
rows.push(['Backend PID', String(info?.backendPid ?? 0)]);
|
|
577
|
+
rows.push(['SSL Connection', tls ? 'true' : 'false']);
|
|
578
|
+
if (tls) {
|
|
579
|
+
rows.push(['SSL Library', tls.library]);
|
|
580
|
+
rows.push(['SSL Protocol', tls.protocol]);
|
|
581
|
+
rows.push([
|
|
582
|
+
'SSL Key Bits',
|
|
583
|
+
tls.keyBits !== null ? String(tls.keyBits) : '',
|
|
584
|
+
]);
|
|
585
|
+
rows.push(['SSL Cipher', tls.cipher]);
|
|
586
|
+
rows.push([
|
|
587
|
+
'SSL Compression',
|
|
588
|
+
tls.compression !== 'off' ? 'true' : 'false',
|
|
589
|
+
]);
|
|
590
|
+
rows.push(['ALPN', tls.alpn && tls.alpn.length > 0 ? tls.alpn : 'none']);
|
|
591
|
+
}
|
|
592
|
+
rows.push([
|
|
593
|
+
'Superuser',
|
|
594
|
+
ctx.settings.db?.parameterStatus('is_superuser') ?? 'unknown',
|
|
595
|
+
]);
|
|
596
|
+
rows.push([
|
|
597
|
+
'Hot Standby',
|
|
598
|
+
ctx.settings.db?.parameterStatus('in_hot_standby') ?? 'unknown',
|
|
599
|
+
]);
|
|
600
|
+
return rows;
|
|
601
|
+
};
|
|
602
|
+
const runConninfo = async (ctx) => {
|
|
603
|
+
const db = ctx.settings.db;
|
|
604
|
+
if (!db) {
|
|
605
|
+
writeOut('You are currently not connected to a database.\n');
|
|
606
|
+
return { status: 'ok' };
|
|
607
|
+
}
|
|
608
|
+
const rows = buildConninfoRows(ctx, db);
|
|
609
|
+
const rs = {
|
|
610
|
+
command: 'SELECT',
|
|
611
|
+
rowCount: rows.length,
|
|
612
|
+
oid: null,
|
|
613
|
+
fields: [textField('Parameter'), textField('Value')],
|
|
614
|
+
rows: rows.map(([p, v]) => [p, v]),
|
|
615
|
+
notices: [],
|
|
616
|
+
};
|
|
617
|
+
// Render with the active printer (honours `\pset format`), titled
|
|
618
|
+
// "Connection Information" with the default `(N rows)` footer left on,
|
|
619
|
+
// matching PG18's `printQuery(... title = "Connection Information")`.
|
|
620
|
+
const popt = {
|
|
621
|
+
...ctx.settings.popt,
|
|
622
|
+
title: 'Connection Information',
|
|
623
|
+
topt: {
|
|
624
|
+
...ctx.settings.popt.topt,
|
|
625
|
+
title: 'Connection Information',
|
|
626
|
+
defaultFooter: true,
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
const out = process.stdout;
|
|
630
|
+
await pickActivePrinter(ctx.settings).printQuery(rs, popt, out);
|
|
631
|
+
return { status: 'ok' };
|
|
632
|
+
};
|
|
633
|
+
/** `\conninfo` — print info about the current connection. */
|
|
634
|
+
export const cmdConninfo = {
|
|
635
|
+
name: 'conninfo',
|
|
636
|
+
helpKey: 'conninfo',
|
|
637
|
+
run: (ctx) => runConninfo(ctx),
|
|
638
|
+
};
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// \encoding — overrides cmd_format.ts's version so the live connection
|
|
641
|
+
// also sees the new client_encoding (via SET client_encoding TO …).
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
/** `\encoding [NAME]` — show or set the client encoding, propagating to the connection. */
|
|
644
|
+
export const cmdEncoding = {
|
|
645
|
+
name: 'encoding',
|
|
646
|
+
helpKey: 'encoding',
|
|
647
|
+
run: async (ctx) => {
|
|
648
|
+
const arg = ctx.nextArg('normal');
|
|
649
|
+
const topt = ctx.settings.popt.topt;
|
|
650
|
+
if (arg === null) {
|
|
651
|
+
writeOut(`${topt.encoding}\n`);
|
|
652
|
+
return { status: 'ok' };
|
|
653
|
+
}
|
|
654
|
+
const db = ctx.settings.db;
|
|
655
|
+
if (db && !db.isClosed()) {
|
|
656
|
+
try {
|
|
657
|
+
// PG accepts both quoted and unquoted encoding names; we use the
|
|
658
|
+
// quoted form for safety. Encoding names are ASCII identifiers.
|
|
659
|
+
await db.execSimple(`SET client_encoding TO ${db.escapeLiteral(arg)}`);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
663
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
664
|
+
return { status: 'error' };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
topt.encoding = arg;
|
|
668
|
+
ctx.settings.vars.set('ENCODING', arg);
|
|
669
|
+
return { status: 'ok' };
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// \password
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
/**
|
|
676
|
+
* Encode a password into PostgreSQL's SCRAM-SHA-256 verifier format, the
|
|
677
|
+
* value `ALTER ROLE … PASSWORD '…'` expects when the server is configured
|
|
678
|
+
* with `password_encryption = scram-sha-256` (the default since PG 14):
|
|
679
|
+
*
|
|
680
|
+
* SCRAM-SHA-256$<iterations>:<base64 salt>$<base64 storedKey>:<base64 serverKey>
|
|
681
|
+
*
|
|
682
|
+
* Derivation follows RFC 5802 §3:
|
|
683
|
+
*
|
|
684
|
+
* SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
|
|
685
|
+
* ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
|
|
686
|
+
* StoredKey = SHA256(ClientKey)
|
|
687
|
+
* ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
|
|
688
|
+
*
|
|
689
|
+
* Sending the encoded form (rather than the plaintext password) means the
|
|
690
|
+
* server never sees the password, even briefly, and the SCRAM verifier is
|
|
691
|
+
* stored as-is in `pg_authid.rolpassword`.
|
|
692
|
+
*
|
|
693
|
+
* Iterations default to 4096 to match PG's `scram_iterations` default and
|
|
694
|
+
* libpq's `PQchangePassword`. The caller can override (lower for tests,
|
|
695
|
+
* higher for stronger hardening).
|
|
696
|
+
*/
|
|
697
|
+
export const scramSha256Verifier = (password, salt, iterations = 4096) => {
|
|
698
|
+
const saltedPassword = pbkdf2Sync(Buffer.from(password, 'utf8'), salt, iterations, 32, 'sha256');
|
|
699
|
+
const clientKey = createHmac('sha256', saltedPassword)
|
|
700
|
+
.update('Client Key')
|
|
701
|
+
.digest();
|
|
702
|
+
const storedKey = createHash('sha256').update(clientKey).digest();
|
|
703
|
+
const serverKey = createHmac('sha256', saltedPassword)
|
|
704
|
+
.update('Server Key')
|
|
705
|
+
.digest();
|
|
706
|
+
return ('SCRAM-SHA-256$' +
|
|
707
|
+
String(iterations) +
|
|
708
|
+
':' +
|
|
709
|
+
salt.toString('base64') +
|
|
710
|
+
'$' +
|
|
711
|
+
storedKey.toString('base64') +
|
|
712
|
+
':' +
|
|
713
|
+
serverKey.toString('base64'));
|
|
714
|
+
};
|
|
715
|
+
/**
|
|
716
|
+
* Default password / prompt reader. Delegates to the shared input layer
|
|
717
|
+
* ({@link readInputLine}), which suppresses echo on a TTY and falls back to a
|
|
718
|
+
* plain line read otherwise. Kept as a named function so the test seam in
|
|
719
|
+
* {@link CmdConnectDeps} can swap it out.
|
|
720
|
+
*/
|
|
721
|
+
function defaultReadLine(prompt, opts) {
|
|
722
|
+
return readInputLine(prompt, { echo: opts.echo });
|
|
723
|
+
}
|
|
724
|
+
/** `\password [USERNAME]` — change a role's password, locally hashed as SCRAM. */
|
|
725
|
+
export const cmdPassword = {
|
|
726
|
+
name: 'password',
|
|
727
|
+
helpKey: 'password',
|
|
728
|
+
run: async (ctx) => {
|
|
729
|
+
const db = ctx.settings.db;
|
|
730
|
+
if (!db || db.isClosed()) {
|
|
731
|
+
writeErr(`\\${ctx.cmdName}: not connected\n`);
|
|
732
|
+
return { status: 'error' };
|
|
733
|
+
}
|
|
734
|
+
// Refuse to prompt for a password when stdin is not a TTY. Upstream uses
|
|
735
|
+
// /dev/tty as a fallback so it works even with piped stdin; we don't yet
|
|
736
|
+
// wire that path, so notty is a hard error (it would otherwise corrupt
|
|
737
|
+
// the mainloop's own readline iterator on stdin). Tests mock the prompt
|
|
738
|
+
// via `readLine` and run with `notty: false`, the `defaultSettings`
|
|
739
|
+
// default.
|
|
740
|
+
if (ctx.settings.notty) {
|
|
741
|
+
writeErr(`\\${ctx.cmdName}: not in interactive mode\n`);
|
|
742
|
+
return { status: 'error' };
|
|
743
|
+
}
|
|
744
|
+
const userArg = ctx.nextArg('sql-id');
|
|
745
|
+
let user = userArg;
|
|
746
|
+
if (user === null) {
|
|
747
|
+
try {
|
|
748
|
+
const rs = await db.query('SELECT CURRENT_USER');
|
|
749
|
+
const row = rs.rows[0] ?? [];
|
|
750
|
+
user = typeof row[0] === 'string' ? row[0] : null;
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
754
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
755
|
+
return { status: 'error' };
|
|
756
|
+
}
|
|
757
|
+
if (!user) {
|
|
758
|
+
writeErr(`\\${ctx.cmdName}: could not determine current user\n`);
|
|
759
|
+
return { status: 'error' };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
let pw1;
|
|
763
|
+
let pw2;
|
|
764
|
+
try {
|
|
765
|
+
pw1 = await currentDeps.readLine(`Enter new password for user "${user}": `, { echo: false });
|
|
766
|
+
pw2 = await currentDeps.readLine('Enter it again: ', { echo: false });
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
770
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
771
|
+
return { status: 'error' };
|
|
772
|
+
}
|
|
773
|
+
if (pw1 !== pw2) {
|
|
774
|
+
writeErr(`\\${ctx.cmdName}: Passwords didn't match.\n`);
|
|
775
|
+
return { status: 'error' };
|
|
776
|
+
}
|
|
777
|
+
if (pw1.length === 0) {
|
|
778
|
+
writeErr(`\\${ctx.cmdName}: empty password\n`);
|
|
779
|
+
return { status: 'error' };
|
|
780
|
+
}
|
|
781
|
+
// Match upstream's PQchangePassword: `ALTER USER <id> PASSWORD <lit>`.
|
|
782
|
+
// (ALTER USER and ALTER ROLE are synonyms in PG, but we follow the
|
|
783
|
+
// libpq spelling for byte-for-byte parity with vanilla psql.)
|
|
784
|
+
const salt = currentDeps.randomBytes(16);
|
|
785
|
+
const verifier = scramSha256Verifier(pw1, salt);
|
|
786
|
+
const sql = 'ALTER USER ' +
|
|
787
|
+
db.escapeIdentifier(user) +
|
|
788
|
+
' PASSWORD ' +
|
|
789
|
+
db.escapeLiteral(verifier);
|
|
790
|
+
try {
|
|
791
|
+
await db.execSimple(sql);
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
795
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
796
|
+
return { status: 'error' };
|
|
797
|
+
}
|
|
798
|
+
return { status: 'ok' };
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Registry hook
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
/**
|
|
805
|
+
* Register the four connection commands on the given registry. Called from
|
|
806
|
+
* `dispatch.ts::defaultRegistry()`. Re-registering an existing primary name
|
|
807
|
+
* overrides — that's how we replace the no-op `\encoding` from cmd_format
|
|
808
|
+
* with this WP's version that propagates to the connection.
|
|
809
|
+
*/
|
|
810
|
+
export const registerConnectCommands = (registry) => {
|
|
811
|
+
registry.register(cmdConnect);
|
|
812
|
+
registry.register(cmdConninfo);
|
|
813
|
+
registry.register(cmdEncoding);
|
|
814
|
+
registry.register(cmdPassword);
|
|
815
|
+
};
|