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/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
|
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { getApiClient } from
|
|
5
|
-
import { auth, refreshToken } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { isCi } from
|
|
9
|
-
import { log } from
|
|
10
|
-
import { extendTokenSet } from
|
|
11
|
-
export const command =
|
|
12
|
-
export const aliases = [
|
|
13
|
-
export const describe =
|
|
14
|
-
export const builder = (yargs) => yargs.option(
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getApiClient } from "../api.js";
|
|
5
|
+
import { auth, refreshToken } from "../auth.js";
|
|
6
|
+
import { CREDENTIALS_FILE } from "../config.js";
|
|
7
|
+
import { isConfigInit, isCurrentBranchProbe } from "../context.js";
|
|
8
|
+
import { isCi } from "../env.js";
|
|
9
|
+
import { log } from "../log.js";
|
|
10
|
+
import { extendTokenSet } from "../utils/auth.js";
|
|
11
|
+
export const command = "auth";
|
|
12
|
+
export const aliases = ["login"];
|
|
13
|
+
export const describe = "Authenticate";
|
|
14
|
+
export const builder = (yargs) => yargs.option("context-file", {
|
|
15
15
|
hidden: true,
|
|
16
16
|
});
|
|
17
17
|
export const handler = async (args) => {
|
|
18
18
|
await authFlow(args);
|
|
19
19
|
};
|
|
20
|
-
export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth,
|
|
20
|
+
export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, "force-auth": forceAuthKebab, allowUnsafeTls, }) => {
|
|
21
21
|
const allowInteractiveAuth = forceAuth ?? forceAuthKebab;
|
|
22
22
|
if (!allowInteractiveAuth && isCi()) {
|
|
23
|
-
throw new Error(
|
|
23
|
+
throw new Error("Cannot run interactive auth in CI");
|
|
24
24
|
}
|
|
25
25
|
const tokenSet = await auth({
|
|
26
26
|
oauthHost: oauthHost,
|
|
@@ -30,16 +30,16 @@ export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceA
|
|
|
30
30
|
const credentialsPath = join(configDir, CREDENTIALS_FILE);
|
|
31
31
|
try {
|
|
32
32
|
await preserveCredentials(credentialsPath, tokenSet, getApiClient({
|
|
33
|
-
apiKey: tokenSet.access_token ||
|
|
33
|
+
apiKey: tokenSet.access_token || "",
|
|
34
34
|
apiHost,
|
|
35
35
|
}));
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
38
|
-
log.error(
|
|
39
|
-
return
|
|
38
|
+
log.error("Failed to save credentials");
|
|
39
|
+
return "";
|
|
40
40
|
}
|
|
41
|
-
log.info(
|
|
42
|
-
return tokenSet.access_token ||
|
|
41
|
+
log.info("Auth complete");
|
|
42
|
+
return tokenSet.access_token || "";
|
|
43
43
|
};
|
|
44
44
|
const preserveCredentials = async (path, credentials, apiClient) => {
|
|
45
45
|
const { data: { id }, } = await apiClient.getCurrentUserInfo();
|
|
@@ -52,13 +52,13 @@ const preserveCredentials = async (path, credentials, apiClient) => {
|
|
|
52
52
|
writeFileSync(path, contents, {
|
|
53
53
|
mode: 0o700,
|
|
54
54
|
});
|
|
55
|
-
log.debug(
|
|
56
|
-
log.debug(
|
|
55
|
+
log.debug("Saved credentials to %s", path);
|
|
56
|
+
log.debug("Credentials MD5 hash: %s", md5hash(contents));
|
|
57
57
|
};
|
|
58
58
|
const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
59
59
|
// Use existing access_token, if present and valid
|
|
60
60
|
if (tokenSet.access_token && tokenSet.expires_at > Date.now()) {
|
|
61
|
-
log.debug(
|
|
61
|
+
log.debug("Using existing valid access_token");
|
|
62
62
|
const apiClient = getApiClient({
|
|
63
63
|
apiKey: tokenSet.access_token,
|
|
64
64
|
apiHost: props.apiHost,
|
|
@@ -67,10 +67,10 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
|
67
67
|
}
|
|
68
68
|
// Either access_token is missing or its expired. Refresh the token
|
|
69
69
|
log.debug(tokenSet.expires_at < Date.now()
|
|
70
|
-
?
|
|
71
|
-
:
|
|
70
|
+
? "Token is expired, attempting refresh"
|
|
71
|
+
: "Token is missing access_token, attempting refresh");
|
|
72
72
|
if (!tokenSet.refresh_token) {
|
|
73
|
-
log.debug(
|
|
73
|
+
log.debug("TokenSet is missing refresh_token, starting authentication");
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
try {
|
|
@@ -87,13 +87,13 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
|
|
|
87
87
|
apiHost: props.apiHost,
|
|
88
88
|
});
|
|
89
89
|
await preserveCredentials(credentialsPath, extendedTokenSet, apiClient);
|
|
90
|
-
log.debug(
|
|
90
|
+
log.debug("Token refresh successful");
|
|
91
91
|
return { apiKey, apiClient };
|
|
92
92
|
}
|
|
93
93
|
catch (err) {
|
|
94
|
-
const typedErr = err instanceof Error ? err : new Error(
|
|
95
|
-
log.debug(
|
|
96
|
-
throw new Error(
|
|
94
|
+
const typedErr = err instanceof Error ? err : new Error("Unknown error");
|
|
95
|
+
log.debug("Failed to refresh token: %s", typedErr.message);
|
|
96
|
+
throw new Error("AUTH_REFRESH_FAILED");
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
export const ensureAuth = async (props) => {
|
|
@@ -107,23 +107,28 @@ export const ensureAuth = async (props) => {
|
|
|
107
107
|
if (isCurrentBranchProbe(props)) {
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
|
+
// `config init` only scaffolds a neon.ts and installs npm packages locally; it
|
|
111
|
+
// never calls the Neon API, so skip auth entirely — no token refresh, no login.
|
|
112
|
+
if (isConfigInit(props)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
110
115
|
// `dev` runs a function locally. It injects the selected branch's env vars
|
|
111
116
|
// when credentials happen to be available, but must never trigger an
|
|
112
117
|
// interactive login: use an API key or existing stored credentials if
|
|
113
118
|
// present, otherwise run with no API client (env injection is skipped).
|
|
114
|
-
const isLocalDev = props._[0] ===
|
|
119
|
+
const isLocalDev = props._[0] === "dev";
|
|
115
120
|
// `bootstrap` only copies a public template repo; it never calls the Neon
|
|
116
121
|
// API, so it must work without credentials and must never pop a browser
|
|
117
122
|
// login. It uses an API key / stored credentials when present (harmless),
|
|
118
123
|
// otherwise it proceeds with no API client.
|
|
119
|
-
const isBootstrap = props._[0] ===
|
|
124
|
+
const isBootstrap = props._[0] === "bootstrap";
|
|
120
125
|
// `init` manages its own auth flow (asks the user if they have an account,
|
|
121
126
|
// then triggers OAuth at the right time). Skip the global auth middleware.
|
|
122
|
-
const isInit = props._[0] ===
|
|
127
|
+
const isInit = props._[0] === "init";
|
|
123
128
|
// Use existing API key or handle auth command
|
|
124
|
-
if (props.apiKey || props._[0] ===
|
|
129
|
+
if (props.apiKey || props._[0] === "auth") {
|
|
125
130
|
if (props.apiKey) {
|
|
126
|
-
log.debug(
|
|
131
|
+
log.debug("Using an API key to authorize requests");
|
|
127
132
|
}
|
|
128
133
|
props.apiClient = getApiClient({
|
|
129
134
|
apiKey: props.apiKey,
|
|
@@ -134,10 +139,10 @@ export const ensureAuth = async (props) => {
|
|
|
134
139
|
const credentialsPath = join(props.configDir, CREDENTIALS_FILE);
|
|
135
140
|
// Handle case when credentials file exists
|
|
136
141
|
if (existsSync(credentialsPath)) {
|
|
137
|
-
log.debug(
|
|
142
|
+
log.debug("Trying to read credentials from %s", credentialsPath);
|
|
138
143
|
try {
|
|
139
|
-
const contents = readFileSync(credentialsPath,
|
|
140
|
-
log.debug(
|
|
144
|
+
const contents = readFileSync(credentialsPath, "utf8");
|
|
145
|
+
log.debug("Credentials MD5 hash: %s", md5hash(contents));
|
|
141
146
|
const tokenSet = JSON.parse(contents);
|
|
142
147
|
// Try to use existing token or refresh it
|
|
143
148
|
const result = await handleExistingToken(tokenSet, props, credentialsPath);
|
|
@@ -148,32 +153,33 @@ export const ensureAuth = async (props) => {
|
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
catch (err) {
|
|
151
|
-
if (!(err instanceof Error &&
|
|
152
|
-
err.
|
|
156
|
+
if (!(err instanceof Error &&
|
|
157
|
+
err.message === "AUTH_REFRESH_FAILED") &&
|
|
158
|
+
err.code !== "ENOENT" &&
|
|
153
159
|
!(err instanceof SyntaxError)) {
|
|
154
160
|
// Throw for any errors except auth refresh failure, missing file, or invalid credentials file
|
|
155
161
|
throw err;
|
|
156
162
|
}
|
|
157
163
|
// Fall through to new auth flow for auth failures
|
|
158
|
-
log.debug(
|
|
164
|
+
log.debug("Ensure auth failed, starting authentication", err);
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
else {
|
|
162
|
-
log.debug(
|
|
168
|
+
log.debug("Credentials file %s does not exist, starting authentication", credentialsPath);
|
|
163
169
|
}
|
|
164
170
|
// `dev` never launches the interactive browser flow. With no usable
|
|
165
171
|
// credentials it proceeds without an API client; env injection is skipped
|
|
166
172
|
// and the function still runs locally.
|
|
167
173
|
if (isLocalDev) {
|
|
168
|
-
log.debug(
|
|
174
|
+
log.debug("dev: no usable credentials; running without env injection");
|
|
169
175
|
return;
|
|
170
176
|
}
|
|
171
177
|
if (isBootstrap) {
|
|
172
|
-
log.debug(
|
|
178
|
+
log.debug("bootstrap: no usable credentials; continuing without auth");
|
|
173
179
|
return;
|
|
174
180
|
}
|
|
175
181
|
if (isInit) {
|
|
176
|
-
log.debug(
|
|
182
|
+
log.debug("init: skipping global auth; init manages its own auth flow");
|
|
177
183
|
return;
|
|
178
184
|
}
|
|
179
185
|
// Start new auth flow if no valid token exists or refresh failed
|
|
@@ -193,16 +199,16 @@ export const deleteCredentials = (configDir) => {
|
|
|
193
199
|
try {
|
|
194
200
|
if (existsSync(credentialsPath)) {
|
|
195
201
|
rmSync(credentialsPath);
|
|
196
|
-
log.info(
|
|
202
|
+
log.info("Deleted credentials from %s", credentialsPath);
|
|
197
203
|
}
|
|
198
204
|
else {
|
|
199
|
-
log.debug(
|
|
205
|
+
log.debug("Credentials file %s does not exist", credentialsPath);
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
catch (err) {
|
|
203
|
-
const typedErr = err instanceof Error ? err : new Error(
|
|
204
|
-
log.error(
|
|
205
|
-
throw new Error(
|
|
209
|
+
const typedErr = err instanceof Error ? err : new Error("Unknown error");
|
|
210
|
+
log.error("Failed to delete credentials: %s", typedErr.message);
|
|
211
|
+
throw new Error("CREDENTIALS_DELETE_FAILED");
|
|
206
212
|
}
|
|
207
213
|
};
|
|
208
|
-
const md5hash = (s) => createHash(
|
|
214
|
+
const md5hash = (s) => createHash("md5").update(s).digest("hex");
|