neonctl 2.28.0 → 2.29.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 +71 -71
- package/dist/analytics.js +35 -33
- package/dist/api.js +34 -34
- package/dist/auth.js +50 -44
- package/dist/cli.js +2 -2
- package/dist/commands/auth.js +58 -52
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +154 -147
- package/dist/commands/bucket.js +124 -118
- package/dist/commands/checkout.js +49 -49
- package/dist/commands/config.js +212 -88
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +96 -96
- package/dist/commands/databases.js +23 -23
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +97 -98
- package/dist/commands/index.js +26 -26
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +223 -166
- package/dist/commands/neon_auth.js +381 -363
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +101 -99
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +21 -21
- package/dist/commands/schema_diff.js +23 -23
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +17 -17
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +23 -16
- package/dist/current_branch_fast_path.js +6 -6
- package/dist/dev/env.js +34 -34
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +19 -19
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +94 -92
- package/dist/log.js +2 -2
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +15 -15
- package/dist/test_utils/fixtures.js +34 -31
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +13 -13
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +20 -15
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +6 -7
package/dist/psql/wire/tls.js
CHANGED
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
* is the default and `servername` is set. The connection layer wires
|
|
28
28
|
* `servername` to the configured host before calling us.
|
|
29
29
|
*/
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
import
|
|
33
|
-
import * as
|
|
34
|
-
import { SSLRequest } from
|
|
30
|
+
import { createHash, createPrivateKey } from "node:crypto";
|
|
31
|
+
import { appendFileSync, promises as fs } from "node:fs";
|
|
32
|
+
import * as path from "node:path";
|
|
33
|
+
import * as tls from "node:tls";
|
|
34
|
+
import { SSLRequest } from "./protocol.js";
|
|
35
35
|
/**
|
|
36
36
|
* Hash of the peer cert for `tls-server-end-point`. libpq always uses SHA-256
|
|
37
37
|
* of the DER-encoded certificate; we match that.
|
|
@@ -43,9 +43,9 @@ export function computeChannelBindingData(cert) {
|
|
|
43
43
|
// marks it as `Buffer | undefined` on some versions, hence the guard.
|
|
44
44
|
const raw = cert.raw;
|
|
45
45
|
if (!raw || raw.length === 0) {
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error("TLS channel binding: peer certificate has no DER bytes");
|
|
47
47
|
}
|
|
48
|
-
return createHash(
|
|
48
|
+
return createHash("sha256").update(raw).digest();
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Send SSLRequest, read the 1-byte server response, and either upgrade the
|
|
@@ -73,32 +73,32 @@ export function computeChannelBindingData(cert) {
|
|
|
73
73
|
* fallback, so a server that does not speak TLS surfaces the handshake
|
|
74
74
|
* failure rather than a quiet downgrade.
|
|
75
75
|
*/
|
|
76
|
-
export async function negotiateTls(socket, sslMode, tlsOpts = {}, fileOpts = {}, negotiation =
|
|
77
|
-
if (sslMode ===
|
|
78
|
-
return { kind:
|
|
76
|
+
export async function negotiateTls(socket, sslMode, tlsOpts = {}, fileOpts = {}, negotiation = "postgres") {
|
|
77
|
+
if (sslMode === "disable") {
|
|
78
|
+
return { kind: "plain", socket };
|
|
79
79
|
}
|
|
80
80
|
// Direct SSL (libpq `sslnegotiation=direct`, PG 17+): skip the `SSLRequest`
|
|
81
81
|
// probe and start the TLS handshake straight away. The parse layer has
|
|
82
82
|
// already rejected weak sslmodes, so this path is only reached with an
|
|
83
83
|
// encrypted mode — never falling back to plaintext.
|
|
84
|
-
if (negotiation ===
|
|
84
|
+
if (negotiation === "direct") {
|
|
85
85
|
const mergedOpts = await loadTlsFileOptions(tlsOpts, fileOpts, sslMode);
|
|
86
86
|
return upgradeToTls(socket, mergedOpts, fileOpts.sslkeylogfile, fileOpts.sslkey,
|
|
87
87
|
/* requireAlpn */ true);
|
|
88
88
|
}
|
|
89
89
|
const reply = await sendSslRequest(socket);
|
|
90
|
-
if (reply ===
|
|
90
|
+
if (reply === "S") {
|
|
91
91
|
const mergedOpts = await loadTlsFileOptions(tlsOpts, fileOpts, sslMode);
|
|
92
92
|
return upgradeToTls(socket, mergedOpts, fileOpts.sslkeylogfile, fileOpts.sslkey);
|
|
93
93
|
}
|
|
94
94
|
// reply === 'N': server refused TLS.
|
|
95
|
-
if (sslMode ===
|
|
96
|
-
sslMode ===
|
|
97
|
-
sslMode ===
|
|
95
|
+
if (sslMode === "require" ||
|
|
96
|
+
sslMode === "verify-ca" ||
|
|
97
|
+
sslMode === "verify-full") {
|
|
98
98
|
throw new Error(`SSL connection required (sslmode=${sslMode}) but server refused (replied 'N')`);
|
|
99
99
|
}
|
|
100
100
|
// 'allow' / 'prefer': fall back to plain text.
|
|
101
|
-
return { kind:
|
|
101
|
+
return { kind: "plain", socket };
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
104
|
* Read each non-empty file path in `fileOpts` and merge the bytes onto a
|
|
@@ -126,12 +126,12 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
126
126
|
// validate the chain. Mirror that so a stale / placeholder
|
|
127
127
|
// `sslrootcert=` doesn't blow up sslmode=require connections.
|
|
128
128
|
const needsRootCert = sslMode === undefined ||
|
|
129
|
-
sslMode ===
|
|
130
|
-
sslMode ===
|
|
129
|
+
sslMode === "verify-ca" ||
|
|
130
|
+
sslMode === "verify-full";
|
|
131
131
|
if (needsRootCert &&
|
|
132
132
|
fileOpts.sslrootcert !== undefined &&
|
|
133
|
-
fileOpts.sslrootcert !==
|
|
134
|
-
if (fileOpts.sslrootcert ===
|
|
133
|
+
fileOpts.sslrootcert !== "") {
|
|
134
|
+
if (fileOpts.sslrootcert === "system") {
|
|
135
135
|
// `sslrootcert=system`: use the OS / OpenSSL trust store instead of a
|
|
136
136
|
// file. libpq's OpenSSL build honours OpenSSL's $SSL_CERT_FILE (a single
|
|
137
137
|
// bundle) and $SSL_CERT_DIR (a directory of hashed CA files); we read
|
|
@@ -140,11 +140,11 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
140
140
|
// for verify-* modes) is left intact.
|
|
141
141
|
const systemCas = [];
|
|
142
142
|
const sslCertFile = process.env.SSL_CERT_FILE;
|
|
143
|
-
if (sslCertFile !== undefined && sslCertFile !==
|
|
144
|
-
systemCas.push(await readPem(
|
|
143
|
+
if (sslCertFile !== undefined && sslCertFile !== "") {
|
|
144
|
+
systemCas.push(await readPem("sslrootcert", sslCertFile, "CERTIFICATE"));
|
|
145
145
|
}
|
|
146
146
|
const sslCertDir = process.env.SSL_CERT_DIR;
|
|
147
|
-
if (sslCertDir !== undefined && sslCertDir !==
|
|
147
|
+
if (sslCertDir !== undefined && sslCertDir !== "") {
|
|
148
148
|
systemCas.push(...(await readCaDir(sslCertDir)));
|
|
149
149
|
}
|
|
150
150
|
if (systemCas.length === 1) {
|
|
@@ -155,7 +155,7 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
else {
|
|
158
|
-
merged.ca = await readPem(
|
|
158
|
+
merged.ca = await readPem("sslrootcert", fileOpts.sslrootcert, "CERTIFICATE");
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
// libpq `sslcertmode` gates whether the client cert/key are sent.
|
|
@@ -163,30 +163,30 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
163
163
|
// - `require`: a cert MUST be configured (we only honour an explicit
|
|
164
164
|
// `sslcert`, not libpq's default `~/.postgresql/postgresql.crt`).
|
|
165
165
|
// - `allow` / unset: current behaviour (send when present).
|
|
166
|
-
const certMode = fileOpts.sslcertmode ??
|
|
167
|
-
const clientCert = fileOpts.sslcert !== undefined && fileOpts.sslcert !==
|
|
166
|
+
const certMode = fileOpts.sslcertmode ?? "allow";
|
|
167
|
+
const clientCert = fileOpts.sslcert !== undefined && fileOpts.sslcert !== ""
|
|
168
168
|
? fileOpts.sslcert
|
|
169
169
|
: undefined;
|
|
170
|
-
if (certMode ===
|
|
170
|
+
if (certMode === "require" && clientCert === undefined) {
|
|
171
171
|
throw new Error(`sslcertmode value "require" requires a client certificate`);
|
|
172
172
|
}
|
|
173
|
-
if (certMode !==
|
|
173
|
+
if (certMode !== "disable") {
|
|
174
174
|
if (clientCert !== undefined) {
|
|
175
|
-
merged.cert = await readPem(
|
|
175
|
+
merged.cert = await readPem("sslcert", clientCert, "CERTIFICATE");
|
|
176
176
|
}
|
|
177
|
-
if (fileOpts.sslkey !== undefined && fileOpts.sslkey !==
|
|
177
|
+
if (fileOpts.sslkey !== undefined && fileOpts.sslkey !== "") {
|
|
178
178
|
await assertKeyPermissions(fileOpts.sslkey);
|
|
179
|
-
merged.key = await readPem(
|
|
179
|
+
merged.key = await readPem("sslkey", fileOpts.sslkey, "PRIVATE KEY");
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
// CRLs come from a single file (`sslcrl`) and/or every file in a directory
|
|
183
183
|
// (`sslcrldir`). Node's `crl` option accepts an array of PEM buffers, so we
|
|
184
184
|
// collect each source and only set `crl` when at least one was read.
|
|
185
185
|
const crls = [];
|
|
186
|
-
if (fileOpts.sslcrl !== undefined && fileOpts.sslcrl !==
|
|
187
|
-
crls.push(await readPem(
|
|
186
|
+
if (fileOpts.sslcrl !== undefined && fileOpts.sslcrl !== "") {
|
|
187
|
+
crls.push(await readPem("sslcrl", fileOpts.sslcrl));
|
|
188
188
|
}
|
|
189
|
-
if (fileOpts.sslcrldir !== undefined && fileOpts.sslcrldir !==
|
|
189
|
+
if (fileOpts.sslcrldir !== undefined && fileOpts.sslcrldir !== "") {
|
|
190
190
|
crls.push(...(await readCrlDir(fileOpts.sslcrldir)));
|
|
191
191
|
}
|
|
192
192
|
if (crls.length === 1) {
|
|
@@ -198,7 +198,7 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
198
198
|
// sslpassword is plumbed through verbatim — OpenSSL applies it when it
|
|
199
199
|
// sees an encrypted key. Empty string is "no passphrase" (libpq's
|
|
200
200
|
// convention) so we skip it.
|
|
201
|
-
if (fileOpts.sslpassword !== undefined && fileOpts.sslpassword !==
|
|
201
|
+
if (fileOpts.sslpassword !== undefined && fileOpts.sslpassword !== "") {
|
|
202
202
|
merged.passphrase = fileOpts.sslpassword;
|
|
203
203
|
}
|
|
204
204
|
// sslkeylogfile is not a tls.connect option; the keylog listener is wired
|
|
@@ -206,7 +206,7 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
206
206
|
// by opening it for append, so an unwritable path fails fast at connect
|
|
207
207
|
// time with `could not open sslkeylogfile "<path>": <reason>` rather than
|
|
208
208
|
// silently dropping keys mid-handshake.
|
|
209
|
-
if (fileOpts.sslkeylogfile !== undefined && fileOpts.sslkeylogfile !==
|
|
209
|
+
if (fileOpts.sslkeylogfile !== undefined && fileOpts.sslkeylogfile !== "") {
|
|
210
210
|
await assertKeyLogFileWritable(fileOpts.sslkeylogfile);
|
|
211
211
|
}
|
|
212
212
|
return merged;
|
|
@@ -219,7 +219,7 @@ export async function loadTlsFileOptions(tlsOpts, fileOpts, sslMode) {
|
|
|
219
219
|
*/
|
|
220
220
|
async function assertKeyLogFileWritable(filePath) {
|
|
221
221
|
try {
|
|
222
|
-
const handle = await fs.open(filePath,
|
|
222
|
+
const handle = await fs.open(filePath, "a");
|
|
223
223
|
await handle.close();
|
|
224
224
|
}
|
|
225
225
|
catch (err) {
|
|
@@ -232,19 +232,19 @@ function isPemArmored(bytes) {
|
|
|
232
232
|
// A PEM file is ASCII text; scan a bounded prefix (skipping leading
|
|
233
233
|
// whitespace libpq tolerates) for the armor marker. DER is binary and will
|
|
234
234
|
// not contain this token at the front.
|
|
235
|
-
const head = bytes.subarray(0, 64).toString(
|
|
236
|
-
return head.includes(
|
|
235
|
+
const head = bytes.subarray(0, 64).toString("latin1");
|
|
236
|
+
return head.includes("-----BEGIN");
|
|
237
237
|
}
|
|
238
238
|
/**
|
|
239
239
|
* Wrap raw DER bytes in the requested PEM armor: base64 the DER, split into
|
|
240
240
|
* 64-char lines (the PEM convention), and bracket with the BEGIN/END markers.
|
|
241
241
|
*/
|
|
242
242
|
function derToPem(der, armor) {
|
|
243
|
-
const b64 = der.toString(
|
|
243
|
+
const b64 = der.toString("base64");
|
|
244
244
|
const lines = b64.match(/.{1,64}/g) ?? [];
|
|
245
|
-
const body = lines.join(
|
|
245
|
+
const body = lines.join("\n");
|
|
246
246
|
const pem = `-----BEGIN ${armor}-----\n${body}\n-----END ${armor}-----\n`;
|
|
247
|
-
return Buffer.from(pem,
|
|
247
|
+
return Buffer.from(pem, "ascii");
|
|
248
248
|
}
|
|
249
249
|
/**
|
|
250
250
|
* Convert a DER-encoded private key to canonical PKCS#8 PEM, the way libpq
|
|
@@ -258,11 +258,11 @@ function derToPem(der, armor) {
|
|
|
258
258
|
*/
|
|
259
259
|
function derPrivateKeyToPem(der) {
|
|
260
260
|
let lastErr;
|
|
261
|
-
for (const type of [
|
|
261
|
+
for (const type of ["pkcs8", "pkcs1", "sec1"]) {
|
|
262
262
|
try {
|
|
263
|
-
const key = createPrivateKey({ key: der, format:
|
|
264
|
-
const pem = key.export({ format:
|
|
265
|
-
return typeof pem ===
|
|
263
|
+
const key = createPrivateKey({ key: der, format: "der", type });
|
|
264
|
+
const pem = key.export({ format: "pem", type: "pkcs8" });
|
|
265
|
+
return typeof pem === "string" ? Buffer.from(pem, "ascii") : pem;
|
|
266
266
|
}
|
|
267
267
|
catch (err) {
|
|
268
268
|
lastErr = err;
|
|
@@ -291,7 +291,7 @@ async function readPem(label, filePath, derArmor) {
|
|
|
291
291
|
if (derArmor !== undefined && !isPemArmored(bytes)) {
|
|
292
292
|
// Private keys need format-aware decoding (PKCS#8 / PKCS#1 / SEC1);
|
|
293
293
|
// certs are a single ASN.1 shape and wrap directly.
|
|
294
|
-
return derArmor ===
|
|
294
|
+
return derArmor === "PRIVATE KEY"
|
|
295
295
|
? derPrivateKeyToPem(bytes)
|
|
296
296
|
: derToPem(bytes, derArmor);
|
|
297
297
|
}
|
|
@@ -310,7 +310,7 @@ async function readPem(label, filePath, derArmor) {
|
|
|
310
310
|
* carry a private key here).
|
|
311
311
|
*/
|
|
312
312
|
async function assertKeyPermissions(keyPath) {
|
|
313
|
-
if (process.platform ===
|
|
313
|
+
if (process.platform === "win32")
|
|
314
314
|
return;
|
|
315
315
|
let stat;
|
|
316
316
|
try {
|
|
@@ -355,7 +355,7 @@ async function readCrlDir(dirPath) {
|
|
|
355
355
|
for (const entry of entries) {
|
|
356
356
|
if (!entry.isFile())
|
|
357
357
|
continue;
|
|
358
|
-
out.push(await readPem(
|
|
358
|
+
out.push(await readPem("sslcrldir", path.join(dirPath, entry.name)));
|
|
359
359
|
}
|
|
360
360
|
return out;
|
|
361
361
|
}
|
|
@@ -381,7 +381,7 @@ export async function readCaDir(dirPath) {
|
|
|
381
381
|
for (const entry of entries) {
|
|
382
382
|
if (!entry.isFile())
|
|
383
383
|
continue;
|
|
384
|
-
out.push(await readPem(
|
|
384
|
+
out.push(await readPem("SSL_CERT_DIR", path.join(dirPath, entry.name), "CERTIFICATE"));
|
|
385
385
|
}
|
|
386
386
|
return out;
|
|
387
387
|
}
|
|
@@ -403,7 +403,7 @@ export function sendSslRequest(socket) {
|
|
|
403
403
|
if (chunk.length === 0)
|
|
404
404
|
return;
|
|
405
405
|
const first = String.fromCharCode(chunk[0]);
|
|
406
|
-
if (first !==
|
|
406
|
+
if (first !== "S" && first !== "N") {
|
|
407
407
|
cleanup();
|
|
408
408
|
reject(new Error(`Unexpected SSLRequest response byte 0x${chunk[0].toString(16)}`));
|
|
409
409
|
return;
|
|
@@ -416,11 +416,11 @@ export function sendSslRequest(socket) {
|
|
|
416
416
|
resolve(first);
|
|
417
417
|
};
|
|
418
418
|
const cleanup = () => {
|
|
419
|
-
socket.removeListener(
|
|
420
|
-
socket.removeListener(
|
|
419
|
+
socket.removeListener("data", onData);
|
|
420
|
+
socket.removeListener("error", onError);
|
|
421
421
|
};
|
|
422
|
-
socket.on(
|
|
423
|
-
socket.on(
|
|
422
|
+
socket.on("data", onData);
|
|
423
|
+
socket.on("error", onError);
|
|
424
424
|
socket.write(SSLRequest());
|
|
425
425
|
});
|
|
426
426
|
}
|
|
@@ -455,16 +455,16 @@ export function mapTlsHandshakeError(err, servername, keyPath) {
|
|
|
455
455
|
// Encrypted client-key decrypt failure (wrong / missing `sslpassword`).
|
|
456
456
|
// OpenSSL throws this synchronously out of `tls.connect`; reshape it to
|
|
457
457
|
// libpq's `could not load private key file "<path>": ... bad decrypt`.
|
|
458
|
-
if (code ===
|
|
459
|
-
const where = keyPath !== undefined && keyPath !==
|
|
458
|
+
if (code === "ERR_OSSL_BAD_DECRYPT" || /bad decrypt/i.test(msg)) {
|
|
459
|
+
const where = keyPath !== undefined && keyPath !== ""
|
|
460
460
|
? `private key file "${keyPath}"`
|
|
461
|
-
:
|
|
461
|
+
: "private key file";
|
|
462
462
|
const mapped = new Error(`could not load ${where}: ${msg}`);
|
|
463
463
|
mapped.cause = err;
|
|
464
464
|
return mapped;
|
|
465
465
|
}
|
|
466
|
-
if (code ===
|
|
467
|
-
const host = servername ??
|
|
466
|
+
if (code === "ERR_TLS_CERT_ALTNAME_INVALID") {
|
|
467
|
+
const host = servername ?? "";
|
|
468
468
|
const mapped = new Error(`server certificate for "${host}" does not match host name "${host}"`);
|
|
469
469
|
mapped.cause = err;
|
|
470
470
|
return mapped;
|
|
@@ -474,10 +474,10 @@ export function mapTlsHandshakeError(err, servername, keyPath) {
|
|
|
474
474
|
// `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED`, etc. libpq collapses
|
|
475
475
|
// them all to `certificate verify failed`.
|
|
476
476
|
const isVerifyFailure = code !== undefined &&
|
|
477
|
-
code !==
|
|
477
|
+
code !== "ERR_TLS_CERT_ALTNAME_INVALID" &&
|
|
478
478
|
/CERT|SIGNATURE|SELF_SIGNED|UNABLE_TO|CHAIN|EXPIRED|NOT_YET_VALID|INVALID_CA/.test(code);
|
|
479
479
|
if (isVerifyFailure || /certificate verify failed/i.test(msg)) {
|
|
480
|
-
const mapped = new Error(
|
|
480
|
+
const mapped = new Error("certificate verify failed");
|
|
481
481
|
mapped.cause = err;
|
|
482
482
|
return mapped;
|
|
483
483
|
}
|
|
@@ -485,7 +485,7 @@ export function mapTlsHandshakeError(err, servername, keyPath) {
|
|
|
485
485
|
}
|
|
486
486
|
/** Pull the SNI `servername` from the TLS options for error messages. */
|
|
487
487
|
function getServername(tlsOpts) {
|
|
488
|
-
return typeof tlsOpts.servername ===
|
|
488
|
+
return typeof tlsOpts.servername === "string"
|
|
489
489
|
? tlsOpts.servername
|
|
490
490
|
: undefined;
|
|
491
491
|
}
|
|
@@ -498,7 +498,7 @@ requireAlpn = false) {
|
|
|
498
498
|
return new Promise((resolve, reject) => {
|
|
499
499
|
let tlsSocket;
|
|
500
500
|
const cleanup = () => {
|
|
501
|
-
tlsSocket?.removeListener(
|
|
501
|
+
tlsSocket?.removeListener("error", onError);
|
|
502
502
|
};
|
|
503
503
|
const onError = (err) => {
|
|
504
504
|
cleanup();
|
|
@@ -523,10 +523,11 @@ requireAlpn = false) {
|
|
|
523
523
|
// Enforce the mandatory `postgresql` ALPN for direct SSL. Without
|
|
524
524
|
// it a TLS terminator / HTTPS proxy with a valid host cert would be
|
|
525
525
|
// accepted and the startup packet sent in the blind (review #9).
|
|
526
|
-
if (requireAlpn &&
|
|
526
|
+
if (requireAlpn &&
|
|
527
|
+
established.alpnProtocol !== "postgresql") {
|
|
527
528
|
cleanup();
|
|
528
529
|
established.destroy();
|
|
529
|
-
reject(new Error(
|
|
530
|
+
reject(new Error("direct SSL connection requires ALPN, but the server did " +
|
|
530
531
|
'not negotiate the "postgresql" protocol'));
|
|
531
532
|
return;
|
|
532
533
|
}
|
|
@@ -538,7 +539,7 @@ requireAlpn = false) {
|
|
|
538
539
|
// `getPeerCertificate(true)` for compatibility.
|
|
539
540
|
const x509 = established.getPeerX509Certificate?.();
|
|
540
541
|
if (x509?.raw && x509.raw.length > 0) {
|
|
541
|
-
channelBindingData = createHash(
|
|
542
|
+
channelBindingData = createHash("sha256")
|
|
542
543
|
.update(x509.raw)
|
|
543
544
|
.digest();
|
|
544
545
|
}
|
|
@@ -549,7 +550,8 @@ requireAlpn = false) {
|
|
|
549
550
|
// accept that channel binding is unavailable.
|
|
550
551
|
const peerCert = established.getPeerCertificate(true);
|
|
551
552
|
if (peerCert?.raw && peerCert.raw.length > 0) {
|
|
552
|
-
channelBindingData =
|
|
553
|
+
channelBindingData =
|
|
554
|
+
computeChannelBindingData(peerCert);
|
|
553
555
|
}
|
|
554
556
|
}
|
|
555
557
|
}
|
|
@@ -558,18 +560,22 @@ requireAlpn = false) {
|
|
|
558
560
|
// path will fall back to SCRAM-SHA-256 (non-PLUS).
|
|
559
561
|
channelBindingData = null;
|
|
560
562
|
}
|
|
561
|
-
resolve({
|
|
563
|
+
resolve({
|
|
564
|
+
kind: "tls",
|
|
565
|
+
socket: established,
|
|
566
|
+
channelBindingData,
|
|
567
|
+
});
|
|
562
568
|
});
|
|
563
569
|
}
|
|
564
570
|
catch (err) {
|
|
565
571
|
reject(mapTlsHandshakeError(err instanceof Error ? err : new Error(String(err)), getServername(tlsOpts), keyPath));
|
|
566
572
|
return;
|
|
567
573
|
}
|
|
568
|
-
tlsSocket.on(
|
|
574
|
+
tlsSocket.on("error", onError);
|
|
569
575
|
// libpq `sslkeylogfile`: append each emitted key-log line so the
|
|
570
576
|
// handshake can be decrypted offline. The path was pre-checked for
|
|
571
577
|
// writability in loadTlsFileOptions.
|
|
572
|
-
if (sslkeylogfile !== undefined && sslkeylogfile !==
|
|
578
|
+
if (sslkeylogfile !== undefined && sslkeylogfile !== "") {
|
|
573
579
|
attachKeyLogListener(tlsSocket, sslkeylogfile);
|
|
574
580
|
}
|
|
575
581
|
});
|
|
@@ -585,12 +591,12 @@ requireAlpn = false) {
|
|
|
585
591
|
* fake socket without a real TLS handshake. Exported for tests.
|
|
586
592
|
*/
|
|
587
593
|
export function attachKeyLogListener(socket, filePath) {
|
|
588
|
-
socket.on(
|
|
594
|
+
socket.on("keylog", (line) => {
|
|
589
595
|
try {
|
|
590
596
|
appendFileSync(filePath, line);
|
|
591
597
|
}
|
|
592
598
|
catch (err) {
|
|
593
|
-
socket.emit(
|
|
599
|
+
socket.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
594
600
|
}
|
|
595
601
|
});
|
|
596
602
|
}
|
package/dist/storage_api.js
CHANGED
|
@@ -23,9 +23,9 @@ export const createProjectBranchBucket = (apiClient, { projectId, branchId, name
|
|
|
23
23
|
}
|
|
24
24
|
return apiClient.request({
|
|
25
25
|
path: bucketsPath(projectId, branchId),
|
|
26
|
-
method:
|
|
26
|
+
method: "POST",
|
|
27
27
|
body,
|
|
28
|
-
format:
|
|
28
|
+
format: "json",
|
|
29
29
|
secure: true,
|
|
30
30
|
});
|
|
31
31
|
};
|
|
@@ -36,8 +36,8 @@ export const createProjectBranchBucket = (apiClient, { projectId, branchId, name
|
|
|
36
36
|
*/
|
|
37
37
|
export const listProjectBranchBuckets = (apiClient, { projectId, branchId }) => apiClient.request({
|
|
38
38
|
path: bucketsPath(projectId, branchId),
|
|
39
|
-
method:
|
|
40
|
-
format:
|
|
39
|
+
method: "GET",
|
|
40
|
+
format: "json",
|
|
41
41
|
secure: true,
|
|
42
42
|
});
|
|
43
43
|
/**
|
|
@@ -47,7 +47,7 @@ export const listProjectBranchBuckets = (apiClient, { projectId, branchId }) =>
|
|
|
47
47
|
*/
|
|
48
48
|
export const deleteProjectBranchBucket = (apiClient, { projectId, branchId, bucketName, }) => apiClient.request({
|
|
49
49
|
path: bucketPath(projectId, branchId, bucketName),
|
|
50
|
-
method:
|
|
50
|
+
method: "DELETE",
|
|
51
51
|
secure: true,
|
|
52
52
|
});
|
|
53
53
|
/**
|
|
@@ -57,9 +57,9 @@ export const deleteProjectBranchBucket = (apiClient, { projectId, branchId, buck
|
|
|
57
57
|
*/
|
|
58
58
|
export const listProjectBranchBucketObjects = (apiClient, { projectId, branchId, bucketName, ...query }) => apiClient.request({
|
|
59
59
|
path: `${bucketPath(projectId, branchId, bucketName)}/objects`,
|
|
60
|
-
method:
|
|
60
|
+
method: "GET",
|
|
61
61
|
query,
|
|
62
|
-
format:
|
|
62
|
+
format: "json",
|
|
63
63
|
secure: true,
|
|
64
64
|
});
|
|
65
65
|
/**
|
|
@@ -79,8 +79,8 @@ export const listProjectBranchBucketObjects = (apiClient, { projectId, branchId,
|
|
|
79
79
|
*/
|
|
80
80
|
export const getProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
|
|
81
81
|
path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}/download`,
|
|
82
|
-
method:
|
|
83
|
-
format:
|
|
82
|
+
method: "GET",
|
|
83
|
+
format: "stream",
|
|
84
84
|
secure: true,
|
|
85
85
|
});
|
|
86
86
|
/**
|
|
@@ -93,7 +93,7 @@ export const getProjectBranchBucketObject = (apiClient, { projectId, branchId, b
|
|
|
93
93
|
*/
|
|
94
94
|
export const deleteProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
|
|
95
95
|
path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}`,
|
|
96
|
-
method:
|
|
96
|
+
method: "DELETE",
|
|
97
97
|
secure: true,
|
|
98
98
|
});
|
|
99
99
|
/**
|
|
@@ -106,9 +106,9 @@ export const deleteProjectBranchBucketObject = (apiClient, { projectId, branchId
|
|
|
106
106
|
*/
|
|
107
107
|
export const deleteProjectBranchBucketObjectsByPrefix = (apiClient, { projectId, branchId, bucketName, prefix, }) => apiClient.request({
|
|
108
108
|
path: `${bucketPath(projectId, branchId, bucketName)}/objects-by-prefix`,
|
|
109
|
-
method:
|
|
109
|
+
method: "DELETE",
|
|
110
110
|
query: { prefix },
|
|
111
|
-
format:
|
|
111
|
+
format: "json",
|
|
112
112
|
secure: true,
|
|
113
113
|
});
|
|
114
114
|
/**
|
|
@@ -130,7 +130,7 @@ export const deleteProjectBranchBucketObjectsByPrefix = (apiClient, { projectId,
|
|
|
130
130
|
* @request POST /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}/presign
|
|
131
131
|
*/
|
|
132
132
|
export const presignUpload = (apiClient, { projectId, branchId, bucketName, objectKey, contentType, expiresInSeconds, }) => {
|
|
133
|
-
const body = { operation:
|
|
133
|
+
const body = { operation: "upload" };
|
|
134
134
|
if (contentType !== undefined) {
|
|
135
135
|
body.content_type = contentType;
|
|
136
136
|
}
|
|
@@ -139,9 +139,9 @@ export const presignUpload = (apiClient, { projectId, branchId, bucketName, obje
|
|
|
139
139
|
}
|
|
140
140
|
return apiClient.request({
|
|
141
141
|
path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}/presign`,
|
|
142
|
-
method:
|
|
142
|
+
method: "POST",
|
|
143
143
|
body,
|
|
144
|
-
format:
|
|
144
|
+
format: "json",
|
|
145
145
|
secure: true,
|
|
146
146
|
});
|
|
147
147
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { log } from
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import emocks from "emocks";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import strip from "strip-ansi";
|
|
7
|
+
import { expect, test as originalTest } from "vitest";
|
|
8
|
+
import { log } from "../log";
|
|
9
9
|
/**
|
|
10
10
|
* Reserve a localhost port and close its listener, returning a URL that is guaranteed to
|
|
11
11
|
* refuse connections right now. Lets a test drive the CLI into a real `ECONNREFUSED`
|
|
@@ -13,8 +13,8 @@ import { log } from '../log';
|
|
|
13
13
|
*/
|
|
14
14
|
const reserveClosedPort = () => new Promise((resolve, reject) => {
|
|
15
15
|
const probe = createServer();
|
|
16
|
-
probe.on(
|
|
17
|
-
probe.listen(0,
|
|
16
|
+
probe.on("error", reject);
|
|
17
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
18
18
|
const { port } = probe.address();
|
|
19
19
|
probe.close((err) => {
|
|
20
20
|
if (err) {
|
|
@@ -33,12 +33,12 @@ export const test = originalTest.extend({
|
|
|
33
33
|
await use(async (mockDir) => {
|
|
34
34
|
const app = express();
|
|
35
35
|
app.use(express.json());
|
|
36
|
-
app.use(
|
|
37
|
-
|
|
36
|
+
app.use("/", emocks(join(process.cwd(), "mocks", mockDir), {
|
|
37
|
+
"404": (_req, res) => res.status(404).send({ message: "Not Found" }),
|
|
38
38
|
}));
|
|
39
39
|
const server = await new Promise((resolve) => {
|
|
40
40
|
const s = app.listen(0, () => {
|
|
41
|
-
log.debug(
|
|
41
|
+
log.debug("Mock server listening at %d", s.address().port);
|
|
42
42
|
resolve(s);
|
|
43
43
|
});
|
|
44
44
|
});
|
|
@@ -64,44 +64,47 @@ export const test = originalTest.extend({
|
|
|
64
64
|
await use(async (args, options = {}) => {
|
|
65
65
|
const apiHost = options.unreachableHost
|
|
66
66
|
? await reserveClosedPort()
|
|
67
|
-
: `http://localhost:${(await runMockServer(options.mockDir ||
|
|
68
|
-
let output =
|
|
69
|
-
let error =
|
|
70
|
-
const cp = fork(join(process.cwd(),
|
|
71
|
-
|
|
67
|
+
: `http://localhost:${(await runMockServer(options.mockDir || "main")).address().port}`;
|
|
68
|
+
let output = "";
|
|
69
|
+
let error = "";
|
|
70
|
+
const cp = fork(join(process.cwd(), "./dist/index.js"), [
|
|
71
|
+
"--api-host",
|
|
72
72
|
apiHost,
|
|
73
|
-
|
|
74
|
-
options.output ?? (options.outputTable ?
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
"--output",
|
|
74
|
+
options.output ?? (options.outputTable ? "table" : "yaml"),
|
|
75
|
+
"--api-key",
|
|
76
|
+
"test-key",
|
|
77
|
+
"--no-analytics",
|
|
78
78
|
...args,
|
|
79
79
|
], {
|
|
80
|
-
stdio:
|
|
80
|
+
stdio: "pipe",
|
|
81
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
81
82
|
env: {
|
|
82
|
-
PATH:
|
|
83
|
+
PATH: `${join(process.cwd(), "mocks/bin")}:${process.env.PATH}`,
|
|
83
84
|
...(options.env ?? {}),
|
|
84
85
|
},
|
|
85
86
|
});
|
|
86
87
|
return new Promise((resolve, reject) => {
|
|
87
|
-
cp.stdout?.on(
|
|
88
|
+
cp.stdout?.on("data", (data) => {
|
|
88
89
|
output += data.toString();
|
|
89
90
|
});
|
|
90
|
-
cp.stderr?.on(
|
|
91
|
+
cp.stderr?.on("data", (data) => {
|
|
91
92
|
error += data.toString();
|
|
92
93
|
log.error(data.toString());
|
|
93
94
|
});
|
|
94
|
-
cp.on(
|
|
95
|
+
cp.on("error", (err) => {
|
|
95
96
|
log.error(err);
|
|
96
97
|
throw err;
|
|
97
98
|
});
|
|
98
|
-
cp.on(
|
|
99
|
+
cp.on("close", (code) => {
|
|
99
100
|
try {
|
|
100
101
|
expect(code).toBe(options?.code ?? 0);
|
|
101
102
|
expect(output).toMatchSnapshot();
|
|
102
103
|
if (options.stderr !== undefined) {
|
|
103
|
-
expect(strip(error).replace(/\s+/g,
|
|
104
|
-
? options.stderr
|
|
104
|
+
expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string"
|
|
105
|
+
? options.stderr
|
|
106
|
+
.toString()
|
|
107
|
+
.replace(/\s+/g, " ")
|
|
105
108
|
: options.stderr);
|
|
106
109
|
}
|
|
107
110
|
resolve();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OAuth2Server } from "oauth2-mock-server";
|
|
2
|
+
import { log } from "../log";
|
|
3
3
|
export const startOauthServer = async () => {
|
|
4
4
|
const server = new OAuth2Server();
|
|
5
|
-
await server.issuer.keys.generate(
|
|
6
|
-
await server.start(0,
|
|
7
|
-
log.debug(
|
|
5
|
+
await server.issuer.keys.generate("RS256");
|
|
6
|
+
await server.start(0, "localhost");
|
|
7
|
+
log.debug("Started OAuth server on port %d", server.address().port);
|
|
8
8
|
return server;
|
|
9
9
|
};
|