neonctl 2.28.0 → 2.29.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 +2 -2
- 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 +33 -33
- 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/README.md
CHANGED
|
@@ -361,7 +361,7 @@ neonctl env pull
|
|
|
361
361
|
neonctl env pull --branch preview --file .env.preview
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
If you'd rather not keep env vars on disk, inject them at runtime instead with `neon-env run -- <your dev command>` (from `@
|
|
364
|
+
If you'd rather not keep env vars on disk, inject them at runtime instead with `neon-env run -- <your dev command>` (from `@neon/env`) or `neonctl dev`, and pass `--no-env-pull` to `link` / `checkout`.
|
|
365
365
|
|
|
366
366
|
**Where `.neon` lives**: `link` writes `.neon` into the **current working directory** by default. If an existing `.neon` is found in any parent directory, that file is reused — so commands run from a sub-directory of a linked project still pick up the project's context. To pin the location explicitly, pass `--context-file <path>`.
|
|
367
367
|
|
|
@@ -373,7 +373,7 @@ Describe a branch's desired state in a `neon.ts` policy and reconcile it from th
|
|
|
373
373
|
|
|
374
374
|
```ts
|
|
375
375
|
// neon.ts
|
|
376
|
-
import { defineConfig } from '@
|
|
376
|
+
import { defineConfig } from '@neon/config/v1';
|
|
377
377
|
|
|
378
378
|
export default defineConfig({
|
|
379
379
|
// Static: what exists on every branch (drives the typed env).
|
package/dist/analytics.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { join } from
|
|
3
|
-
import { Analytics } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
const WRITE_KEY =
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Analytics } from "@segment/analytics-node";
|
|
4
|
+
import { getApiClient, isNeonApiError } from "./api.js";
|
|
5
|
+
import { CREDENTIALS_FILE } from "./config.js";
|
|
6
|
+
import { isCurrentBranchProbe } from "./context.js";
|
|
7
|
+
import { getGithubEnvVars, isCi } from "./env.js";
|
|
8
|
+
import { log } from "./log.js";
|
|
9
|
+
import pkg from "./pkg.js";
|
|
10
|
+
const WRITE_KEY = "3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV";
|
|
11
11
|
/**
|
|
12
12
|
* Raw-argv fallback for the offline `--current-branch` probe. The init
|
|
13
13
|
* middleware runs before validation, where the parsed `currentBranch` flag may
|
|
14
14
|
* not be populated yet, so we also scan `process.argv` directly to be safe.
|
|
15
15
|
*/
|
|
16
|
-
const hasCurrentBranchArgv = () => process.argv.includes(
|
|
16
|
+
const hasCurrentBranchArgv = () => process.argv.includes("--current-branch");
|
|
17
17
|
let client;
|
|
18
18
|
let clientInitialized = false;
|
|
19
|
-
let userId =
|
|
19
|
+
let userId = "";
|
|
20
20
|
/**
|
|
21
21
|
* Phase 1: Run before validation so the Segment client exists if any
|
|
22
22
|
* middleware (e.g. auth) fails. Enables sendError() in the fail handler.
|
|
@@ -36,11 +36,11 @@ export const initAnalyticsClientMiddleware = (args) => {
|
|
|
36
36
|
clientInitialized = true;
|
|
37
37
|
client = new Analytics({
|
|
38
38
|
writeKey: WRITE_KEY,
|
|
39
|
-
host:
|
|
39
|
+
host: "https://track.neon.tech",
|
|
40
40
|
});
|
|
41
|
-
log.debug(
|
|
41
|
+
log.debug("Initialized CLI analytics client");
|
|
42
42
|
client.identify({
|
|
43
|
-
userId:
|
|
43
|
+
userId: "anonymous",
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
@@ -56,11 +56,13 @@ export const analyticsMiddleware = async (args) => {
|
|
|
56
56
|
}
|
|
57
57
|
try {
|
|
58
58
|
const credentialsPath = join(args.configDir, CREDENTIALS_FILE);
|
|
59
|
-
const credentials = readFileSync(credentialsPath, {
|
|
59
|
+
const credentials = readFileSync(credentialsPath, {
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
});
|
|
60
62
|
userId = JSON.parse(credentials).user_id;
|
|
61
63
|
}
|
|
62
64
|
catch (err) {
|
|
63
|
-
log.debug(
|
|
65
|
+
log.debug("Failed to read credentials file", err);
|
|
64
66
|
}
|
|
65
67
|
try {
|
|
66
68
|
if (args.apiKey) {
|
|
@@ -75,25 +77,25 @@ export const analyticsMiddleware = async (args) => {
|
|
|
75
77
|
args.authMethod = authDetails.auth_method;
|
|
76
78
|
args.authData = authDetails.auth_data;
|
|
77
79
|
// Get user id if not org api key
|
|
78
|
-
if (!userId && authDetails.auth_method !==
|
|
80
|
+
if (!userId && authDetails.auth_method !== "api_key_org") {
|
|
79
81
|
const resp = await apiClient?.getCurrentUserInfo?.();
|
|
80
82
|
userId = resp?.data?.id;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
else {
|
|
84
86
|
args.accountId = userId;
|
|
85
|
-
args.authMethod =
|
|
87
|
+
args.authMethod = "oauth";
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
catch (err) {
|
|
89
|
-
log.debug(
|
|
91
|
+
log.debug("Failed to get user id from api", err);
|
|
90
92
|
}
|
|
91
93
|
client.identify({
|
|
92
|
-
userId: userId?.toString() ??
|
|
94
|
+
userId: userId?.toString() ?? "anonymous",
|
|
93
95
|
});
|
|
94
96
|
client.track({
|
|
95
|
-
userId: userId ||
|
|
96
|
-
event:
|
|
97
|
+
userId: userId || "anonymous",
|
|
98
|
+
event: "CLI Started",
|
|
97
99
|
properties: getAnalyticsEventProperties(args),
|
|
98
100
|
context: {
|
|
99
101
|
direct: true,
|
|
@@ -102,12 +104,12 @@ export const analyticsMiddleware = async (args) => {
|
|
|
102
104
|
};
|
|
103
105
|
export const closeAnalytics = async (opts) => {
|
|
104
106
|
if (client) {
|
|
105
|
-
log.debug(
|
|
107
|
+
log.debug("Flushing CLI analytics");
|
|
106
108
|
// `timeout` bounds how long we wait for in-flight events to flush so a
|
|
107
109
|
// slow / unreachable track.neon.tech can't hang a short-lived command
|
|
108
110
|
// (e.g. the psql launch path, which flushes here before process.exit).
|
|
109
111
|
await client.closeAndFlush(opts);
|
|
110
|
-
log.debug(
|
|
112
|
+
log.debug("Flushed CLI analytics");
|
|
111
113
|
}
|
|
112
114
|
};
|
|
113
115
|
export const sendError = (err, errCode) => {
|
|
@@ -115,13 +117,13 @@ export const sendError = (err, errCode) => {
|
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
119
|
const apiError = isNeonApiError(err) ? err : undefined;
|
|
118
|
-
const requestId = apiError?.headers?.[
|
|
120
|
+
const requestId = apiError?.headers?.["x-neon-ret-request-id"];
|
|
119
121
|
if (requestId) {
|
|
120
|
-
log.debug(
|
|
122
|
+
log.debug("Failed request ID: %s", requestId);
|
|
121
123
|
}
|
|
122
124
|
client.track({
|
|
123
|
-
event:
|
|
124
|
-
userId: userId ||
|
|
125
|
+
event: "CLI Error",
|
|
126
|
+
userId: userId || "anonymous",
|
|
125
127
|
properties: {
|
|
126
128
|
message: err.message,
|
|
127
129
|
stack: err.stack,
|
|
@@ -130,7 +132,7 @@ export const sendError = (err, errCode) => {
|
|
|
130
132
|
requestId: requestId,
|
|
131
133
|
},
|
|
132
134
|
});
|
|
133
|
-
log.debug(
|
|
135
|
+
log.debug("Sent CLI error event: %s", errCode);
|
|
134
136
|
};
|
|
135
137
|
export const trackEvent = (event, properties) => {
|
|
136
138
|
if (!client) {
|
|
@@ -138,14 +140,14 @@ export const trackEvent = (event, properties) => {
|
|
|
138
140
|
}
|
|
139
141
|
client.track({
|
|
140
142
|
event,
|
|
141
|
-
userId: userId ||
|
|
143
|
+
userId: userId || "anonymous",
|
|
142
144
|
properties,
|
|
143
145
|
});
|
|
144
|
-
log.debug(
|
|
146
|
+
log.debug("Sent CLI event: %s", event);
|
|
145
147
|
};
|
|
146
148
|
export const getAnalyticsEventProperties = (args) => ({
|
|
147
149
|
version: pkg.version,
|
|
148
|
-
command: args._.join(
|
|
150
|
+
command: args._.join(" "),
|
|
149
151
|
flags: {
|
|
150
152
|
output: args.output,
|
|
151
153
|
},
|
package/dist/api.js
CHANGED
|
@@ -11,29 +11,29 @@
|
|
|
11
11
|
// {@link isNeonApiError} and reads `error.status` / `error.data`. There is no
|
|
12
12
|
// axios anywhere in neonctl: requests go through the global `fetch`, and this is
|
|
13
13
|
// the one place HTTP errors are shaped.
|
|
14
|
-
import { Readable } from
|
|
15
|
-
import
|
|
16
|
-
import { createClient, createConfig } from
|
|
17
|
-
import
|
|
18
|
-
import { log } from
|
|
19
|
-
import pkg from
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
import * as raw from "@neon/sdk/raw";
|
|
16
|
+
import { createClient, createConfig } from "@neon/sdk/raw";
|
|
17
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
18
|
+
import { log } from "./log.js";
|
|
19
|
+
import pkg from "./pkg.js";
|
|
20
20
|
// Node's global `fetch` (undici) ignores HTTP_PROXY / HTTPS_PROXY / NO_PROXY,
|
|
21
21
|
// whereas the axios-based client neonctl used previously honored them. Restore
|
|
22
22
|
// that behaviour by installing a proxy-aware global dispatcher — but only when a
|
|
23
23
|
// proxy is actually configured, so the default (no-proxy) path stays untouched.
|
|
24
24
|
// This covers every `fetch` neonctl makes, including the direct S3 upload.
|
|
25
25
|
const PROXY_ENV_VARS = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
"HTTP_PROXY",
|
|
27
|
+
"http_proxy",
|
|
28
|
+
"HTTPS_PROXY",
|
|
29
|
+
"https_proxy",
|
|
30
|
+
"ALL_PROXY",
|
|
31
|
+
"all_proxy",
|
|
32
32
|
];
|
|
33
33
|
if (PROXY_ENV_VARS.some((name) => process.env[name])) {
|
|
34
34
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
35
35
|
}
|
|
36
|
-
const DEFAULT_API_HOST =
|
|
36
|
+
const DEFAULT_API_HOST = "https://console.neon.tech/api/v2";
|
|
37
37
|
const REQUEST_TIMEOUT_MS = 60000;
|
|
38
38
|
const USER_AGENT = `neonctl v${pkg.version}`;
|
|
39
39
|
/** Mirrors the api-client `ContentType` enum used by the `request()` escape hatch. */
|
|
@@ -53,7 +53,7 @@ export var ContentType;
|
|
|
53
53
|
export class NeonApiError extends Error {
|
|
54
54
|
constructor(message, init = {}) {
|
|
55
55
|
super(message);
|
|
56
|
-
this.name =
|
|
56
|
+
this.name = "NeonApiError";
|
|
57
57
|
this.status = init.status;
|
|
58
58
|
this.statusText = init.statusText;
|
|
59
59
|
this.data = init.data;
|
|
@@ -68,18 +68,18 @@ export function isNeonApiError(err) {
|
|
|
68
68
|
}
|
|
69
69
|
/** Extract a `message` string from a parsed error body, if present. */
|
|
70
70
|
export function messageFromBody(body) {
|
|
71
|
-
if (body && typeof body ===
|
|
71
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
72
72
|
const message = body.message;
|
|
73
|
-
if (typeof message ===
|
|
73
|
+
if (typeof message === "string")
|
|
74
74
|
return message;
|
|
75
75
|
}
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
78
|
/** Extract a machine-readable `code` string from a parsed error body, if present. */
|
|
79
79
|
export function codeFromBody(body) {
|
|
80
|
-
if (body && typeof body ===
|
|
80
|
+
if (body && typeof body === "object" && "code" in body) {
|
|
81
81
|
const code = body.code;
|
|
82
|
-
if (typeof code ===
|
|
82
|
+
if (typeof code === "string")
|
|
83
83
|
return code;
|
|
84
84
|
}
|
|
85
85
|
return undefined;
|
|
@@ -113,7 +113,7 @@ function headersToObject(headers) {
|
|
|
113
113
|
}
|
|
114
114
|
function isAbortError(err) {
|
|
115
115
|
return (err instanceof Error &&
|
|
116
|
-
(err.name ===
|
|
116
|
+
(err.name === "AbortError" || err.name === "TimeoutError"));
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
119
|
* Walk an error's `cause` chain to find the underlying socket/DNS `code` (e.g.
|
|
@@ -124,9 +124,9 @@ function isAbortError(err) {
|
|
|
124
124
|
function readSocketCode(err) {
|
|
125
125
|
let current = err;
|
|
126
126
|
for (let depth = 0; depth < 6 && current != null; depth++) {
|
|
127
|
-
if (typeof current ===
|
|
127
|
+
if (typeof current === "object" && "code" in current) {
|
|
128
128
|
const code = current.code;
|
|
129
|
-
if (typeof code ===
|
|
129
|
+
if (typeof code === "string")
|
|
130
130
|
return code;
|
|
131
131
|
}
|
|
132
132
|
current = current.cause;
|
|
@@ -160,10 +160,10 @@ function httpError(response, body) {
|
|
|
160
160
|
*/
|
|
161
161
|
function networkError(err) {
|
|
162
162
|
if (isAbortError(err)) {
|
|
163
|
-
return new NeonApiError(
|
|
163
|
+
return new NeonApiError("Request timed out", { code: "ECONNABORTED" });
|
|
164
164
|
}
|
|
165
165
|
return new NeonApiError(err instanceof Error ? err.message : String(err), {
|
|
166
|
-
code: readSocketCode(err) ??
|
|
166
|
+
code: readSocketCode(err) ?? "ENETWORK",
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
@@ -176,11 +176,11 @@ const timedFetch = async (input, init) => {
|
|
|
176
176
|
const signal = init?.signal
|
|
177
177
|
? AbortSignal.any([init.signal, timeout])
|
|
178
178
|
: timeout;
|
|
179
|
-
const method = init?.method ?? (input instanceof Request ? input.method :
|
|
179
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
180
180
|
const url = input instanceof Request ? input.url : String(input);
|
|
181
|
-
log.debug(
|
|
181
|
+
log.debug("%s %s", method.toUpperCase(), url);
|
|
182
182
|
const response = await fetch(input, { ...init, signal });
|
|
183
|
-
log.debug(
|
|
183
|
+
log.debug("%d %s", response.status, response.statusText);
|
|
184
184
|
return response;
|
|
185
185
|
};
|
|
186
186
|
const RETRY_COUNT = 5;
|
|
@@ -211,7 +211,7 @@ export const retryOnLock = async (fn) => {
|
|
|
211
211
|
throw errOut;
|
|
212
212
|
};
|
|
213
213
|
function buildUrl(apiHost, path, query) {
|
|
214
|
-
const url = new URL(`${apiHost.replace(/\/+$/,
|
|
214
|
+
const url = new URL(`${apiHost.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`);
|
|
215
215
|
if (query) {
|
|
216
216
|
for (const [key, value] of Object.entries(query)) {
|
|
217
217
|
if (value === undefined || value === null)
|
|
@@ -223,7 +223,7 @@ function buildUrl(apiHost, path, query) {
|
|
|
223
223
|
}
|
|
224
224
|
async function readJsonBody(response) {
|
|
225
225
|
const text = await response.text();
|
|
226
|
-
if (text.trim() ===
|
|
226
|
+
if (text.trim() === "")
|
|
227
227
|
return undefined;
|
|
228
228
|
try {
|
|
229
229
|
return JSON.parse(text);
|
|
@@ -241,7 +241,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
|
|
|
241
241
|
auth: () => apiKey,
|
|
242
242
|
baseUrl,
|
|
243
243
|
fetch: timedFetch,
|
|
244
|
-
headers: {
|
|
244
|
+
headers: { "User-Agent": USER_AGENT },
|
|
245
245
|
}));
|
|
246
246
|
/** Await a raw call, unwrap to a `{ data, status, headers }` envelope, or throw {@link NeonApiError}. */
|
|
247
247
|
async function call(run) {
|
|
@@ -254,7 +254,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
|
|
|
254
254
|
}
|
|
255
255
|
const response = result.response;
|
|
256
256
|
if (!response) {
|
|
257
|
-
throw networkError(result.error ?? new Error(
|
|
257
|
+
throw networkError(result.error ?? new Error("No response from Neon API"));
|
|
258
258
|
}
|
|
259
259
|
if (!response.ok) {
|
|
260
260
|
throw httpError(response, result.error ?? result.data);
|
|
@@ -274,7 +274,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
|
|
|
274
274
|
*/
|
|
275
275
|
async function request(params) {
|
|
276
276
|
const url = buildUrl(baseUrl, params.path, params.query);
|
|
277
|
-
const headers = {
|
|
277
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
278
278
|
if (params.secure !== false) {
|
|
279
279
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
280
280
|
}
|
|
@@ -284,7 +284,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
|
|
|
284
284
|
payload = params.body;
|
|
285
285
|
}
|
|
286
286
|
else if (params.body !== undefined) {
|
|
287
|
-
headers[
|
|
287
|
+
headers["Content-Type"] = ContentType.Json;
|
|
288
288
|
payload = JSON.stringify(params.body);
|
|
289
289
|
}
|
|
290
290
|
let response;
|
|
@@ -301,13 +301,13 @@ export const getApiClient = ({ apiKey, apiHost }) => {
|
|
|
301
301
|
if (!response.ok) {
|
|
302
302
|
// For a streamed download the error body arrives as a stream too; hand it
|
|
303
303
|
// back as a Node `Readable` so the caller can drain it for a message.
|
|
304
|
-
const errorBody = params.format ===
|
|
304
|
+
const errorBody = params.format === "stream" && response.body
|
|
305
305
|
? webStreamToNodeReadable(response.body)
|
|
306
306
|
: await readJsonBody(response);
|
|
307
307
|
throw httpError(response, errorBody);
|
|
308
308
|
}
|
|
309
309
|
let data;
|
|
310
|
-
if (params.format ===
|
|
310
|
+
if (params.format === "stream") {
|
|
311
311
|
data = response.body
|
|
312
312
|
? webStreamToNodeReadable(response.body)
|
|
313
313
|
: undefined;
|
package/dist/auth.js
CHANGED
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createServer } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import open from
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { extendTokenSet } from
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import open from "open";
|
|
6
|
+
import * as client from "openid-client";
|
|
7
|
+
import { sendError } from "./analytics.js";
|
|
8
|
+
import { matchErrorCode } from "./errors.js";
|
|
9
|
+
import { log } from "./log.js";
|
|
10
|
+
import { extendTokenSet } from "./utils/auth.js";
|
|
11
11
|
// oauth server timeouts
|
|
12
12
|
const SERVER_TIMEOUT = 10000;
|
|
13
13
|
// where to wait for incoming redirect request from oauth server to arrive
|
|
14
14
|
const REDIRECT_URI = (port) => `http://127.0.0.1:${port}/callback`;
|
|
15
15
|
// These scopes cannot be cancelled, they are always needed.
|
|
16
|
-
const ALWAYS_PRESENT_SCOPES = [
|
|
16
|
+
const ALWAYS_PRESENT_SCOPES = ["openid", "offline", "offline_access"];
|
|
17
17
|
const NEONCTL_SCOPES = [
|
|
18
18
|
...ALWAYS_PRESENT_SCOPES,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
"urn:neoncloud:projects:create",
|
|
20
|
+
"urn:neoncloud:projects:read",
|
|
21
|
+
"urn:neoncloud:projects:update",
|
|
22
|
+
"urn:neoncloud:projects:delete",
|
|
23
|
+
"urn:neoncloud:orgs:create",
|
|
24
|
+
"urn:neoncloud:orgs:read",
|
|
25
|
+
"urn:neoncloud:orgs:update",
|
|
26
|
+
"urn:neoncloud:orgs:delete",
|
|
27
|
+
"urn:neoncloud:orgs:permission",
|
|
28
28
|
];
|
|
29
29
|
const AUTH_TIMEOUT_SECONDS = 60;
|
|
30
|
-
export const defaultClientID =
|
|
30
|
+
export const defaultClientID = "neonctl";
|
|
31
31
|
export const refreshToken = async ({ oauthHost, clientId, allowUnsafeTls }, tokenSet) => {
|
|
32
|
-
log.debug(
|
|
33
|
-
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method:
|
|
32
|
+
log.debug("Discovering oauth server");
|
|
33
|
+
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
|
|
34
34
|
timeout: SERVER_TIMEOUT,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
execute: allowUnsafeTls
|
|
36
|
+
? // eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
37
|
+
[client.allowInsecureRequests]
|
|
38
|
+
: undefined,
|
|
37
39
|
});
|
|
38
40
|
return await client.refreshTokenGrant(configuration, tokenSet.refresh_token);
|
|
39
41
|
};
|
|
40
42
|
export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
|
|
41
|
-
log.debug(
|
|
42
|
-
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method:
|
|
43
|
+
log.debug("Discovering oauth server");
|
|
44
|
+
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
|
|
43
45
|
timeout: SERVER_TIMEOUT,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
execute: allowUnsafeTls
|
|
47
|
+
? // eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
48
|
+
[client.allowInsecureRequests]
|
|
49
|
+
: undefined,
|
|
46
50
|
});
|
|
47
51
|
//
|
|
48
52
|
// Start HTTP server and wait till /callback is hit
|
|
49
53
|
//
|
|
50
|
-
log.debug(
|
|
54
|
+
log.debug("Starting HTTP Server for callback");
|
|
51
55
|
const server = createServer();
|
|
52
|
-
server.listen(0,
|
|
56
|
+
server.listen(0, "127.0.0.1", function () {
|
|
53
57
|
log.debug(`Listening on port ${this.address().port}`);
|
|
54
58
|
});
|
|
55
|
-
await new Promise((resolve) => server.once(
|
|
59
|
+
await new Promise((resolve) => server.once("listening", resolve));
|
|
56
60
|
const listen_port = server.address().port;
|
|
57
61
|
// https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
|
|
58
62
|
const state = client.randomState();
|
|
@@ -67,17 +71,17 @@ export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
|
|
|
67
71
|
//
|
|
68
72
|
// Wait for callback and follow oauth flow.
|
|
69
73
|
//
|
|
70
|
-
if (!request.url?.startsWith(
|
|
74
|
+
if (!request.url?.startsWith("/callback")) {
|
|
71
75
|
response.writeHead(404);
|
|
72
76
|
response.end();
|
|
73
77
|
return;
|
|
74
78
|
}
|
|
75
79
|
// process the CORS preflight OPTIONS request
|
|
76
|
-
if (request.method ===
|
|
80
|
+
if (request.method === "OPTIONS") {
|
|
77
81
|
response.writeHead(200, {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
"Access-Control-Allow-Origin": "*",
|
|
83
|
+
"Access-Control-Allow-Methods": "GET, POST",
|
|
84
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
81
85
|
});
|
|
82
86
|
response.end();
|
|
83
87
|
return;
|
|
@@ -87,29 +91,31 @@ export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
|
|
|
87
91
|
pkceCodeVerifier: codeVerifier,
|
|
88
92
|
expectedState: state,
|
|
89
93
|
});
|
|
90
|
-
response.writeHead(200, {
|
|
91
|
-
createReadStream(join(fileURLToPath(new URL(
|
|
94
|
+
response.writeHead(200, { "Content-Type": "text/html" });
|
|
95
|
+
createReadStream(join(fileURLToPath(new URL(".", import.meta.url)), "./callback.html")).pipe(response);
|
|
92
96
|
clearTimeout(timer);
|
|
93
97
|
const exp = new Date();
|
|
94
98
|
exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
|
|
95
99
|
resolve(extendTokenSet(tokenSet));
|
|
96
100
|
server.close();
|
|
97
101
|
};
|
|
98
|
-
server.on(
|
|
102
|
+
server.on("request", (req, res) => {
|
|
99
103
|
void onRequest(req, res);
|
|
100
104
|
});
|
|
101
105
|
//
|
|
102
106
|
// Open browser to let user authenticate
|
|
103
107
|
//
|
|
104
|
-
const scopes = clientId == defaultClientID
|
|
108
|
+
const scopes = clientId == defaultClientID
|
|
109
|
+
? NEONCTL_SCOPES
|
|
110
|
+
: ALWAYS_PRESENT_SCOPES;
|
|
105
111
|
const authUrl = client.buildAuthorizationUrl(configuration, {
|
|
106
|
-
scope: scopes.join(
|
|
112
|
+
scope: scopes.join(" "),
|
|
107
113
|
state,
|
|
108
114
|
code_challenge: codeChallenge,
|
|
109
|
-
code_challenge_method:
|
|
115
|
+
code_challenge_method: "S256",
|
|
110
116
|
redirect_uri: REDIRECT_URI(listen_port),
|
|
111
117
|
});
|
|
112
|
-
log.info(
|
|
118
|
+
log.info("Awaiting authentication in web browser.");
|
|
113
119
|
log.info(`Auth Url: ${authUrl}`);
|
|
114
120
|
open(authUrl.href).catch((err) => {
|
|
115
121
|
const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { tryCurrentBranchFastPath } from
|
|
2
|
+
import { tryCurrentBranchFastPath } from "./current_branch_fast_path.js";
|
|
3
3
|
// Fast path for the offline `(config) status --current-branch` probe (used by shell
|
|
4
4
|
// prompts): read the pinned branch from `.neon` without loading the full command tree,
|
|
5
5
|
// api-client, and yargs (~200ms). Falls through to the full CLI for everything else, so
|
|
6
6
|
// the heavy `index.js` is imported lazily and only when actually needed.
|
|
7
7
|
if (!tryCurrentBranchFastPath(process.argv)) {
|
|
8
|
-
void import(
|
|
8
|
+
void import("./index.js");
|
|
9
9
|
}
|